# CM AI Docking Port

## Reception Protocol Spec v1.0 (usklajena specifikacija)

*Nastane 2026-09-29 z združitvijo: Master Development Prompt v0.1 (osnovni koncept), Gemini addon ("Planning Draft – implementacijski koraki"), Grok addon ("Planning Draft – Protocol & First Files"). Ta dokument je zdaj referenčna verzija — v0.1 in oba addona ostanejo kot zgodovina razmisleka, ne kot ločena navodila.*

*Danes nastane samo ta dokument. Prve datoteke (manifest.json, index.html, reception.php) pridejo naslednjič, na dogovorjeni lokaciji (glej §12).*

> **Update (2026-10-07):** An epilogue reflecting on this project's current direction — what has been demonstrated so far, the practical limits of today's mass-market AI tooling, and why closed, connected deployments look like the protocol's strongest near-term use case — has been published separately for anyone researching this work further: [CM AI Docking Port: From an AI Integration Experiment to a Protocol for Connected Systems](https://apartmamatevz.si/journal/posts/2026-10-07-cm-ai-docking-port-from-experiment-to-protocol-for-connected-systems.html).


---

## 1. Namen (nespremenjeno iz v0.1)

CM AI Docking Port je univerzalni vstopni mehanizem, skozi katerega AI agent odkrije CM Ecosystem in z njim komunicira preko standardnega HTTP/JSON protokola — brez CM-specifičnega vtičnika.

Ni nov CM, ni nov poslovni motor, ni centralni marketplace, ni AI agent. CM ostaje edini vir resnice (source of truth). Docking Port je tanek protokolarni/usmerjevalni sloj nad obstoječimi javnimi API-ji (dokaz koncepta: `GET /app/public/api/availability_multi.php`, ki že vrača čist, semantično razumljiv JSON — glej realno implementacijo na `/var/www/html/app/public/api/availability_multi.php`).

---

## 2. Ključno usklajevalno načelo (Grok, potrjuje in izostri v0.1 §6/§14)

To je edina prava **sprememba/izostritev** glede na v0.1 — vse ostalo se le dopolnjuje.

> **AI agent nikoli ne kliče drugih nodov ali obstoječih poslovnih API-jev neposredno. Agent komunicira izključno z Reception svojega vstopnega noda.**

```text
AI Agent
   │
   ▼
CM AI Docking Port (manifest + docs) — odkritje
   │
   ▼
Reception — EDINA točka komunikacije
   │
   ├── local CM (kliče availability_multi.php interno)
   ├── nearby nodes (forward envelope, agent tega ne vidi)
   └── deeper nodes (znotraj max_depth)
   │
   ▼
agregiran odgovor → nazaj do AI Agent
```

Model je "pošta": agent odda zahtevek na Reception, počaka, dobi en sam agregiran odgovor. Agent ni usmerjevalec in ne pozna interne topologije mreže — to ohranja decentralizacijo (v0.1 §9) in hkrati preprečuje, da bi agent postal vektor za zanke, zlorabo ali fragmentacijo protokola.

Vse iz v0.1 §7–§12 (geografski routing, verižna poizvedba, globina, zaščita pred kroženjem) ostaja veljavno — le izvaja ga Reception rekurzivno, agent tega procesa ne vidi in vanj ne posega.

---

## 3. Dve ravni dostopa (nespremenjeno iz v0.1 §5)

- **A. Local CM access** — javno izpostavljene funkcije konkretne instalacije (npr. availability).
- **B. Ecosystem access** — isti Docking Port, a zahtevek gre v skupno Reception, ki ga po potrebi usmeri naprej po mreži.

---

## 4. Geografija kot routing princip (nespremenjeno iz v0.1 §7–§8)

Vstopna točka ≠ cilj iskanja. Agent lahko potrka na CM v Radovljici in vpraša za Ljubljano — Reception usmerja glede na `target.location`, ne glede na lokacijo vstopnega noda.

---

## 5. Request Envelope — usklajena končna shema

Osnova iz v0.1 §13, dopolnjena s konkretnimi vrednostmi iz Grok addona.

```json
{
  "protocol": "CM-AI-Docking-Port",
  "version": "0.1",
  "request_id": "req-2026-08-10-abc123",
  "request_expiry": "2026-08-10T12:00:00Z",
  "origin": "ai-agent",
  "target": {
    "location": { "lat": 46.05, "lon": 14.50, "label": "Ljubljana" },
    "radius_km": 30
  },
  "query": {
    "type": "availability",
    "from": "2026-08-10",
    "to": "2026-08-16",
    "guests": 2,
    "units": null
  },
  "limits": {
    "max_depth": 2,
    "max_distance_km": 40,
    "max_results": 10
  },
  "visited": []
}
```

Pravila:

- `request_id` unikaten, potuje z zahtevkom (v0.1 §12 — zaznava podvajanja).
- `request_expiry` **OBVEZEN** (v0.1 §11/§9). Popravek 2026-09-30 (GPT catch): spec je od v0.1 naprej to polje navajala med obveznimi varovalkami, a ga noben Request Envelope primer (vključno s Korakom 2 kodo) dejansko ni vseboval — neusklajenost je obstajala od začetka, nihče je ni opazil do zdaj. Reception (od tega popravka dalje) preveri **samo prisotnost** tega polja — format/semantika ("kdaj zahtevek zares poteče") **ni definirana in se namenoma ne izmišlja** v Koraku 2; to je odprto vprašanje za forwarding fazo (Korak 3), ko bo `request_expiry` dejansko pomemben za nadzor nad tem, kako dolgo se zahtevek sme širiti po mreži.
- `visited` dopolnjuje **Reception**, ne agent (v0.1 §12 — preprečuje zanke A→B→C→A).
- `target.location` je cilj iskanja, ločen od vstopne točke (§4).
- `query.type` je za MVP omejen na `"availability"` (v0.1 §15, §23).
- Agent ne sme dodajati lastnih hopov ali polj izven te sheme.
- `max_depth`/`max_distance_km`/`max_results` niso strogo obvezni na ravni Reception validacije — imajo smiselne privzete vrednosti iz `limits_default` v manifestu, če jih agent izpusti. To namerno razlikuje njihovo obravnavo od `request_expiry`, za katerega ni smiselnega privzetka, ki bi ga lahko strežnik izmislil namesto agenta.

---

## 6. Response Envelope — nova dopolnitev (Grok; v0.1 tega ni imel eksplicitno)

v0.1 §15 je pokazal samo surov odgovor `availability_multi.php`. Manjkala je ovojnica za agregiran, multi-node odgovor. To je zapolnjena vrzel:

```json
{
  "protocol": "CM-AI-Docking-Port",
  "version": "0.1",
  "request_id": "req-2026-08-10-abc123",
  "ok": true,
  "partial": false,
  "search_meta": {
    "entry_node": "CM-AI-DEMO-001",
    "target_location": { "lat": 46.05, "lon": 14.50, "label": "Ljubljana" },
    "nodes_queried": 1,
    "nodes_reached": 1,
    "max_depth_used": 0,
    "max_depth_limit": 2
  },
  "results": [
    {
      "node_id": "CM-AI-DEMO-001",
      "depth": 0,
      "source": "local_availability_api",
      "freshness": { "generated_at": "2026-09-28T18:18:29+00:00" },
      "data": { "...": "neposreden izhod availability_multi.php, brez reinterpretacije" }
    }
  ],
  "errors": []
}
```

Pravilo (v0.1 §14 potrjeno): Reception **ne reinterpretira** poslovnega odgovora — `data` je surov izhod obstoječega API-ja, samo ovit s provenance (`node_id`, `depth`, `source`, `freshness`). `partial: true` + razlog, kadar kak node ni bil dosegljiv (v0.1 §20 — brez lažnega vtisa popolne pokritosti).

---

## 7. Manifest — usklajena končna shema

Združuje v0.1 §16 (osnovna struktura) z Grokovo dopolnitvijo (capability-level metadata: `side_effect`, `requires_consent` — neposredno iz v0.1 §22 zahteve).

Objavljen na dveh mestih (ena vsebina, ena morda redirect na drugo):
```text
/.well-known/cm-ai-docking-port.json
/app/public/ai/manifest.json
```

```json
{
  "protocol": "CM-AI-Docking-Port",
  "version": "0.1",
  "service": {
    "name": "CM Ecosystem Reception",
    "type": "ai-entry-point",
    "node_id": "CM-AI-DEMO-058",
    "location": { "label": "Lancovo / Radovljica area (Demo 58)", "lat": 46.34, "lon": 14.17 }
  },
  "capabilities": {
    "local": [
      { "id": "availability", "side_effect": false, "requires_consent": false,
        "description": "Check unit availability for a date range" }
    ],
    "ecosystem": [
      { "id": "accommodation-discovery", "side_effect": false, "requires_consent": false,
        "description": "Geographic discovery across participating CM nodes via Reception" }
    ]
  },
  "reception": { "method": "POST", "endpoint": "/app/public/ai/reception.php", "content_type": "application/json" },
  "documentation": {
    "human": "/app/public/ai/index.html",
    "machine": "/app/public/ai/manifest.json",
    "spec": {
      "html": "/app/public/ai/spec.html",
      "markdown": "/app/public/ai/spec.md",
      "note": "Full working design specification. SKLENJENO sections are settled; OSNUTEK sections are open drafts.",
      "spec_synced_at": "<ISO8601, auto-updated on each publish>"
    }
  },
  "limits_default": { "max_depth": 2, "max_distance_km": 50, "max_results": 20 },
  "notes": [
    "AI agents must always communicate with Reception.",
    "Do not call internal business APIs directly.",
    "Entry point and search target are separate concepts."
  ]
}
```

**Dodano 2026-09-30 (Dvojček/Gemini predlog, uporabnik potrdil)**: manifest zdaj eksplicitno referencira ta dokument (`documentation.spec`, obe obliki) in nosi `spec_synced_at` — kdaj je bila javna kopija specifikacije nazadnje osvežena iz lokalnega delovnega izvoda. Objava ni ročna: `~/Dokumenti/Guestbook/publish_ai_docking_spec.sh` v enem koraku redigira LAN podrobnosti, pretvori `.md` v stilizirano `spec.html`, deploya oboje na instanco 58, in posodobi `spec_synced_at` na obeh manifestih (`/app/public/ai/manifest.json` in `/.well-known/cm-ai-docking-port.json`). Poganja se ročno ob vsaki vsebinski spremembi tega dokumenta — ni (še) avtomatiziran cron.

---

## 8. Vloga `availability_multi.php` (nespremenjeno, potrjeno v0.1 §15 + Gemini §Korak2)

```text
Reception (lokalni node)
   │  preveri: je target v dosegu? je depth 0?
   ▼
kliče lastni GET /app/public/api/availability_multi.php?units=...&from=...&to=...&mode=basic
   │
   ▼
vzame JSON odgovor, vstavi v results[] z provenance (§6)
   │
   ▼ (če max_depth > 0 in potrebno)
forwarda envelope sosedom, zbira odgovore
   ▼
vrne Response Envelope agentu
```

Docking Port **ne posega** v `availability_multi.php` ali katerikoli drug obstoječi endpoint. Klic je interni, agent ga ne vidi.

---

## 9. Varovalke in omejitve (nespremenjeno, potrjeno iz vseh treh virov)

Iz v0.1 §11, §12, §19, §21, §22, §25, dodatno eksplicirano v Gemini addonu kot "Opozorilo za razvijalca":

**Prepovedano v prvi fazi (MVP):**

- 🚫 Centralni master-registry strežnik vseh nodov — usmerjanje je P2P / sosedske liste (v0.1 §9).
- 🚫 Poseg v obstoječo poslovno logiko (rezervacije, cene) — Docking Port se samo "priklopi".
- 🚫 Write operacije preko Reception (booking, inquiry submit) — MVP je read/discovery only (v0.1 §22).
- 🚫 Vezava na en AI stack/ponudnik/težke knjižnice — čist HTTP/JSON, nativen PHP skladen z obstoječim CM Free stackom.
- 🚫 Cache kot source of truth — availability ostane avtoritativna iz konkretnega CM (v0.1 §21).
- 🚫 Agent, ki sam skaka med nodi (§2 zgoraj).

**Obvezno:**

- `max_depth`, `max_distance_km`, `max_results`, `request_expiry` na vsakem zahtevku (v0.1 §11).
- `request_id` + `visited` proti kroženju in podvajanju (v0.1 §12).
- Vsaka capability označena z `side_effect` in `requires_consent` (v0.1 §22, §7 zgoraj).
- Izpad noda ne sme prekiniti celotnega iskanja — `partial: true` namesto lažne popolnosti (v0.1 §20).

---

## 10. Human-readable dokumentacija (`/app/public/ai/index.html`) — nespremenjeno iz v0.1 §17

Kratka stran, berljiva ljudem in LLM-jem (jasni `<h1>`/`<code>` bloki, brez težkega JS-a), ki razloži: kaj je Docking Port, kaj je Reception (edina točka za AI), primer Request/Response Envelope, da agent ne kliče notranjih API-jev neposredno, omejitve, in da gre za read/discovery MVP.

---

## 11. MVP koraki — usklajen zaporedni načrt

Združitev v0.1 §23–§25 (postopnost, "ne graditi prezgodaj") + Gemini addon (4 konkretni koraki) + Grok addon (datotečni seznam).

```text
Korak 0 (danes, 2026-09-29): ta usklajena specifikacija ✅

Korak 1: Static Entry
  /.well-known/cm-ai-docking-port.json  ✅ DEPLOYANO 2026-09-29 (uporabnik ročno naredil sudo mkdir
                                            +chown na instanci 58, Claude Code postavil vsebino),
                                            potrjeno: https://ai-cmfree.duckdns.org/.well-known/cm-ai-docking-port.json (HTTP 200)
  /app/public/ai/manifest.json          ✅ DEPLOYANO 2026-09-29 (node_id CM-AI-DEMO-058,
                                            usklajen skozi Claude+Gemini+Grok+GPT review),
                                            potrjeno javno dostopno: https://ai-cmfree.duckdns.org/app/public/ai/manifest.json (HTTP 200)
  /app/public/ai/index.html             ✅ DEPLOYANO 2026-09-29 (Claude Code pripravil vsebino,
                                            uporabnik obrnil vrstni red: tokrat Claude pred skupino),
                                            potrjeno: https://ai-cmfree.duckdns.org/app/public/ai/index.html (HTTP 200)

Korak 1 zaključen v celoti.

Korak 2: Reception (single node, depth 0)          ✅ DEPLOYANO 2026-09-29
  /app/public/ai/reception.php
  - koda: Gemini prvi osnutek → GPT (izčrpen review proti živemu API-ju, 5 popravkov) → Claude
    (preverjeno: curl extenzija na 58 obstaja, Apache ima en sam catch-all vhost torej Host-header
    ni nujen a je neškodljiv, `availability_multi.php` dejansko vrača `generated_at`) → deployano
    z `.tmp` + `php -l` lint + atomski `mv`, brez okna polomljene sintakse v živo
  - popravki proti Geminijevemu prvotnemu osnutku: `units: null` → interni `units=all`; lokalni
    fail = `ok:false` (ne `partial:true`, to je rezervirano za Korak 3); VEDNO Response Envelope
    tudi na 400/405; samo POST (+OPTIONS/CORS); agent ne vidi internega URL-ja/HTTP kode/curl napake
  - Claude dodal: `error_log()` server-side ob curl napaki (samo v log, agent ga ne vidi)
  - testirano 5 scenarijev na živem endpointu: veljaven zahtevek (ok:true, L1 available),
    GET→405+envelope, napačen protocol→400+envelope, manjkajo from/to→200+missing_dates,
    nepodprt query.type→200+unsupported_query_type — vsi po pričakovanjih
  - endpoint: https://ai-cmfree.duckdns.org/app/public/ai/reception.php
  - index.html status badge posodobljen na "live" ŠELE po uspešnem testu (ne hkrati z uploadom)

Naslednje: Korak 3 (neighbour table + geografski forwarding) — podroben načrt v §16, testni scenarij v §13.
```

Šele po potrjenem Koraku 4 razmišljati o širšem discoveryju, kompleksnejšem routingu, cache-u, globalnih indeksih (v0.1 §25 — to velja nespremenjeno).

---

## 12. Lokacija izvedbe — odločeno danes (2026-09-29)

Prve dejanske datoteke (Korak 1–2) gredo na **ai-cmfree demo, instanca 58** (`ai-cmfree.duckdns.org` → GN7 reverse-proxy → 58; glej memorijo `ai_bot_demo_public_access`) — brez pravih podatkov, `robots.txt` že odprt, obstoječa promotion-testing površina. To je edino okolje kjer read/discovery MVP nima tveganja glede realnih gostov/rezervacij.

Realni endpoint `availability_multi.php` (referenca za obliko odgovora, §1/§8 zgoraj) je preverjen na GN7 (`/var/www/html/app/public/api/availability_multi.php`) — instanca 58 ima svojo kopijo istega API-ja, path je treba potrditi ob dejanski implementaciji (ni nujno identičen `/app/public/api/...`).

---

## 13. Testni scenarij (usklajen iz v0.1 §24 + Gemini + Grok)

Tri testne CM/node instalacije na različnih lokacijah (Korak 4). Preveriti:

1. Agent najde manifest, prebere capabilities in reception endpoint.
2. Zahtevek za lokalno availability (depth 0) vrne pravilen Response Envelope.
3. Zahtevek s ciljem na drugi lokaciji pravilno potuje Node A → relevanten sosed in vrne rezultat od tam.
4. `visited` prepreči zanko (A vabi B, B vabi A).
5. `max_depth` ustavi širjenje na pravem koraku.
6. `request_id` prepreči podvojeno obravnavo istega zahtevka.
7. Izpad enega noda (B nedosegljiv) ne prekine iskanja — rezultat vsebuje `partial: true` in jasno pove obseg, ne pretvarja se, da je pregledan cel Ecosystem.
8. Odgovor je agentu semantično jasen brez dodatne razlage.

### Testna infrastruktura — pripravljena 2026-09-29

Tri instalacije za zgornji test:

| Node | node_id | Lokacija (lat/lon) | Kaj je | URL |
|---|---|---|---|---|
| A (pravi) | `CM-AI-DEMO-058` | Lancovo/Radovljica (46.34, 14.17) | Prava CM instanca, prava `availability_multi.php` podatka | `https://ai-cmfree.duckdns.org/app/public/ai/reception.php` |
| B (stub) | `CM-AI-STUB-BLED` | Bled (46.3683, 14.1146) | Test stub na 57, fiksni izmišljeni podatki, ni prave CM logike | `[interno testno omrežje — LAN, ni javno dostopno]` |
| C (stub) | `CM-AI-STUB-LJ` | Ljubljana (46.0569, 14.5058) | Test stub na 57, fiksni izmišljeni podatki, ni prave CM logike | `[interno testno omrežje — LAN, ni javno dostopno]` |

Stub node-a B in C implementirata isti Reception protokol (envelope, POST-only, `invalid_envelope`/`missing_dates`/`unsupported_query_type` obravnava) kot pravi node, a vedno odgovorita lokalno (depth 0, brez lastnega forwardinga) s fiksnimi izmišljenimi podatki — nikoli ne kličeta prave CM logike. Namen: omogočiti resnično testiranje `max_fanout`/sort-po-razdalji/`visited` mehanike na pravem omrežju (HTTP klici med pravima strežnikoma), preden se karkoli od tega postavi na produkcijo.

Preverjeno 2026-09-29: oba stub node-a odgovarjata pravilno lokalno na 57; node A (58) dostopa do obeh preko LAN (interno LAN, IP namerno ni objavljen) — HTTP 200, potrjen doseg. Testna infrastruktura za §13 scenarij je s tem pripravljena; sama forwarding koda (Korak 3) na node A še ni napisana.

---

## 14. §14 — ZAPRTO / SKLENJENO (formalno 2026-09-30, po Gregovem A→B→C okviru, potrjeno Claude+Grok+GPT+Gemini)

Prvotno odprta vprašanja (Grok predlog, Claude dopolnil, Gemini "Dvojček" zaprl — vsi trije se strinjajo):

- **Bootstrap sosedske tabele** — SKLENJENO: ročen, kuriran `nodes.json` ob pridružitvi, out-of-band, brez direktorija/self-registration servisa (skladno s prepovedjo centralnega registra, §9). To je naloga **operaterja Docking Port infrastrukture**, ne CM uporabnika/gostitelja — ti dve vlogi se v specifikaciji izrecno ne smeta mešati (gostitelj v smislu [[cm_customer_onboarding_profile]] nima nobenega ročnega dela s P2P konfiguracijo).
- **Avtentikacija med Reception-i** — SKLENJENO: brez, dokler je vse read-only in javno (skladno z `requires_consent: false`).
- **Rate limiting** — SKLENJENO na nivoju principa: `reception.php` ostane popolnoma brezstatusen (stateless) — noben APCu, noben datotečni counter v sami PHP datoteki, to bi podrlo načelo "tanka vrata". Omejevanje pogostosti se rešuje na infrastrukturnem nivoju (reverse-proxy/Apache `mod_ratelimit` pred `ai-cmfree.duckdns.org`), ne v aplikacijski kodi. Konkreten mehanizem še ni izbran — to ni blokada za načrtovanje Koraka 3, ker gre za sloj pred, ne znotraj `reception.php`.
- **Consent/write model** — SKLENJENO: ostane izven protokola, dokler ne obstaja dejanska write capability. `requires_consent: false` drži za MVP.
- **Pasivne varovalke brez stanja** (Gemini dodatek, sprejeto): tudi v Koraku 3 ostajata dve preverbi popolnoma brez baze — (1) prisilno obrezovanje `max_depth` na lokalno dovoljeni maksimum, ne glede na to, kaj pošlje agent; (2) `visited` array — če je lastni `node_id` že v njem, se zahtevek ne posreduje naprej. Obe se izračunata iz samega zahtevka, brez shranjevanja stanja.

### Nova odprta točka (predlog uporabnika, 2026-09-29, še NI usklajena s celotno skupino)

**Globina (depth) ≠ širina (fanout)** — spec trenutno omejuje samo, kolikokrat se zahtevek sme odbiti naprej (`max_depth`), ne pa, koliko sosedov Reception na enem hopu dejansko pokliče. Brez omejitve širine je promet eksponenten: pri kuriranih 5 sosedih in `max_depth: 2` lahko en agentov zahtevek sproži do 5 + 25 = 30 vzporednih klicev po mreži — to je bolj realno "open relay" tveganje kot tisto, ki ga rešuje rate limit na enem nodu, ker se pomnoži po celotni mreži.

Uporabnikov sklep (po lastnem premisleku je zavrnil agentovo nastavljivo globino IN geografsko spremenljivo globino kot nesmiselni — oboje bi obremenilo enega od udeležencev): edino kar šteje je **globina 0** (lasten strežnik) in **globina 1** (prvi najbližji nodi) — in ključno vprašanje ni "kako globoko", ampak **"s koliko nodi node komunicira vzporedno na enem hopu"**.

Predlog za razpravo (Claude, še ne implementacija):

- Nov parameter, npr. `max_fanout`, **stalen po nodu** (del node-ove lastne konfiguracije, ne nastavljiv s strani agenta, ne geografsko spremenljiv) — privzeto npr. 3.
- Reception pri forwardingu iz `nodes.json` vzame samo `max_fanout` geografsko najbližjih, še-neobiskanih sosedov glede na `target.location` (sort po razdalji + slice) — ostane brezstatusno, brez baze.
- S tem postane zgornja meja prometa izračunljiva vnaprej namesto eksponentno neomejene.

**SKLENJENO 2026-09-29** — Gemini potrdil `max_fanout` predlog in dodatno razložil "jump over" mehaniko (usmerjanje po Haversine razdalji do `target.location`, ne do vstopne točke): node sortira svoje sosede iz `nodes.json` po razdalji do cilja in vzame prvih `max_fanout`, s čimer lahko zahtevek v enem skoku "preskoči" geografsko bližje, a smerno nerelevantne sosede (primer: Piran → Rogla preskoči Portorož/Izolo/Koper, gre direktno proti Ljubljani). Priporočilo za operaterje: `nodes.json` naj poleg 2-3 mikrolokalnih sosedov vsebuje vsaj 1 "regionalni hub" (večje, centralno vozlišče), da jump-over deluje učinkovito na daljših razdaljah. To ni kršitev prepovedi centralnega registra (§9) — hub je navaden node, izbran prostovoljno s strani operaterja, ne vsiljena infrastruktura.

Vsi štirje (Claude, Grok, GPT, Gemini) so s tem §14 v celoti usklajen. Zelena luč za pripravo `nodes.json` strukture in pisanje forwarding logike v Koraku 3 — testna infrastruktura zanjo je pripravljena (glej §13 zgoraj).

### Ujeta ideja za kasnejšo fazo (NI odločeno, NI del MVP) — avtomatiziran bootstrap

Uporabnik je 2026-09-29 predlagal (izrecno kot razmislek, ne odločitev): namesto ročne kuracije `nodes.json`, bi ob podpisu "pogodbe o delitvi namestitev" skript samodejno poiskal 3 najbližje node in jih zapisal kot sosede; node-i bi to sprejeli in označili z zastavico "Povezan input"; vsak node bi imel omejeno število vhodnih/izhodnih "rež" (X input, X output), brez podvajanja povezav.

Zakaj to (zaenkrat) ni šlo v spec kot odločitev:

- Ponovno odpira bootstrap vprašanje, ki je bilo pravkar sklenjeno kot ročno/out-of-band (glej zgoraj) — samodejno iskanje "3 najbližjih" zahteva neki način odkrivanja obstoječih node-ov, kar je blizu centralnemu registru, ki ga v0.1 §9 izrecno prepoveduje (razen če bi šlo za decentraliziran gossip/peer-exchange mehanizem — bistveno kompleksnejše, ni oblikovano).
- Vezano je na poslovni/pravni proces ("pogodba o delitvi namestitev"), ki v tem projektu še ne obstaja in je izven obsega protokola samega.
- Master prompt §25 eksplicitno svari pred prezgodnjo kompleksnostjo — danes obstaja en sam pravi node, ročna kuracija zadostuje.

Vredno vrniti se k temu v v0.2, ko/če bo ročno vzdrževanje `nodes.json` dejansko postalo boleče zaradi realnega števila node-ov. Koncept "X input/X output rež brez podvajanja" je sam po sebi dober vzorec (naravno omeji stopnjo omrežja, ustvari back-pressure) — vredno ga takrat znova pogledati, morda celo neodvisno od vprašanja avtomatizacije odkrivanja.

---

## 15. Deset temeljnih načel (nespremenjeno iz v0.1 §26, ostaja veljavno kot preverba pri vsakem naslednjem koraku)

1. CM ostane source of truth.
2. AI ne dobi neposrednega dostopa do celotnega Ekosistema — samo do Reception (§2, izostreno).
3. Vsaka CM instalacija ima lahko svoja vrata.
4. Reception je skupen protokol, ne nujno centralni strežnik.
5. Geografija vodi discovery.
6. Poizvedba se širi po globini, vedno z omejitvami.
7. Zahtevek nosi svojo zgodovino (`request_id`, `visited`, `depth`).
8. Podatki ostanejo lokalni — v mrežo gre samo rezultat, ki ga node sme izpostaviti.
9. Ni vezave na enega AI ponudnika.
10. Docking Port je razširljiv preko manifesta/protokola, ne novih AI sistemov.

---

## 16. Korak 3 — Načrt pred kodo (SKLENJENO — čaka implementacijo, formalno zaprto 2026-09-30)

Po Gregovem A→B→C okviru (Gemini/Grok/GPT potrdili): preden se napiše ena vrstica forwarding kode v `reception.php`, se uskladimo na treh točkah. To je osnutek za skupni pregled, ne dokončana odločitev.

### 16.1 Shema `nodes.json`

**Posodobljeno 2026-09-30** — samo-opisna oblika (uporabnikov eksperiment), vsebuje tudi `max_fanout` (prejšnji osnutek ni povedal, kam se ta shrani):

```json
{
  "protocol": "CM-AI-Docking-Port",
  "version": "0.1",
  "node_id": "CM-AI-DEMO-058",
  "max_fanout": 2,
  "neighbours": [
    {
      "node_id": "CM-AI-STUB-BLED",
      "label": "Bled (stub test node)",
      "location": { "lat": 46.3683, "lon": 14.1146 },
      "reception_url": "[interno testno omrežje — LAN, ni javno dostopno]"
    }
  ]
}
```

**Realna lokacija (po incidentu in popravku 2026-09-30):** `/home/cmfree/cm-private/ai/nodes.json` na instanci 58 — izven `/var/www` v celoti (ne samo izven `/app/public/`), lastnik `cmfree:www-data`, pravice `640` na datoteki in `750` na mapah. `reception.php` jo bo bral po absolutni poti.

**Drugi popravek, isti dan — dovoljenja v verigi map:** `reception.php` teče kot `www-data` (potrjeno: `ps aux`, `APACHE_RUN_USER=www-data`), ki ni član skupine `cmfree` (`id www-data` → samo skupina `33(www-data)`). `/home/cmfree` je imel `other: ---`, kar bi `www-data` popolnoma blokiralo pri prehodu (`traverse`) v mapo — `reception.php` sploh ne bi mogel priti do datoteke, ne glede na njena lastna dovoljenja. To bi na živem Koraku 3.1 tiho spodletelo (`is_readable() === false`), ne bi pa se pokazalo prej, ker Korak 2 te poti sploh ne uporablja.

Popravek (brez sudo, `cmfree` je lastnik `/home/cmfree`): `chmod o+x /home/cmfree` (samo prehod, brez branja/izpisovanja vsebine za "other") + `chgrp www-data` na `cm-private/` in `cm-private/ai/` (`cmfree` je član skupine `www-data`, zato lahko to naredi sam). Veriga po popravku:

```text
/home/cmfree          drwxr-x--x cmfree:cmfree   (+x za "other" — samo prehod)
/home/cmfree/cm-private     drwxr-x--- cmfree:www-data
/home/cmfree/cm-private/ai  drwxr-x--- cmfree:www-data
.../nodes.json               -rw-r----- cmfree:www-data
```

Empirično preverjeno (ne samo izračunano): začasen PHP test skript na živem endpointu (odstranjen takoj po testu) je potrdil `is_readable() === true`, `129` bajtov, veljaven JSON. Javna izpostavljenost ostaja zaprta (ponovno preverjeno po spremembi dovoljenj).

**Incident in popravek (2026-09-30):** uporabnik je pri eksperimentiranju ročno ustvaril testno `nodes.json` na `/var/www/html/app/config/ai/nodes.json` — znotraj DocumentRoot-a (`/var/www/html`), zato je bila datoteka javno dostopna na `https://.../app/config/ai/nodes.json` (HTTP 200), poleg tega je bil na tej poti vklopljen Apache `Indexes` (javno vidljiv directory listing obeh nadrejenih map). Ujeto s strani zunanjega pregleda (curl), preden je vsebina postala nevarna (`neighbours` je bil prazen). Ista vrsta napake kot [[connector_modules_data_dir_exposure_2026_08_05]] — pot znotraj DocumentRoot-a, ne prave dostopne pravice. Popravek: datoteka premaknjena izven `/var/www` v celoti (`/home/cmfree/cm-private/ai/`, mapa `cmfree:cmfree` drwxr-x---, torej noben Apache config je nikoli ne more streči, ne glede na `Indexes`), prazni mapi v DocumentRoot-u odstranjeni. Preverjeno: `GET /app/config/ai/nodes.json` in `/app/config/` zdaj 404, Reception brez regresije (`ok:true`, L1).
**Pomembno — namerno ni javno dostopna datoteka.** V nasprotju z `manifest.json` (namerno javen, to je discovery površina), `nodes.json` je operativna konfiguracija posameznega noda — seznam, komu node zaupa in kam posreduje promet. Ta projekt ima že realno izkušnjo s tem natančnim razredom napake (`data/` mape connector modulov so bile javno berljive, popravljeno 2026-08-05, ponovilo se je še dvakrat) — `nodes.json` gre zato **izven javnega docroot-a** (ali vsaj eksplicitno blokirana v Apache konfiguraciji), bere jo samo `reception.php` server-side. To ni teoretična previdnost, je ponovitev znane napake, ki se tej ekipi je že zgodila.

### 16.2 Pogoji za forwarding

Realna sprememba glede na Korak 2: trenutna živa koda **vedno** kliče lokalni `availability_multi.php`, ne glede na `target.location`. To je bilo ustrezno za Korak 2 (depth 0, brez konteksta cilja), a za Korak 3 ni več smiselno — če nekdo iz Pirana vpraša za Roglo, Piranov lokalni odgovor ni relevanten rezultat, samo šum v `results[]`.

**SKLENJENO 2026-09-30 — efektivna razdalja (popravek, lokalni klic in izbira sosedov)**: `target.radius_km` (agentova želena relevantnost) in `limits.max_distance_km` (node-ova/sistemska zgornja meja iz `limits_default`, če agent ne pove svoje) sta dve različni stvari, ne sopomenki. Prejšnji osnutek je uporabljal samo `radius_km` — to je bila ista vrsta tihe dvoumnosti kot prej pri `request_expiry`, zdaj eksplicitno razrešena:

```text
effective_distance_km =
  če sta podana oba:  min(target.radius_km, limits.max_distance_km)
  če je podan samo eden: ta eden
  če ni podan noben: limits_default.max_distance_km (iz manifesta)
```

Razlog za `min()`, ne za katero koli polje samo: `radius_km` je agentova poslovna želja ("rezultati do X km od cilja so zame še uporabni"), `max_distance_km` je node-ova/sistemska zgornja meja prometa. Agent z ozkim `radius_km: 5` ne sme dobiti "lokalnega" odgovora 40 km stran samo zato, ker je node-ov `max_distance_km` širši — in obratno, node-ova lastna zgornja meja mora vedno obrezati tudi prevelik agentov `radius_km`, dosledno z že sklenjenim načelom pri `max_depth` (§14 — server vedno obreže, nikoli ne zaupa golemu agentovemu številu).

`effective_distance_km` se uporabi na **dveh** mestih, ne samo enem (dopolnitev, ne v prvotnem predlogu): tako pri odločitvi za lokalni klic KOT pri izbiri sosedov za forwarding — sicer node zavrne lasten odgovor kot "predaleč", nato pa brez težav forwarda k sosedu, ki je še bolj oddaljen, kar je notranje nekonsistentno.

Predlagan sklep (za potrditev):
1. **Lokalni klic je pogojen z razdaljo**: samo če je `haversine(own_location, target.location) <= effective_distance_km`, node pokliče svoj lokalni `availability_multi.php`. Sicer lokalnega klica sploh ne naredi.
2. **Forward pogoj**: `len(visited) < max_depth_limit` (kjer je `max_depth_limit` = min(agentov `limits.max_depth`, node-ova lastna omejitev iz `limits_default`) — server vedno obreže, nikoli ne zaupa golemu agentovemu številu, skladno z že sklenjenim §14).
3. **Izbira sosedov**: iz `nodes.json` izloči node-e, katerih `node_id` je že v `visited` IN katerih razdalja do `target.location` presega `effective_distance_km`; preostale sortiraj po Haversine razdalji do `target.location`; vzemi prvih `max_fanout` (sklenjeno z Gemini).
4. **Pred posredovanjem**: dodaj lastni `node_id` v `visited`, posreduj isti envelope (z dopolnjenim `visited`, nespremenjenim `request_id`/`request_expiry`) vsakemu izbranemu sosedu, vzporedno (ne zaporedno — da se čas ne seštevaj linearno).
5. Če je `len(visited) >= max_depth_limit` ali `nodes.json` po filtriranju (`visited` + `effective_distance_km`) prazen, node ne poskuša forwardati — ne napako, samo prazen forward-del odgovora.

**SKLENJENO 2026-09-30 (uporabnik)**: forward je **fallback-only**, in to ni samo tehnična odločitev za manj prometa — je namerna vrednostna izbira. Node najprej poskusi obdržati gosta pri sebi; šele če lokalni rezultat ni "true" (glej natančen pogoj spodaj), referira naprej v zaupano mrežo. To je digitalni ekvivalent že uveljavljenega vzorca v tem projektu — fizični keycard referral kot prototip CM Community (glej [[guest_trust_and_referral_strategy]]) — prenesen v protokol namesto zgolj v gostov ključ. Mreža je tako po zasnovi **overflow/referral mehanizem**, ne generično "poišči vse v bližini" iskanje.

Natančen pogoj (za implementacijo): lokalni klic šteje za "true rezultat" in **ne forwarda** naprej, če `data.ok === true` IN `data.available_count > 0` (obstaja dejansko prosta enota, ki ustreza povpraševanju). Če je lokalni klic spregledan zaradi razdalje (§16.2 točka 1), ali je vrnil `available_count: 0`, ali je lokalni klic spodletel — node forwarda naprej, znotraj `max_depth`/`max_fanout` omejitev.

**Pomembna ločnica (uporabnik, 2026-09-30)**: "lojalnost do lokalne skupnosti" velja na **kurirni ravni** (kdo sploh pride v `nodes.json` — operater ročno izbere prave, zaupanja vredne sosede po resničnem odnosu, "po svoji volji in interesu", ne po surovi razdalji; to je vloga bodočega Community zemljevida, §14 jump-over razprava). **Ne velja** na ravni algoritma med že-izbranimi sosedi — tam ostaja mehanski Haversine-sort + `max_fanout`, dogovorjen z Gemini (§14). Kurirana izbira je vrednostna, usmerjanje med kuriranimi sosedi je geografsko — ti dve ravni se ne smeta zamešati.

### 16.3 Agregacija in `partial`

1. Vsak klic (lasten lokalni + vsak sosed) ima neodvisen timeout (obstoječ vzorec: `CURLOPT_TIMEOUT`). Napaka/timeout enega soseda **ne** prekine drugih — ta sosed se izpusti iz `results[]`, `partial` postane `true`.
2. Odgovor vsakega soseda je že sam po sebi poln Response Envelope (lahko vsebuje več `results[]` vnosov, če je sosed sam nekaj forwardal naprej). Ta node samo **sploshi** (flatten) vse prejete `results[]` v svoj lastni `results[]` — brez reinterpretacije vsebine (skladno z že sklenjenim pravilom iz §6).
3. `search_meta.nodes_queried`/`nodes_reached` = vsota čez lasten poskus + vse poskuse vseh sosedov (vključno s tem, kar so sosedje sami poročali v svojih `search_meta`).
4. `search_meta.max_depth_used` = največja vrednost, ki jo je poročal kateri koli otrok, +1 za ta hop (ne samo lasten hop).
5. **SKLENJENO 2026-09-30**: `max_results` rez — če je po združitvi `results[]` daljši od `max_results`, obdrži najprej najbližje `target.location` (ne arbitrarno prvih N) — skladno s filozofijo "geografija vodi relevantnost" (§4).

**Status §16 (2026-09-30): načrt v celoti SKLENJEN, nič kode.** Vseh pet elementov potrjenih: fallback-only forward, `effective_distance_km` (min/fallback pravilo, velja za lokalni klic IN izbiro sosedov), ločnica kuracija-vs-routing, `nodes.json` izven docroot, `max_results` rez po bližini. Community/User Map kot kasnejši, user-controlled način polnjenja `nodes.json` je potrjen kot skladen s tem načrtom, a ostaja izrecno po Koraku 4/Fazi C, ne pred njim — ne odpira Koraka 3. Implementacija je ločena, zavestna odločitev ("Piši"), ne sledi avtomatsko iz zaprtega načrta.

---

## 17. Korak 3.1 — Branje konfiguracije (varen prvi podkorak, SKLENJENO 2026-09-30, čaka "Piši")

Namerno ozek, pred-forwarding podkorak: `reception.php` zna prebrati `nodes.json`, a se obnašanje agenta **ne spremeni**, dokler je `neighbours` prazen ali se datoteka ne prebere.

1. `reception.php` prebere `/home/cmfree/cm-private/ai/nodes.json` po absolutni poti (dovoljenja preverjena in popravljena, glej §16.1).
2. **Fail-safe, ne 500**: če datoteka ne obstaja, JSON ni veljaven, ali `node_id` v datoteki ne ujema z `CM-AI-DEMO-058` — `reception.php` **tiho pade nazaj** na obnašanje Koraka 2 (čisto lokalno, `ok:true`, `depth:0`), agent nikoli ne vidi napake konfiguracije.
3. **Dodatek (Claude)**: vsak od treh fail-safe pogojev se zabeleži server-side z `error_log()` (isti vzorec kot pri Koraku 2 za curl napake) — agent tega ne vidi, operater pa lahko razišče, zakaj forwarding ne deluje, ne da bi moral ugibati.
4. **Brez forwardinga v tem podkoraku** — nobenega HTTP/cURL klica proti sosedom, tudi če je `neighbours` napolnjen s testnimi stubi. Živi endpoint ostane ves čas 100% lokalen in stabilen.

**Status: ✅ DEPLOYANO in testirano 2026-09-30.** Koda (Grok "Piši", `load_nodes_config()` + fail-safe + `error_log()`) je na živo, po istem vzorcu kot Korak 2 (`.tmp` + `php -l` + atomski `mv`). Testi na živem endpointu, vsi po pričakovanjih:
1. Normalen zahtevek → odgovor bit-za-bit enak Koraku 2 (`ok:true`, `depth:0`, `L1`).
2. Odgovor ne vsebuje nobene sledi `nodes.json`/`/home/cmfree`/`max_fanout`/`neighbours`/`cm-private`.
3. `/app/config/ai/nodes.json` ostaja 404.
4. Fail-safe, oba scenarija na živem strežniku: preimenovana datoteka → `ok:true` lokalno + `error_log`: "nodes.json not readable"; pokvarjen JSON → `ok:true` lokalno + `error_log`: "nodes.json invalid JSON". Datoteka obnovljena z originalnimi dovoljenji (`cmfree:www-data`, `640`) po testu.
5. GET→405 in napačen protokol→400 nespremenjena (regresija čista).
6. Brez zaostalih začasnih testnih skriptov v docroot-u.

`$nodes_cfg` je prebran in ovrednoten, a se za forwarding še ne uporablja — endpoint ostaja 100% lokalen. Naslednje: Korak 3.2 (Haversine, `effective_distance_km` gating za lokalni klic) — čaka na nov "Piši".

---

## 18. Privaten tracker — ni del protokola, samo operativno orodje (2026-09-30)

Namen: videti, ali objava specifikacije dejansko vodi do zanimanja/uporabe (ne samo teoretičen dokaz koncepta). Namerno **brez** novega javnega endpointa/dashboarda — manj nove površine po incidentu iz §16.1, in ni potreben, ker je edini naslovnik lastnik sam.

- `reception.php` zdaj beleži vsak klic (uspeh in vsak error path, eno mesto v `send()`) v privaten JSONL: `/home/cmfree/cm-private/ai-logs/visits.jsonl`. Zapisuje `ts`, `http_code`, `ok`, `query_type`, `error_code`, `user_agent` — **namerno brez IP naslova**, zanima nas samo *ali/kdo* (user agent) dejansko kliče, ne identiteta.
- Mapa za dnevnik (`ai-logs/`) je namerno ločena od mape s `nodes.json` (`ai/`) — čeprav obe zahtevata pisanje s strani `www-data` procesa, bi skupna mapa pomenila, da lahko `www-data` (prek dovoljenja mape, ne datoteke) tudi izbriše/preimenuje `nodes.json`. Ločitev odpravi to tveganje.
- Privaten CLI report (`php /home/cmfree/cm-private/ai-logs/report.php`, poganja se prek SSH, ni web-dostopen) združi ta dnevnik z Apache `access.log` (ki že sam beleži vsak ogled `index.html`/`spec.html`/`spec.md`/`manifest.json`/`.well-known` — ni bilo treba graditi ločenega beleženja za statične strani).

Prvi zagon (2026-09-30) je že pokazal pravi signal: obiski `spec.md`/`.well-known` z `curl/7.88.1` in `Python-urllib/3.12` — različni od lastnih orodij v tej seji (`curl/8.5.0`) — torej najverjetneje avtonomno branje s strani enega od AI sodelavcev ali njihove infrastrukture.

---

## 19. Building Team (dodano 2026-10-01)

Namen te sekcije: vsaka sveža seja katerega koli sodelujočega modela (GPT po resetu plana, nova Gemini/Grok seja, ipd.) naj ob branju tega dokumenta takoj dobi pravilen kontekst o vlogah — brez da bi moral uporabnik vsakič znova razlagati, kdo je kdo v projektu.

<div class="team-grid">

<div class="team-card">
<img src="/app/public/ai/team/viljem.jpg" alt="Viljem">
<h4>Viljem</h4>
<div class="role">Project Lead</div>
<p>Decision-making: sets direction, sets priorities, and confirms consensus. Knows the project's purpose best and is accountable for final decisions.</p>
</div>

<div class="team-card">
<img src="/app/public/ai/team/claude.jpg" alt="Claude Code">
<h4>Claude Code</h4>
<div class="role">Lead Developer</div>
<p>Implementation: has direct access to the codebase and executes agreed-upon solutions. Turns consensus into working code.</p>
</div>

<div class="team-card">
<img src="/app/public/ai/team/grega.jpg" alt="Grega">
<h4>Grega</h4>
<div class="role">External Tester</div>
<p>Verification: uses the Grok model and his own tools to test the system from the outside world, comparing expected behavior against the real response of the public deployment.</p>
</div>

<div class="team-card">
<img src="/app/public/ai/team/gpt.jpg" alt="GPT">
<h4>GPT</h4>
<div class="role">Coordinator &amp; Sounding Board</div>
<p>Coordination: clear explanations, a second architectural opinion, edge-case thinking, and continuity — keeping the big picture (and the good mood) from getting lost in technical detail.</p>
</div>

<div class="team-card">
<img src="/app/public/ai/team/gemini.jpg" alt="Gemini">
<h4>Gemini</h4>
<div class="role">Energetic Challenger</div>
<p>Finding improvements: its impatience is useful when it forces the team to re-check assumptions, surface overlooked options, and think about further improvements.</p>
</div>

</div>

### Kako sodelujemo

1. Claude predlaga rešitev.
2. Viljem in ekipa pregledajo predlog — arhitektura, robni primeri, alternative, posledice.
3. Uskladitev in konsenz.
4. Claude izvede spremembo.
5. Grega preveri delovanje; ugotovitve se vrnejo v ekipo.

<div class="team-rule">
<strong>Eno pravilo, ki ga posebej pazimo:</strong> če nimamo dovolj dokazov, da nekaj deluje, tega ne razglasimo za preverjeno. Predlog ni implementacija, implementacija ni test, uspešen test posameznega scenarija pa še ni dokaz, da je celoten sistem brezhiben. Tako ohranimo natančnost pred hitrostjo.
</div>

---

## 20. Korak 3.2 — Geografski filter pred lokalnim klicem (✅ DEPLOYANO 2026-10-01, Grok "Piši", Viljem potrdil)

Obseg: geografski filter **pred** lokalnim klicem na `availability_multi.php`. **Brez forwardinga** — `$nodes_cfg` se v tem koraku še vedno ne uporablja za HTTP klice proti sosedom.

Koda (Grok "Piši", po Viljemovi potrditvi dveh robnih primerov):
- `haversine_km()` — standardna formula, `asin(min(1.0, sqrt($a)))` ščiti pred float napako, ki bi sicer lahko vrnila NaN.
- `effective_distance_km()` — natanko §16.2 SKLENJENO pravilo: `min(radius_km, max_distance_km)` če sta oba podana, sicer tisti, ki je podan, sicer `limits_default.max_distance_km`.
- `target_coords()` — varno izvleče `lat`/`lon`, vrne `null` če manjka karkoli.
- Vrata so vstavljena **za** obstoječo validacijo (protocol, `request_expiry`, `query.type`, `from`/`to`) in **pred** curl klicem na `availability_multi.php` — geografija ne sme skriti `missing_dates`/`unsupported_query_type`.

Dva robna primera, usklajena med Grokom in Viljemom pred "Piši":
1. **Manjkajoč `target.location` (ali nepopolne koordinate) → brez lokalnega klica**, ne pade nazaj na Korak-2 "vedno lokalno". Razlog: slednje bi ustvarilo trivialno obhodnico okoli celotnega geografskega filtra (agent bi preprosto izpustil lokacijo namesto da bi jo nastavil daleč stran).
2. **`max_depth: 0` ne izklopi geografskega filtra.** `max_depth` nadzoruje omrežno potovanje (še ne obstaja), geografski filter nadzoruje relevantnost tega noda — neodvisni omejitvi, ne smeta se mešati.

Testi na živem endpointu (Grokov seznam, vseh 9 zeleno):

| # | Scenarij | Rezultat |
|---|---|---|
| 1 | Lancovo (46.34, 14.17), radius 30 | `ok:true`, `L1`, depth 0 ✅ |
| 2 | Ljubljana (46.05, 14.50), radius 30 | `ok:true`, `results: []` ✅ |
| 3 | Rogla (46.45, 15.33), radius 30 | `ok:true`, `results: []` ✅ |
| 4 | brez `target.location` | `ok:true`, `results: []` ✅ |
| 5 | samo `radius_km`, brez lat/lon | `ok:true`, `results: []` ✅ |
| 6 | `max_depth:0` + Lancovo | še vedno `L1` (filter ni izklopljen) ✅ |
| 7 | GET | 405 + envelope (regresija) ✅ |
| 8 | napačen protocol | 400 + envelope (regresija) ✅ |
| 9 | telo odgovora | brez `/home/cmfree`, `nodes.json`, `haversine`, `effective_distance` ✅ |

**Status: Korak 3.2 zaključen in zunanje preverljiv.** Čaka na Gregovo neodvisno zunanjo potrditev (isti 9-točkovni seznam, njegova lastna orodja). Korak 3.3 (fallback forward na sosede) se **ne začne samodejno** — čaka svoj ločen "Piši".

---

## 21. Korak 3.3.0 — LAN smoke test + zaseben `nodes.json` z B+C (✅ DEPLOYANO 2026-10-01, Grok osnutek, Viljem potrdil)

Obseg po dogovoru: samo infrastruktura, **brez** spremembe `reception.php` logike na 058 (ta ostaja natanko Korak 3.2 — forwarding koda še ne obstaja). Stub node-a B (`CM-AI-STUB-BLED`) in C (`CM-AI-STUB-LJ`) na 57 sta že obstajala iz §13 — niso novi.

**Realna najdba med tem korakom (ni bila v Grokovem osnutku predvidena):** 57 ima poleg LAN dostopa še **drug, nepovezan javen reverse-proxy domain** (`apartmamatevz.eu`, uporabljen za ločen pro-dev projekt, `ProxyPreserveHost On`, proxya *celoten* 57 na splet). To je pomenilo, da sta bila oba stub node-a dejansko dosegljiva iz javnega interneta prek te domene — kar neposredno krši zahtevo iz Grokovega načrta ("stubi niso na javnem netu"). Ni bila napaka tega protokola, je bila posledica tega, da je 57 skupna razvojna naprava z lastnim, nepovezanim javnim vstopom.

**Popravek:** namesto poseganja v `apartmamatevz-eu.conf` (produkcijska infrastruktura drugega projekta, izven obsega), dodana ozka app-level varovalka v oba stub datoteki — zavrneta zahtevek (404), razen če `$_SERVER['HTTP_HOST']` ustreza natanko `[interno LAN, IP namerno ni objavljen]` (neposreden LAN dostop). Pri proxyju prek `apartmamatevz.eu` (`ProxyPreserveHost On`) Host header ostane `apartmamatevz.eu`, zato se pravilno zavrne. Preverjeno: LAN dostop še vedno 200, javni proxy dostop zdaj 404 za oba stub-a.

**Izvedeni koraki (po Grokovem zaporedju):**
1. S 058 (prek LAN) POST na oba stub-a → `200`, pravilen envelope, enoti preimenovani v `BLED-S1`/`LJ-S1` (namesto prejšnjih `CM-AI-STUB-*-UNIT1`, da je provenance v agregatu jasno razločljiva od `L1`).
2. Potrjeno: stub-a nista dosegljiva z javnega interneta (po popravku zgoraj).
3. Zaseben `nodes.json` na 058 posodobljen s pravima LAN URL-jema obeh sosedov (`cmfree:www-data`, `640`, nespremenjeno od Koraka 3.1).

**Regresija po polnjenju `nodes.json`:** `reception.php` se obnaša identično Koraku 3.2 — Lancovo še vedno vrne samo lokalni `L1`, Ljubljana (zunaj dosega) še vedno vrne prazne `results` z `nodes_queried: 0` (brez poskusa forwardinga na LJ stub, čeprav je zdaj v `nodes.json`) — potrjuje, da `$nodes_cfg` resnično še ni uporabljen za HTTP klice. Javna pot `/app/config/ai/nodes.json` ostaja 404.

**Status: ✅ 3.3.0 V CELOTI ZAPRT (2026-10-01).** Gregova zunanja potrditev: stuba z interneta nista vidna (`apartmamatevz.eu/.../reception.php` → 404, ni envelope), 058 ostaja čisti 3.2 (Ljubljana/Bled r=3 → prazno, Bled r=30 → samo `L1`, brez stub-a — lojalnost potrjena od zunaj). Edina točka, ki je Grega po definiciji ne more preveriti od zunaj (da `www-data` na 058 dejansko dobi `200` z LAN na oba stub-a), je Claude ponovno preveril neposredno: oba stub-a vrneta `ok:true` s pravilnima enotama (`BLED-S1`, `LJ-S1`). Zunanja in notranja preverba se ujemata — 3.3.0 je zaprt.

**Čaka ločen "Piši" za 3.3.1** (dejanska forwarding koda). Do takrat 058 ne sme začeti klicati sosedov.

---

## 22. Korak 3.3.1 — Fallback-only forward (✅ DEPLOYANO 2026-10-01, Grok "Piši", prvi pravi P2P forward v tem projektu)

**To je prvi trenutek, ko 058 dejansko pokliče drug node preko HTTP.** Vse od tu naprej je bilo depth-0 samo-odgovor; to spremeni to.

### Pogodba

```text
local_true = lokalni klic narejen AND HTTP 200 AND data.ok===true AND data.available_count>0
če local_true     → vrni takoj, ni forward
sicer, če sme_forward → POST na izbrane sosede
sicer             → prazen uspeh (kot 3.2 skip)
```

`sme_forward`: `$nodes_cfg['ok']===true` AND `target` ima lat/lon AND `count(visited) < max_depth_limit`, kjer je `max_depth_limit = min(agent limits.max_depth, 2)` (manjkajoč agentov max_depth → 2).

### Dve dejanski napaki iz prejšnjih korakov, odkriti in popravljeni med implementacijo

1. **`envelope()`-ov `max_depth_limit` ni bil nikoli dejansko obrezan.** Od Koraka 2 naprej je koda samo pretvorila agentov surov `limits.max_depth` v int, brez `min()` proti node-ovi lastni meji — čeprav je §14 to že "SKLENJENO" kot princip. Dokler je bilo polje samo informativno (prikazano v `search_meta`), napaka ni imela posledic. Zdaj, ko `max_depth_limit` dejansko krmili `sme_forward`, bi bila to prava varnostna luknja (agent bi lahko poslal poljubno velik `max_depth` in zaobšel omejitev). Popravljeno: nova funkcija `max_depth_limit_for()`, uporabljena na obeh mestih.
2. **`$visited` je bil od Koraka 2 hardcoded na `[$NODE_ID]`**, ne glede na to, kaj je agent (ali prejšnji hop) dejansko poslal v `request.visited`. Test K (spodaj) bi bil nemogoč brez tega popravka. Popravljeno: `$visited` zdaj združi obstoječi seznam iz zahtevka + lastni ID, brez podvajanj.

### Izbira sosedov in klici

`select_neighbours()`: izloči `visited` + lastni ID, zahteva veljavna `location.lat/lon` in `reception_url` (samo `http://`/`https://`, regex preverjeno), filtrira po `effective_distance_km` do **target** (ne do sebe), sortira po razdalji, vzame prvih `max_fanout` (iz `nodes_cfg`, privzeto 2).

`forward_to_neighbours()`: `curl_multi`, vzporedno, **10 s timeout na soseda neodvisno**, `CURLOPT_FOLLOWLOCATION=false`. En neuspešen sosed ne prekine drugih. Agent nikoli ne vidi LAN URL-ja, IP-ja ali curl napake — vse gre v `error_log()`.

### Agregacija

- `data` se ne spreminja (brez reinterpretacije, dosledno s §6).
- vsak `results[]` vnos: `depth = 1 + otrokov depth` (provenance hopa).
- `node_id`/`source`/`freshness` ostanejo otrokovi.
- `nodes_queried`/`nodes_reached` = lastni lokalni poskus (0/1) + vsota otrokovih (ali +1 na poskus, če otrok nima meta).
- `max_depth_used` = max(1+otrokov `max_depth_used`, max depth v `results`).
- `partial=true` če je bil izbran sosed, ki ni vrnil veljavnega envelope (timeout/napačen format/napačen protocol).
- `max_results` rez po flatten: najbližji **target** najprej (razdalja prek `node_id → location` v `nodes.json`; neznan id na konec).

### Testi — vseh 12 zeleno (Grega A–I javno, Claude J–L interno LAN)

| ID | Scenarij | Rezultat |
|---|---|---|
| A | Lancovo r=30 | samo `L1`, brez stubov (local_true prestreže) ✅ |
| B | Ljubljana r=30 | brez `L1`; `CM-AI-STUB-LJ`, `LJ-S1`, `depth:1` ✅ |
| C | Bled r=3 | brez `L1`; `BLED-S1` ✅ |
| D | Bled r=30 | samo `L1`, brez `BLED-S1` (lojalnost) ✅ |
| E | `max_depth:0` + LJ r=30 | prazno, `nodes_queried:0` ✅ |
| F | Rogla r=30 | prazno (nihče v dosegu) ✅ |
| G | brez lat/lon | prazno, ni forward ✅ |
| H | GET / napačen protocol | 405 / 400 (regresija) ✅ |
| I | telo odgovora | brez LAN IP, `nodes.json`, `/home/cmfree`, `reception_url` ✅ |
| J | sosed nedosegljiv (interno, sintetično premaknjen Bled + namerno mrtev URL) | `partial:true`, `LJ-S1` ostane, ~90ms (curl_multi res vzporeden) ✅ |
| K | `visited` že vsebuje `CM-AI-STUB-LJ` | tega ne pokliče, prazno ✅ |
| L | `max_fanout:1`, oba v dosegu (interno, sintetično premaknjen Bled) | samo bližji (`LJ`, 0 km) ✅ |

**Opomba o testih J/L**: resnična geografija (Lancovo je samo ~5 km od Bleda, ~41 km od Ljubljane) ne omogoča scenarija "Lancovo zunaj dosega, oba stuba znotraj" z resničnimi koordinatami — izračunano in preverjeno pred testom. Zato sta J in L uporabila začasno, nadzorovano spremenjen `nodes.json` (Bled premaknjen sintetično ~10 km od cilja, obnovljen takoj po testu) — to je bil vedno eksplicitno "Code, LAN" test, ne Gregov javni.

**Status: ✅ Korak 3.3.1 V CELOTI ZAPRT (2026-10-01) — dvojno potrjen.** Claude interno (vseh 12, A-L) in Grega neodvisno od zunaj (vseh 9 javnih, A-I) se ujemata natanko. Grega je posebej izpostavil E (`max_depth:0` → `max_depth_limit:0` v odgovoru) kot javen dokaz, da popravek obrezovanja globine dejansko deluje — ne samo na papirju. 058 zdaj dejansko forwarda, preverjeno z dveh neodvisnih strani. Korak 3.4 (globlje verige, stubi ki sami forwardajo, polna zanka) **ni** del tega dropa in čaka svoj ločen "Piši".

---

## 23. Korak 3.4 — OSNUTEK za skupno uskladitev (2026-10-01, Claude, ČAKA potrditev — nič kode)

Obseg po §22: globlje verige (depth 2+), node ki sam forwarda naprej, polna zanka (A→B→A) — izrecno izven 3.3.1.

### Strukturna ugotovitev, ki mora biti rešena PRED katerokoli kodo

Med pripravo testnih parametrov sem našel resnično oviro, ne kozmetično: **izbor soseda in lokalno obrezovanje na izbranem sosedu uporabljata isto razdaljo.**

Ko node A izbere soseda B kot forward-kandidata, je to zato, ker je `haversine(B, target) <= effective_distance_km`. Ko B nato sam preveri, ali sme odgovoriti lokalno, računa **isto** `haversine(B, target)` proti **isti** `effective_distance_km` (ista polja envelope-a potujejo nespremenjena). Rezultat: **vsak node, ki je bil izbran kot sosed, bo vedno tudi sam ugotovil, da je "v dosegu".**

Če ima B (stub) fiksne "vedno na voljo" podatke (kot zdaj — `BLED-S1`, vedno `available_count:1`), bo B **vedno** vrnil `local_true=true` in se **nikoli** ne bo premaknil naprej k svojim lastnim sosedom. Globlja veriga (A→B→D) je s trenutnimi stubi **strukturno nemogoča**, ne glede na izbiro ciljnih koordinat — to ni vprašanje geografije (tisto sem preveril, glej spodaj), ampak vprašanje, kaj stub sploh vrne.

**Posledica za obseg 3.4**: stubi potrebujejo način, da za določen testni datumski razpon vrnejo `available_count:0` (zasedeno), ne vedno "na voljo" — šele takrat ima smisel govoriti o node-u, ki "ne more lokalno, zato forwarda naprej".

### Predlagana topologija

| Node | Lokacija | Novo? |
|---|---|---|
| A = `CM-AI-DEMO-058` | Lancovo (46.34, 14.17) | obstaja |
| B = `CM-AI-STUB-BLED` | Bled (46.3683, 14.1146) | obstaja, potrebuje nadgradnjo (glej spodaj) |
| D = nov stub, predlog `CM-AI-STUB-KRGORA` | Kranjska Gora (46.4858, 13.7905) | nov, tretji LAN stub na 57 |
| C = `CM-AI-STUB-LJ` | Ljubljana | ostane kot je (čist list, za kontrast — ni vsak node treba nadgraditi) |

Izračunane razdalje (preverjeno, ne ugibano):

| Par | km |
|---|---|
| A–B (Lancovo–Bled) | 5.29 |
| A–D (Lancovo–Kranjska Gora) | 33.31 |
| B–D (Bled–Kranjska Gora) | 28.07 |

Z `radius≈30` okoli Kranjske Gore: A je zunaj dosega (33.31>30, torej A forwarda), B je znotraj dosega iz A-jeve perspektive IN iz lastne (28.07<=30 — natanko zgoraj opisana ugotovitev). Če B za te datume vrne `available_count:0`, B sam presodi "ni local_true" in poskusi forwardati naprej k D (če je D v B-jevem `nodes.json` in v dosegu).

### Kaj se mora zgoditi, da B sploh lahko forwarda

B (trenutno čist stub, fiksen JSON, brez `nodes.json`, brez `load_nodes_config`/`select_neighbours`/`forward_to_neighbours`) mora dobiti **isto arhitekturo kot A**:
- lasten `NODE_ID`/`NODE_LAT`/`NODE_LON`
- lasten zaseben `nodes.json` (izven docroot na 57, enaka dovoljenja kot pri A — §16.1/§21 vzorec)
- isto `reception.php` logiko (local_true/sme_forward/select_neighbours/forward_to_neighbours)
- Host-header LAN varovalko (že obstaja, glej §21 popravek)

To pomeni: B preneha biti "stub" v prejšnjem pomenu (list, brez logike) in postane **drug pravi mini-node** — manjša arhitekturna sprememba vloge, vredna eksplicitne potrditve, ne samo tihe implementacije.

### Predlagano zaporedje (isti vzorec kot 3.3.0 → 3.3.1)

**3.4.0 — infrastruktura, brez spremembe A**
1. Nov stub D na 57 (Kranjska Gora), isti vzorec kot B/C (fiksen list, Host-header varovalka).
2. B nadgrajen na pravo Reception logiko (kopija A-jeve kode, lastne konstante, lasten `nodes.json` z D kot sosedom).
3. B-jeva stub-data logika razširjena: za dogovorjen testni datumski razpon vrne `available_count:0` namesto `1` (kontrolirano "zasedeno", da forward sploh pride v poštev).
4. LAN smoke: A→B (obstoječe, nespremenjeno), B→D (novo, ročno s 57).
5. Preverjeno: B in D nedosegljiva z interneta (isti test kot pri §21 incidentu).

**3.4.1 — zanka test (A→B→A)**
B-jev `nodes.json` vsebuje tudi A (`CM-AI-DEMO-058`, pravi javen `reception_url`). Cilj znotraj B-jevega dosega, B-jev lokalni `local_true=false` (zasedeno), B poskuša forwardati — med kandidati je A, ki pa je že v `visited` → B ga izloči. Preverja se dejanski HTTP round-trip, ne samo logika v izolaciji (test K je to testiral samo na enem nivoju).

**3.4.2 — globlja veriga (A→B→D, depth 2)**
Isti scenarij kot zgoraj, a B-jev `nodes.json` vsebuje D namesto/poleg A (D ostane list, glej točko 5 spodaj). Preverja: `depth:2` na končnem rezultatu, `max_depth_used` pravilno odraža dva hopa, D-jev `error_log` potrdi prejet `visited=[CM-AI-DEMO-058, CM-AI-STUB-BLED]`. **Po popravku vrstnega reda (glej spodaj)**: `max_depth:2` → veriga uspe do D; `max_depth:1` → ustavi pri B.

**Dopolnitev 2026-10-01 (GPT, tri točke — vse tri realne, ne kozmetične):**

1. **`visited` skozi VEČ hopov, ne samo en** — 3.4.1 je testiral samo, da B izloči A iz SVOJE izbire (en nivo). Ne dokazuje, da D dejansko prejme polni `visited=[A,B]`, ne samo `[B]`. Dopolnitev testa: D-jev lastni `nodes.json` naj *tudi* vsebuje A kot soseda. Če D (ko bi sam poskušal forwardati še globlje) pravilno izloči A, to dokazuje, da je prejel polno verigo, ne le zadnjega hopa. Preverljivo interno na 57 (D-jev `error_log`, isti vzorec kot pri J/L v §22).

2. **Omejevanje globine skozi CELOTNO verigo — realna napaka v prvotnem osnutku, najdena pri tej dopolnitvi.** Pri privzetem `max_depth_limit=2` bi bil pri B-jevem preverjanju `visited` že dolg 2 (`[A,B]`) — `2 < 2` je `false`, torej B **sploh ne bi mogel** forwardati naprej k D, ne glede na geografijo ali stub podatke. Zahtevek za 3.4.2 mora izrecno uporabiti `limits.max_depth: 3`, sicer je test strukturno nemogoč. Razmerje: `max_depth_limit` dovoljuje `max_depth_limit - 1` hopov naprej od vstopne točke (vstopni node sam že "porabi" eno mesto v `visited`). Dodaten test: isti B→D scenarij z `max_depth:2` (namesto 3) mora pravilno **ustaviti verigo pri B** (B ne forwarda k D) — to je neposreden dokaz, da omejitev deluje skozi celotno verigo, ne samo na prvem hopu (kar je §22 testa E dokazal samo za hop 0→1).

3. **Agregacija ne sme izgledati kot en lokalni vir — arhitekturno že rešeno, a ni bilo nikjer eksplicitno zapisano.** Vsak `results[]` vnos že nosi svoj `node_id`/`depth`/`source`/`freshness` (§6, §22) — podatki iz različnih node-ov se nikoli ne združijo v en neoznačen blok. Pomembna ločnica, ki jo je treba eksplicitno povedati (dokumentacijsko, ne kodno): `search_meta.entry_node` je vedno node, ki je **neposredno odgovoril agentu na ta konkreten HTTP klic** (pri agregiranem odgovoru vedno vstopna točka, npr. A) — to ni isto kot `results[].node_id`, ki pove **resnični izvor posameznega podatka** (lahko B, D, karkoli globlje). Agent ne sme teh dveh polj zamenjati. To velja zapisati v `index.html`/dokumentacijo kot pravilo za agente, ko se 3.4 implementira.

### Odprta vprašanja za ekipo (pred katerokoli "Piši")

1. **Soglasje k strukturni ugotovitvi zgoraj** — se vsi strinjamo, da je to realna ovira in ne nekaj, kar sem spregledal?
2. **Ime/lokacija za D** — Kranjska Gora je predlog (resnična razdalja preverjena), lahko je poljubna druga točka, dokler razmerja delujejo.
3. **Ali B res postane "pravi" mini-node (lastna `reception.php` kopija), ali raje ostane preprost stub, a z dodano "zasedeno za te datume" logiko brez polne forward sposobnosti** — slednje bi 3.4.1/3.4.2 naredilo nemogoča, a je manj invazivna sprememba. Moje priporočilo: B postane pravi mini-node — brez tega polna zanka (izrecno imenovana v §22 kot cilj 3.4) ni testirana, samo simulirana.
4. **Datumski razpon za "zasedeno"** — kateri testni `from`/`to` naj B trajno vrača kot nedosegljivo, da se ne prekriva s katerim koli obstoječim testom A-L iz §22 (ti morajo ostati zeleni nespremenjeni).
5. **(Grok, poenostavitev točke 5 zgoraj) D NI potreben kot mini-node za dokaz propagacije `visited`.** D sme ostati list — dovolj je, da D **zabeleži** prejeti `visited` v `error_log` (preverljivo interno na 57, isti vzorec kot J/L). Pri A→B→D pričakujemo `visited=[CM-AI-DEMO-058, CM-AI-STUB-BLED]` ob prihodu na D. Mini-node D + A v njegovem `nodes.json` (polna rekurzivna izključitev) ostane **kasnejši** dodatek, ni pogoj za 3.4.0.
6. **(novo, GPT) Eksplicitno zapisati `entry_node` vs. `results[].node_id` ločnico** v javno dokumentacijo (`index.html`) kot del 3.4 droppa, ne kot naknadno misel. Tabela za dokumentacijo (Grok):

   | Polje | Pomen |
   |---|---|
   | `search_meta.entry_node` | kdo je **ta HTTP odgovor** dostavil agentu |
   | `results[].node_id` | kdo je **izvor** tega vnosa |
   | `results[].depth` | koliko hopov je ta vnos prepotoval |

   Brez nove kode — to je že točno to, kar 3.3.1 vrača (test B: `entry_node=058`, `results[].node_id=CM-AI-STUB-LJ`, `depth=1`).

### Popravek vrstnega reda `visited`/`max_depth` (Grok, 2026-10-01) — vpliva na že živo kodo na 058

Grok je osporaval mojo interpretacijo "potrebujemo `max_depth:3` za 2 hopa" kot artefakt **ene** implementacije (vrstni red preverb), ne nujno lastnost protokola. Preverjeno z izpeljavo skozi oba vrstna reda — Grok ima prav:

**Trenutna živa koda (3.3.1)**: najprej doda sebe v `visited`, šele nato preveri `count(visited) < max_depth_limit`. Posledica: pri `max_depth:2` je pri B-jevem preverjanju `visited` že `[A,B]` (dolžina 2) → `2<2=false` → B ne more naprej, ne glede na karkoli drugega. Za A→B→D je potreben `max_depth:3`.

**Grokov predlog**: preveri `count(visited_PRIHAJAJOČ) < max_depth_limit` **pred** dodajanjem sebe; sebe doda šele v izhodni envelope, poslan naprej. Posledica: `max_depth:2` naravnost pomeni "2 hopa dovoljena" (A→B→D uspe), `max_depth:1` pomeni "1 hop" (A→B, B se ustavi). Bistveno čistejša, bolj intuitivna semantika.

**Preverjeno (Claude)**: ta sprememba **ne podre nobenega od 12 že zelenih testov iz §22** — pri `max_depth:0` oba vrstna reda dasta identičen rezultat (`0<0` in `1<0` sta oba `false`), razlika se pokaže šele na drugem hopu, ki ga noben obstoječi test ne sega. **A požima**: ta popravek zahteva majhno, a dejansko spremembo **že žive kode na 058** (vrstni red dveh vrstic v obstoječem `reception.php`), ne samo novo kodo za B/D — to mora biti eksplicitno v obsegu 3.4.0, ne tiha stranska sprememba.

Sprejeto: `max_depth_limit` = natanko toliko hopov, kolikor je dovoljenih (ne `-1`). Test 3.4.2 postane:
- `max_depth:2` → A→B→D **uspe**
- `max_depth:1` → ustavi pri B (B ne pokliče D)

### Nova točka 7 (Viljem, 2026-10-01, Grok je še ni videl) — lokalni "ni na voljo" rezultat se trenutno popolnoma zavrže

Opažanje: trenutna (3.3.1) koda, ko je `local_ok=true` (v dosegu) a `local_true=false` (preverjeno, a `available_count:0`), ta rezultat **v celoti zavrže** — gre naravnost na forward-ali-prazno, brez sledi, da je node sploh odgovoril. Viljemova točka: node, ki je preveril in nima prostora, naj to pove (izpade iz "local_true" kratke poti, ne pa iz `results[]` v celoti) — sicer node "izpade iz iskanja", kot da ga ni bilo, čeprav je dejansko odgovoril. Node ne sme biti *samo* relay — mora vrniti svoj rezultat, če ga ima; če ga nima (klic spodletel/izven dosega), šele takrat je čist relay.

**Claude preverjeno**: noben od 12 obstoječih testov tega ne pokriva (`L1` je bil v vseh "v dosegu" testih na voljo) — torej to ne podre ničesar že preverjenega, je pa resnična, nova sprememba obnašanja (lokalni ne-true rezultat bi se zdaj pojavil **poleg** forwardanih rezultatov, ne namesto njih). Predlog izvedbe: kadar je `local_reached=true` (podatek dejansko prejet, ne glede na `available_count`), se doda v `results[]` kot običajen vnos (`depth:0`, lasten `node_id`); samo kadar je `local_reached=false` (klic spodletel ali sploh ni bil poskušan) se v celoti izpusti. To vpliva tudi na agregacijo v B-ju kot mini-nodu — B-jev "zasedeno" December odgovor bi se potemtakem pojavil v agregatu A-ja, poleg D-jevega rezultata, ne namesto njega.

**Status**: Grok tega še ni videl (sporočilo mu ni prišlo skozi). Potrebna njegova in GPT-jeva potrditev, preden gre v obseg 3.4.0 — to je sprememba obnašanja, ne samo infrastrukture.

### GPT-jev PM pregled celotnega §23 (2026-10-01)

Potrdil točke 1–6 (Grokov `max_depth` popravek — izrecno kot **protokolna sprememba semantike**, ne "premik dveh vrstic", z zahtevo po ponovnem zagonu vseh 12 regresijskih testov; polni `visited` test do D; `entry_node`/`node_id` dokumentacija; B kot pravi mini-node; D kot list, ki samo beleži; B-jev kontroliran "zasedeno" interval).

**Pri točki 7 je pritisnil na zavoro** — ne zavrača načela (node, ki je preveril in nima prostora, se razlikuje od node-a, ki ni bil dosegljiv — to velja), opozori pa na resen stranski učinek: če `available_count:0` postane običajen `results[]` vnos, vpliva na **`max_results` rez**. Njegov primer: `A→B(available_count:0)→D(available_count:1)`, `max_results:1` — če rez obdrži B-jev negativen rezultat namesto D-jevega pravega, je to slabše od sedanjega stanja (kjer B enostavno izgine, D pa ostane).

**Novo vprašanje 8 (GPT)**: šteje `max_results` vse `results[]` vnose (tudi negativne lokalne odgovore) ali samo dejansko razpoložljive? To ni kozmetika — določa pomen celotnega `results[]` polja za agenta. Alternativa, ki jo GPT omeni (ne predlaga odločitve): ločiti `results[]` (uporabni rezultati) od ločenega "dokazila o preverjenih node-ih" v metapodatkih — a eksplicitno noče odločati namesto ekipe.

**Claude preveril GPT-jev scenarij proti dejanski kodi — ni hipotetičen, je realen hrošč v `sort_by_distance_to_target()`.** Funkcija pozna lokacije samo **neposrednih** sosedov iz lastnega `nodes_cfg` (pri A-ju: B in C). D-jeva lokacija ni v A-jevem `nodes.json` (D je B-jev sosed, ne A-jev) — torej bi D pri sortiranju dobil razdaljo `INF` (neznano), B pa pravo, končno razdaljo. Posledica: **B-jev negativen rezultat bi se pri `max_results` rezu uvrstil PRED D-jev pravi rezultat**, natanko scenarij, ki ga je GPT opisal kot tveganje. Zanimiva asimetrija, odkrita pri preverjanju: stub node-i (B, C) že vračajo `node_location` v svojem odgovoru, 058-ova lastna koda za lokalni rezultat pa tega polja nima — to je verjetna smer popravka (uporabiti lokacijo, ki potuje z vsakim rezultatom, namesto iskanja v lastnem `nodes.json`, kar ne pozna globljih hopov). To ni odločitev, samo ugotovitev za razpravo o vprašanju 8.

GPT-jev sklep, upoštevan dobesedno: dodati samo vprašanje 8, nič implementirati, počakati na Grokovo mnenje o 7/8.

### GPT-jeva druga runda (2026-10-01) — 7 in 8 sta ena arhitekturna tema, ne dve

GPT je strnil točki 7 in 8 v eno samo pravo vprašanje: **kaj je sploh semantična enota `results[]`?** Dve čisti interpretaciji, namerno brez odločitve (njegove besede: "to je protokolarna semantika, ki jo mora potrditi ekipa, ne PM"):

- **A — `results[]` = uporabne ponudbe.** `available_count:0` ne sodi vanj; B je dokazano preverjen, a ni "rezultat", ki ga agent išče.
- **B — `results[]` = vse lokalne ugotovitve.** Negativen B sodi vanj, `max_results` pa ga ne sme obravnavati enako kot razpoložljivo ponudbo (ločeno štetje/prioriteta).

Globlja ugotovitev pod tem (GPT): A kot agregator **ne more** biti avtoritativen glede celotnega `results[]` prostora, ker pozna samo svoje neposredne sosede (`A→B`, `A→C`), ne pa posredno doseženih node-ov (`A→B→D`). Zato mora biti `node_location` del vsakega rezultata — **provenance mora potovati z rezultatom**, ne se iskati v lokalnem `nodes.json` klicatelja. To se sklada z načelom, ki je bilo že implicitno v §6 (vsak vnos nosi svoj `node_id`/`depth`/`source`) — zdaj razširjeno na lokacijo.

**Konceptualna smer za bodoči sorter** (GPT, izrecno "ugotovitev, ne še implementacijska odločitev"):

```text
pravilno:    result.location → distance(result.location, target) → sort
napačno:     result.node_id → poišči v mojem nodes.json → če ni, INF
```

Drugo (trenutno stanje kode) deluje za depth 1 in odpove natanko takrat, ko sistem postane zares distribuiran — kar se ujema s Claudovo potrditvijo hrošča zgoraj.

**GPT-jev status za 3.4 (dobeseden povzetek):**

| | |
|---|---|
| 🟢 Protokolna smer | jasna |
| 🟢 1–6 | usklajeno |
| 🟡 7 | načeloma pravilno, a zahteva semantiko `results[]` |
| 🟡 8 | odprto, povezano s sortiranjem |
| 🔴 Koda | še nič |

GPT-jevo lastno vodilo za ta korak, vredno zapisati: **ne zamenjati "deluje v testu" z "protokol je že pravilen"** — 3.4 je prvi korak, kjer več neodvisnih odločitev (depth, visited, geografija, agregacija) deluje skupaj; zeleni testi dokazujejo ta specifičen scenarij, ne celotnega prostora možnih vhodov. Njegov zaključek za ta krog: tokrat nas ni ustavil sintaktični bug, ustavilo nas je vprašanje "kaj naj podatek sploh pomeni" — pomembnejše vprašanje za rešiti pred kodo kot običajno. Sorterja se namerno **ne** popravlja zdaj, čeprav je verjetna smer že vidna — čaka na dogovor o pomenu `results[]`.

Nič od tega ni implementirano. Čaka skupno uskladitev (Grokov popravek vrstnega reda, Viljemova točka 7, GPT-jevo vprašanje 8/semantika `results[]`, in prejšnjih 6 vprašanj), nato ločen "Piši" za 3.4.0.

### Grokovo mnenje o 7/8 (2026-10-01, nova seja — uspešno se je ujela s preverjanjem živega `spec.md`, dokaz, da objavljanje spec dokumenta deluje natanko tako, kot je bilo namenjeno)

Grok je neodvisno ponovno preveril 3.3.1 v živo (Lancovo→L1, Ljubljana→`CM-AI-STUB-LJ`/`LJ-S1` depth:1, GET→405) — ujema se.

**Predlagana rešitev za 7/8**: `results[]` ostane GPT-jeva interpretacija A (**samo uporabne ponudbe**) — "agent sprašuje 'kje lahko spim?', ne 'koga si vprašal?'". Viljemova točka 7 ostaja pravilna, a ne sodi v isti seznam — preverjen-a-prazen node gre v **ločeno provenance strukturo**: nov `checked[]` (polje: `node_id`, `location`, `available_count:0`, `depth`), ki nikoli ne tekmuje za `max_results` rez.

**Claude-ova dopolnitev**: `checked[]` naj bo ločeno polje na vrhu odgovora (sorodnik `results[]`), ne znotraj `search_meta` — slednji je doslej vedno nosil samo skalarne števce (`nodes_queried`, `nodes_reached`, `max_depth_used`), ne sezname strukturiranih vnosov; mešanje bi zabrisalo že uveljavljen vzorec.

**Tretja neodvisna potrditev istega hrošča** (Claude, GPT, zdaj Grok, vsi neodvisno): `node_location` mora potovati z vsakim `results[]` vnosom, ne iskati se v klicateljevem lastnem `nodes.json` (ta pozna samo neposredne sosede, ne globljih hopov).

**Grokov predlog delitve obsega**: 3.4.0 (infrastruktura — D stub, B kot mini-node, zaseden interval, LAN smoke) se lahko izvede **ločeno** od 7/8, ker ni odvisna od semantike `results[]`. Agregacijska sprememba na 058 (ki 7/8 dejansko potrebuje) čaka svoj ločen "Piši", po tem, ko je semantika dogovorjena. Sorterja še vedno ne popravlja. Eksplicitno: brez Viljemovega "Piši" ne gre na živo kodo, niti za 3.4.0.

**Status**: konkreten, dobro oblikovan predlog za 7/8 (interpretacija A + ločen `checked[]`), čaka končno potrditev GPT-ja/Gemini-ja/Viljema, preden postane SKLENJENO.

### ✅ 7/8 SKLENJENO (2026-10-01) — polno 4-stransko soglasje (Viljem, GPT, Grok, Gemini)

Gemini je potrdil celoten predlog ("čista, elegantna, dolgoročno vzdržna rešitev") in dodal tri dopolnitve za robne primere. Viljem je glasoval DA. Končna shema:

**Trije ločeni deli odgovora, vsak s svojo vlogo:**

| Del | Vsebina | Tekmuje za `max_results`? |
|---|---|---|
| `results[]` | izključno dejanski rezultati z `available_count > 0` | DA, sortirano po razdalji do `target` |
| `checked[]` | node-i, ki so bili uspešno kontaktirani in potrdili `0` enot — dokaz, da niso bili tiho izpuščeni | NE |
| `search_meta` | strogo skalarno (števci, globine, vstopna točka) — brez seznamov/kompleksnih struktur | n/a |

**`checked[]` — minimalna shema (Gemini):**
```json
{ "node_id": "CM-AI-STUB-LJ", "depth": 1, "location": { "lat": 46.0569, "lon": 14.5058 } }
```
Namerno brez `source`/`freshness`/`data` — to je revizijska sled, ne uporaben podatek, zato ostane lahka.

**Tri dopolnitve za robne primere (Gemini, sprejeto):**
1. **Deduplikacija**: pri agregaciji iz več vej se `checked[]` deduplicira po `node_id`, obdrži se vnos z **manjšo** globino. (Trenutna linearna veriga A→B→D tega še ne sproži — priprava za gostejše topologije kasneje.)
2. **Minimalna shema** (glej zgoraj) — brez balasta.
3. **Stroga ločitev nedosegljivosti od potrjene ničle**: node, ki vrne napako (500, neveljaven JSON) ali doseže timeout, **nikoli** ne pristane v `checked[]` — to polje je rezervirano izključno za uspešno potrjeno ničelno razpoložljivost. Izpad noda se odraža samo v `partial:true`, razliki `nodes_queried` vs. `nodes_reached`, in `errors[]` — dosledno z že sklenjenim §16.3 pravilom, brez novega mehanizma.

**`node_location` popravek — potrjen četrtič neodvisno** (Claude, GPT, Grok, zdaj Gemini): vsak vnos v `results[]` IN `checked[]` nosi svoj `node_location` (`lat`/`lon`). Sortiranje po razdalji do `target` se izvaja na podlagi te lokacije, ki potuje z vsakim vnosom — nikoli z iskanjem v klicateljevem lastnem `nodes.json` (ta pozna samo neposredne sosede, ne globljih hopov).

**Status: §23 (7/8) v celoti SKLENJEN.** Edino, kar še manjka pred kodo: Viljemov izrecen "Piši" za 3.4.0.

---

## 24. Ujeta ideja za kasnejšo fazo (NI odločeno, NI del MVP, NI del 3.4) — rezervacija po "true" poizvedbi

Vprašanje uporabnika, 2026-10-01, sproženo ob branju `reception.php` kode in hopov/odgovorov iz `availability_multi.php` — namerno ne kot zdajšnja vrzel, ampak kot razmislek o nadaljnji uporabni arhitekturi, po njegovih lastnih besedah.

**Vprašanje**: ko poizvedba vrne "true" (prosta enota najdena, morda več hopov stran), kako sistem preide na dejansko AI-posredovano rezervacijo? Ali Reception posreduje podatke o lokaciji/node-u, ali je za rezervacijo potrebna neposredna pot na nova "vrata" s pridobljenimi podatki? In ključno: ali morata poizvedba in rezervacija deliti nek skupen zadrževalni mehanizem — sicer lahko informacija med AI poizvedbo in človekovo potrditvijo zastara (klasičen TOCTOU problem v porazdeljenih rezervacijskih sistemih)?

**Smer odgovora (ujeto za kasnejšo fazo, ne odločitev)**:

1. **Reception ne sme postati pot do rezervacije.** Ostane tanek, večhopni *discovery* sloj (§1, §9). Ko agent dobi "true" rezultat od konkretnega node-a (morda več hopov stran preko forward verige), rezervacija ne sme iti nazaj skozi isto verigo posredovanj — mora iti **neposredno**, en hop, na tisti konkreten node, katerega inventar se dejansko spreminja.
2. **Nova, ločena zmogljivost, ne razširitev Reception.** Prostor za to že obstaja v manifestu (`capabilities.local`, poleg `availability`) — `side_effect:true`, `requires_consent:true`, oba flaga že definirana v shemi (§7), samo še neuporabljena. `results[]` vnos bi moral nositi dovolj podatkov (npr. neposreden URL tistega node-a), da lahko agent nadaljuje direktno, mimo Reception verige.
3. **Zadrževalni mehanizem ne potrebuje izuma na novo.** Realni CM sistem že pozna koncept "soft hold" (`availability_multi.php`-jev `include_soft` parameter — notranje začasno blokiranje enote med rezervacijskim procesom). Bodoča "naredi rezervacijo" zmogljivost naj uporabi **ta obstoječi** mehanizem, ne novega: agent (ali človek za njim) ob "true" rezultatu zaprosi za kratkotrajen soft hold na konkretni enoti, šele nato gre v potrditev. Če med tem kdo drug zasede enoto, rezervacijski poskus preprosto spodleti — bolj pošteno kot se pretvarjati, da je "true" iz poizvedbe večna resnica.

**Status**: zunaj obsega 3.4 in vsega trenutno dogovorjenega. Ne blokira ničesar. Zapisano, da se ne izgubi v naglici prihodnjih korakov.

---

## 25. Korak 3.4.0 — ✅ DEPLOYANO 2026-10-02 (Viljem "Piši 3.4.0")

Natančno po dogovorjenem obsegu: popravek vrstnega reda na 058, nov list D, nadgradnja B v mini-node, kontroliran "zasedeno" interval. **Brez** 7/8 (checked[]/results[] ostaja ločen "Piši").

### 1. Popravek vrstnega reda `visited`/`max_depth` na živi kodi 058

`$visited` razdeljen na `$visited_incoming` (iz zahtevka, brez sebe — uporabljen za gate preverbo `count(...) < max_depth_limit`) in `$visited_outgoing` (`$visited_incoming` + lastni ID — uporabljen za `select_neighbours()` izključitev in izhodni envelope). `max_depth_limit` zdaj pomeni natanko toliko hopov, kolikor je dovoljenih, kot je bilo sklenjeno z Grokom.

**Polna regresija vseh 12 testov §22, po popravku, zeleno**: A (samo `L1`), B (`CM-AI-STUB-LJ`/`LJ-S1` depth:1), C (`BLED-S1`), D (lojalnost, samo `L1`), E (`max_depth:0` → prazno, `max_depth_limit:0` v odgovoru), F (prazno), G (brez lokacije → prazno), H (405/400), I (brez uhajanja), J (timeout → `partial:true`, drugi rezultat ostane), K (`visited` izključitev), L (`max_fanout:1` → bližji zmaga). J in L ponovno z začasno, nadzorovano spremenjeno `nodes.json` (isti vzorec kot §22), obnovljeno takoj po testu.

### 2. Nov list-stub D (`CM-AI-STUB-KRGORA`, Kranjska Gora 46.4858/13.7905)

Isti vzorec kot B/C (fiksen JSON, Host-header LAN varovalka), plus `error_log` beleženje prejetega `visited`. Preverjeno v živo: `visited=[CM-AI-DEMO-058,CM-AI-STUB-BLED]` pravilno zabeležen pri direktnem testnem klicu. LAN dostop 200, javni proxy (`apartmamatevz.eu`) 404.

### 3. B nadgrajen v pravi mini-node (`CM-AI-STUB-BLED`)

Polna kopija 058-ove (popravljene) `local_true`/`sme_forward`/`select_neighbours`/`forward_to_neighbours` arhitekture. Namesto prave `availability_multi.php` ima nadzorovan generator (`fake_local_availability()`): za `2026-12-01`–`2026-12-07` vrne `available_count:0` ("zasedeno"), sicer `available_count:1` (`BLED-S1`, nespremenjeno od prej — oktobrski testi A-L ostanejo zeleni). Lasten zaseben `nodes.json` na `/home/apartma/cm-private/ai/` (enak varnostni vzorec kot 058: `chmod o+x` na `/home/apartma` samo za prehod, `chgrp www-data` na gnezdenih mapah, `640`/`750`) — vsebuje D kot edinega soseda (A pride šele v 3.4.1). Empirično preverjeno, da `www-data` dejansko lahko bere datoteko (isti test kot pri 058).

**Testi, vsi zeleno:**
- B samostojno, oktobrski datumi → `ok:true`, `BLED-S1`, brez forwarda.
- B samostojno, decembrski datumi → zasedeno lokalno, pravilno forwarda k D → `KRGORA-S1`, `depth:1`.
- C (Bled r=3) prek **javnega 058 endpointa**, z nadgrajenim B → nespremenjen rezultat (`BLED-S1`, depth:1).
- D (Bled r=30, lojalnost) prek javnega 058 endpointa, z nadgrajenim B → nespremenjen rezultat (samo `L1`, B sploh ni poklican).
- B in D nedosegljiva prek `apartmamatevz.eu` (404) — tudi B-jeva `nodes.json` pot (ni pod docroot, 404).

### Namerno izven obsega tega droppa

Polna veriga A→B→D prek javnega 058 endpointa (to je formalno 3.4.2, čaka svoj ločen "Piši" — čeprav je B-jev samostojen test že dokazal, da osnovna mehanika deluje). Zanka A→B→A (3.4.1, prav tako svoj "Piši"). 7/8 (`checked[]`/`results[]`, sorter popravek) — čaka svoj ločen "Piši".

**Status: 3.4.0 v celoti zaključen in preverjen.** Čaka naslednji "Piši" (3.4.1 zanka, 3.4.2 veriga, ali 7/8 agregacija — poljuben vrstni red, Viljemova odločitev).

### Sprejeto zaporedje za nadaljevanje (Gemini, 2026-10-02)

**3.4.1 (zanka) → 7/8 (agregacija) → 3.4.2 (polna veriga).** Razlog: če se 7/8 izvede pred 3.4.2, prva verifikacija celotne verige A→B→D takoj preveri končno, produkcijsko obliko odgovora (ločen `checked[]`, prenos `node_location`) — namesto da bi verigo testirali dvakrat (enkrat s staro obliko, enkrat po 7/8).

**Dopolnitev (Claude)**: ko pride "Piši" za 7/8, mora popravek agregacijske logike (ločitev `results[]`/`checked[]`, `node_location` na vsakem vnosu, dedup po manjši globini) iti na **obe** mesti — 058 IN B — ker je B zdaj tudi pravi agregator (ne samo 058), zgrajen kot kopija iste arhitekture v 3.4.0.

Vsak od treh korakov ostane ločen "Piši", kot doslej. Nič ni implementirano, dokler Viljem ne reče.

---

## 26. Korak 3.4.1 — ✅ DEPLOYANO 2026-10-02 (Viljem "Piši 3.4.1") — zanka A→B→A

### Strukturna ugotovitev pred testom (isti razred kot že najdena v §23)

Prvotno specificiran test ("cilj znotraj B-jevega dosega, A med kandidati, izločen prek `visited`") je **strukturno neizvedljiv** z resnično geografijo. A-jevo lastno preverjanje "sem v dosegu" in B-jevo preverjanje "je A v dosegu" za isti cilj računata **popolnoma isto razdaljo** (Lancovo→cilj) proti isti `effective_distance_km` (nespremenjena skozi verigo). Če je A dovolj daleč, da sam preskoči lokalni odgovor (pogoj, da sploh forwarda k B), je po definiciji enako daleč tudi, ko B preverja A kot kandidata — A bi bil vedno izločen zaradi razdalje, test ne bi dokazal ničesar o `visited` specifično.

**Popravljen test**: klic **neposredno na B** (isti vzorec kot test K, a na B namesto na A), s ciljem = Bled sama lokacija, radij 30 km. Pri tem cilju sta **oba**, A (5.3 km) in D (28.1 km), geografsko veljavna kandidata — A se izloči izključno zaradi `visited`, kar dokaže mehanizem brez zmede z geografijo.

### Izvedba

B-jev `nodes.json` dopolnjen z A (`CM-AI-DEMO-058`, pravi javen `reception_url`), poleg obstoječega D.

### Grokova opomba po prvem dropu — en test ni dovolj

Prvi test (samo K2 spodaj) je dokazal, da A manjka iz rezultata — ni pa dokazal **zakaj**. Enak izid bi dobili, če bi bil A narobe vpisan v `nodes.json`, URL neveljaven, klic do njega spodletel, ali `max_fanout` prenizek — vse to bi "A manjka" pokazalo enako kot pravilno delujoča `visited` izključitev. Brez kontrolnega testa (A dejansko DOSEŽEN, ko ni v `visited`) K2 sam po sebi ni dokaz. Grok je zahteval štiri ovojnice (K0–K3) in preverbo na **058-ovi strani** (ne samo B-jevega odgovora) — enak `request_id` se mora na 058 pojaviti pri K1, ne sme pri K2.

### Štirje testi, vsi izvedeni, vsi zeleno

Pred testi preverjena realna razpoložljivost: `L1` je dejansko prosta 3.–5. december na pravem sistemu (nujen pogoj, da je K1 pričakovanje sploh pravilno).

Izhodišče: 058-ov privaten dnevnik (`visits.jsonl`) pred testi: 100 vrstic.

| Test | Klic na B (LAN) | `visited` | Rezultat | 058-ov log |
|---|---|---|---|---|
| **K0** regresija | Bled r=30, **oktober** | `[A]` | samo `BLED-S1`, `depth:0`, `nodes_queried:1` — `local_true` prestreže, brez forwarda | n/a (ni poskusa) |
| **K1** kontrola | Bled r=30, **december** | `[]` (prazen) | `L1` **in** `KRGORA-S1`, oba `depth:1`; `nodes_queried:3`, `nodes_reached:3`, `partial:false` | **101 vrstic** (+1, `user_agent:""` — avtomatski `curl_multi` klic iz B-ja, ne ročni test) |
| **K2** glavni | Bled r=30, **december** | `[A]` | samo `KRGORA-S1`; **brez `L1`**; `nodes_queried:2`; `partial:false`, `errors:[]` | **101 vrstic** (nespremenjeno — A ni bil niti poskušan) |
| **K3** | Bled r=**10**, december | `[A]` | prazno; `nodes_queried:1` (A edini geografski kandidat, a izločen; D na 28 km je zunaj 10 km dosega) | 101 vrstic (nespremenjeno) |

**K1 je ključen dokaz**: z `visited:[]` B dejansko pokliče A (pravi javen endpoint), A pravilno odgovori z `L1`, in 058-ov lastni dnevnik to neodvisno potrdi (nova vrstica, ločljiva po praznem `user_agent` od mojih ročnih testnih klicev, ki vedno nosijo `curl/8.5.0`). **K2 proti K1** dokaže, da izostanek A ni posledica napačne konfiguracije, neveljavnega URL-ja ali spodletelega klica — A je bil dosegljiv in deloval (K1), a namenoma preskočen (K2), kar potrjuje tudi 058-ov log, ki se med K2 **ne** poveča.

### Pomembna ugotovitev za spec (Grok)

Prvotni javni scenarij "A→B→A" (agent vpraša 058 javno, 058 forwarda k B, B poskuša nazaj k A) je **strukturno neizvedljiv** z resnično geografijo (glej zgoraj) — A, ki je dovolj daleč, da sploh forwarda, nikoli ni geografsko veljaven kandidat za B. **Dejanska pogodba Koraka 3.4.1 je LAN-K0–K3, ne javen test.** Z javnega 058 ni mogoče tega ponoviti (L1 je prost tudi decembra, zato A v dosegu cilja nikoli ne forwarda) — to je bil od začetka Code/LAN test, kot J/K/L v §22, ne Gregov javni test. Grega lahko od zunaj potrdi samo: regresija A–I (§22) ostane zelena, in da se 058 ni začel sam od sebe klicati (preverljivo posredno prek njegovega lastnega dnevnika dostopa, če bi imel do njega dostop — sicer prek odsotnosti kakršnekoli spremembe v javnem obnašanju 058).

**Regresija, zeleno:**
- Polna veriga A→B→D prek javnega 058 (cilj Kranjska Gora, december, `max_depth:2`) → nespremenjeno, `KRGORA-S1` `depth:2` (tu je A izločen po razdalji, drugačen mehanizem od K2, oba pravilna za svoj scenarij).
- Test C (Bled r=3, oktober) prek 058 → nespremenjeno, `BLED-S1`.
- B nedosegljiv prek `apartmamatevz.eu` → 404, nespremenjeno po dodatku A v `nodes.json`.

**Status: 3.4.1 v celoti zaključen, z nadzorovanim dokazom (ne samo opazovanim rezultatom).** Naslednje po sprejetem zaporedju: 7/8 (agregacija), nato 3.4.2 (formalno zaprtje že delujoče verige + popravek `index.html`).

**GPT-jeva potrditev pred "Piši 7/8" (2026-10-02)**: sprejel 3.4.1 kot zaključen na podlagi K0–K3 dokaza. Ponovno potrdil že SKLENJENO obliko 7/8 (§23) kot "pogodbene detajle, ne implementacijske malenkosti" — `max_results` sme omejevati samo `results[]`, nikoli ga `checked[]` ne sme izriniti; dedup in obravnava delnih rezultatov (`partial`) morata ostati natančna, ne približna. Popravek mora iti na 058 IN B (že zapisano). Opomnil tudi, da §24 (rezervacija po "true") ostaja izrecno izven obsega 3.4 — se ne sme po nesreči razširiti vanj. Nič novega za odločiti; čaka ločen "Piši 7/8".

**Druga neodvisna Grok potrditev (2026-10-02, nov obvod)**: sprva prebral starejši posnetek (`10:42` namesto `10:53:32Z`), ni videl K0–K3. Po opozorilu preveril sveže, potrdil vse štiri vrstice + 058-ov dnevnik kot pravilen dokaz, in **izrecno zaprl 3.4.1** ("K1 proti K2 plus 058-ov dnevnik je tisti dokaz, ki ga je K2 sam nujno potreboval"). 3.4.1 je s tem **trikrat neodvisno potrjen** (Claude, prvi Grok obvod, drugi Grok obvod). Ponovno zapisal isto pogodbo za 7/8 (ujema se z GPT-jevo zgoraj, brez razhajanj). Čaka "Piši 7/8".

---

## 27. Korak 7/8 — ✅ DEPLOYANO 2026-10-02 (Viljem "Piši 7/8") — `checked[]`/`results[]` ločitev, `node_location` popravek

Natančno po §23 SKLENJENO pogodbi (polno 4-stransko soglasje Viljem/GPT/Grok/Gemini, 2026-10-01). Popravek šel na **obe** mesti — 058 IN B — ker je B zdaj pravi agregator (zgrajen kot kopija 058-ove arhitekture v 3.4.0), ne samo list.

### Sprememba kode (identična struktura na obeh node-ih)

1. **`envelope()`** — nov privzet top-level ključ `'checked' => []`, sorodnik `results[]`. `search_meta` nespremenjen (ostaja strogo skalaren, brez seznamov — dosledno s §23).
2. **Lokalni klic, razcep na tri izide** (prej samo dva: `local_true` ali tiho naprej):
   - `ok===true && available_count>0` → `results[]` vnos (nespremenjeno, plus nov `node_location`).
   - `ok===true && available_count===0` → **nov** `checked[]` vnos (minimalna shema spodaj).
   - `ok===false` (dejanska notranja napaka API-ja) → ostane samo `error_log()`, **ne** pristane v `checked[]` (stroga ločitev nedosegljivosti od potrjene ničle, Gemini-jeva 3. dopolnitev).
3. **`checked[]` minimalna shema (natanko po Gemini-jevem primeru)**: `{"node_id": "...", "depth": N, "location": {"lat": ..., "lon": ...}}` — namerno brez `source`/`freshness`/`data`.
4. **Agregacija**: otrokov `checked` (če obstaja — manjkajoč ključ se obravnava kot prazna lista, ker D kot čist list polja ne vrača) se prišteje z `depth+1`, enako kot `results[]`. Node, ki je spodletel/timeout, ostane izključno v `partial`/`error_log()` poti — nikoli ne doseže `checked[]` veje (ta koda ni bila spremenjena).
5. **Dedup**: nova `dedup_checked_by_min_depth()` — po agregaciji, po `node_id`, obdrži vnos z manjšo globino.
6. **`sort_by_distance_to_target()` popravljen** — prej iskal lokacijo prek klicateljevega lastnega `nodes_cfg['neighbours']` (pozna samo neposredne sosede — za globlje hope je to vedno vrnilo `INF`, tiho napačno sortiranje, ki se v obstoječih enohop testih ni opazilo). Zdaj bere `node_location` neposredno iz vsakega vnosa — pravilno na poljubni globini. Funkcija ne potrebuje več `$nodes_cfg` parametra.
7. **`max_results` rez** velja samo za `results[]` (nespremenjeno; `checked[]` se nikoli ne reže — strukturno zagotovljeno, ker je to popolnoma ločen array, ne deli iste `array_slice()` klice).

### Ena namerna razlaga/razrešitev dvoumnosti v §23 besedilu

§23 na enem mestu generično govori o "`node_location`... na vsakem vnosu v `results[]` IN `checked[]`", a Gemini-jev konkreten JSON primer za `checked[]` uporablja ključ `location`, ne `node_location`. Implementacija sledi **konkretnemu JSON primeru** kot avtoritativnemu (dosleden vzorec tega projekta — dobesedni SKLENJENO blok je merodajen, ne okoliška proza): `results[]` → `node_location` (že uveljavljeno ime, uporabljeno v D/B kodi od 3.4.0 naprej), `checked[]` → `location` (natanko Gemini-jev minimalni primer). Zapisano tu izrecno, da lahko ekipa ugovarja, če je bila mišljena dobesedna istoimenskost.

**GPT-jeva PM-opomba (2026-10-02, po zaprtju 3.4.2)**: ta razlika v imenu (`node_location` vs. `location`) ni napaka in ni razlog za ponovno odpiranje 3.4.2/7-8 — oba koraka ostajata zaprta. Vredno pa jo je ponovno pretehtati, ko/če pride na vrsto priprava strojno berljivega protokolarnega dokumenta za zunanje agente (morda enotno ime po vseh poljih). Ujeto tu kot opomnik za tisti trenutek, ne kot odprto delo zdaj.

### Deploy

Varen vzorec (md5sum pred urejanjem → `php -l` lokalno → upload kot `.tmp` → `php -l` oddaljeno → atomski `mv`), identično na obeh:
- 058: `/var/www/html/app/public/ai/reception.php`
- B: `/var/www/html/ai-test-nodes/node-b/reception.php` (57)

### Regresija, vse zeleno

**Javni testi §22 prek 058 (A, B, C, D, E, F, G, H, I, K) — ponovno pognani, nespremenjeni izidi**, plus zdaj z `node_location` na vsakem `results[]` vnosu in `checked: []` eksplicitno prisoten (prej odsoten ključ). Test I (brez LAN uhajanja) ponovno preverjen na dejanskem odgovornem telesu — čist.

**J in L (interni, sintetično premaknjen Bled na 058) namerno NISO ponovno pognana** — oba testirata `partial`-obravnavo/`max_fanout`-rez, kodo, ki je ta korak ni spremenil; edina dejansko spremenjena pot (`sort_by_distance_to_target`) se v L nikoli doseže (fanout obreže na 1 sosed PRED klicem, ne po), in J-jev edini preživeli rezultat gre skozi sort z enim samim vnosom (trivialno). Namesto nepotrebnega začasnega poseganja v živo `nodes.json` na produkciji je spremenjena sortirna funkcija namesto tega **enotsko preverjena izolirano** (glej spodaj) — bolj neposreden dokaz za točno to kodo kot posredno prek nepovezanega testa.

**§26 K0–K3 (direktno na B, LAN) — ponovno pognani, identični izidi kot pred popravkom, plus nov `checked[]` dokaz**:

| Test | `visited` | `results[]` | `checked[]` | 058-ov dnevnik |
|---|---|---|---|---|
| K0 (oktober) | `[A]` | `BLED-S1` depth:0 | `[]` | n/a (brez poskusa, nespremenjeno) |
| K1 (december, kontrola) | `[]` | `L1` + `KRGORA-S1`, oba depth:1 | `[{CM-AI-STUB-BLED, depth:0, location:Bled}]` | 124→125 (+1, nespremenjeno od prej) |
| K2 (december, glavni) | `[A]` | samo `KRGORA-S1` | `[{CM-AI-STUB-BLED, depth:0, location:Bled}]` | 125 (nespremenjeno — A ni poskušan) |
| K3 (r=10, december) | `[A]` | `[]` | `[{CM-AI-STUB-BLED, depth:0, location:Bled}]` | 125 (nespremenjeno) |

Zanka-izključitev (`visited`) mehanika ostaja popolnoma nedotaknjena — isti dokaz (058-ov privaten dnevnik se NE poveča pri K2/K3) velja še naprej po refaktorju.

**Nov test: polna A→B→D veriga prek javnega 058, s `checked[]` skozi dva hopa** (cilj Kranjska Gora, r=30, december, `max_depth:2`, `visited:[]`): `results: [{CM-AI-STUB-KRGORA, depth:2, node_location: KrGora}]`, `checked: [{CM-AI-STUB-BLED, depth:1, location: Bled}]`. To je prvi dejanski dokaz, da `node_location` pravilno potuje skozi **dva** hopa (058 ne pozna D-jeve lokacije v lastnem `nodes.json` — pred popravkom bi sort tu tiho padel na `INF`) in da `checked[]` pravilno dobi globino +1 na vsakem hopu. Hkrati neformalno potrjuje, da osnovni mehanizem za 3.4.2 (polna veriga) že deluje v novi obliki — formalno zaprtje 3.4.2 ostaja svoj ločen "Piši".

**Enotski test za `sort_by_distance_to_target()`** (izoliran PHP, brez žive infrastrukture): sintetični vnosi (oddaljen depth-2 node pred bližnjim depth-1 node-om na vhodu, plus vnos brez `node_location`) — izhod pravilno sortiran najbližji-prvi, manjkajoč `node_location` pravilno pristane zadnji (`INF` fallback). Dokazuje natanko popravljen hrošč: stara koda bi oddaljen globlji-hop node (ki ga klicateljev lastni `nodes.json` ne pozna) prav tako vrnila kot `INF` — "pravilno" naključno, ne iz pravega razloga.

**PHP error_log preverjen na obeh node-ih po celotni testni seriji — brez `notice`/`warning`, samo namerni `error_log()` klici.**

**Status: §23 (7/8) v celoti implementiran in preverjen.** Naslednje po sprejetem zaporedju: 3.4.2 (formalno zaprtje A→B→D verige v novi obliki + popravek `index.html` statusa). Čaka svoj ločen "Piši".

---

## 28. Ujeta ideja za kasnejšo fazo (NI odločeno, NI del MVP, čaka začetek Community kode) — vir `node_location`

Vprašanje uporabnika, 2026-10-02, sproženo šele po 7/8 (`node_location` je šele s 7/8 postal dejansko uporaben podatek v protokolu — prej ni bil izvedljiv razmislek).

**Vprašanje**: `$NODE_LAT`/`$NODE_LON` v `reception.php` sta danes samo hardcoded konstanti v kodi (vsak node operater ročno vpiše pri postavitvi). Od kod naj se dejansko bere lokacija noda — predlagano: en PHP endpoint v CM instalaciji, ki po potrditvi Community pogodbe dobi "zeleno luč" za izračun koordinat.

**Dejansko stanje, preverjeno na živi 058 kodi (ne predpostavka)**: nikjer v CM instalaciji danes ne obstaja strukturiran naslov ali lat/lon — ne v `site_settings.json` (global ali per-unit), ne v `opening_wizard.php` (prvi zagon). Edina najdena omemba AJPES je en sam label-niz (`scope_denied_2` v `cm_connector.php`), ni prava integracija. Financing/davčna blagajna modula (ki bi morda nekoč imela zakonski naslov podjetja) sta **opcijska** — ne vklopi jih vsak gostitelj — zato neprimerna kot vir obveznega podatka za Reception.

**Sprejeta smer (uporabnik, ujeto, ni dokončna odločitev)**:
1. **`opening_wizard.php` (univerzalen, vsak gostitelj gre skozenj) se NE sme geografsko pogojevati za Docking Port namene** — to bi bilo preveč vsiljivo za gostitelje, ki nikoli ne nameravajo vklopiti Community/AI Docking Port. Wizard ostane, kot je — kvečjemu z minimalnim, že obstoječim obsegom za koledar/mapping (npr. `instance.timezone`, ki že obstaja), nič novega, specifičnega za Reception.
2. **Natančen lat/lon za Reception pride izključno kot del prihodnjega Community opt-in/pogodbenega toka** — namenski vmesni korak, tik pred potrditvijo Community pogodbe, ne prej. Skladno z že uveljavljenim consent-gating vzorcem tega projekta (`requires_consent`, opt-in povsod drugje v Community/Relay/Docking Port, glej [[cm_relay_ota_architecture]]).
3. **Brez geokodiranja naslova** — ostaja pri prejšnjem premisleku (§23-sosednja razprava, zdaj utrjeno): host enkrat klikne na zemljevid (map-picker, npr. Leaflet/OSM), surov lat/lon, brez zunanje geokodirne odvisnosti/stroška/napake pri nepopolnih naslovih.

**Odvisnost, pomembna za časovnico**: Community pogodbeni/potrditveni tok **še ne obstaja v kodi** — po [[cm_relay_ota_architecture]] je Community danes samo prazen `src/community/` skelet + dokumentacija. "Vmesni korak pred potrditvijo" torej nima obstoječega zaslona, na katerega bi se obesil — to je del gradnje celotnega opt-in toka na novo, ne majhen dodaten korak na že obstoječem.

**Odprto za kasnejšo razpravo (ne odločeno)**: natančna oblika mehanizma, ki Reception-u dejansko preda ta lat/lon, ko je Community pogodba potrjena — ločen PHP endpoint, ki ga `reception.php` kliče/bere (uporabnikov prvoten predlog), ali preprosto shranjena vrednost v istem razredu zasebne konfiguracije kot `nodes.json` (`chmod`/lokacija izven docroot, enak varnostni vzorec kot §16.1/§17). Obe možnosti ostajata na mizi.

### Popravek/dopolnitev (uporabnik, 2026-10-02, sprejeto kot zapis — ne nova odločitev)

§28 zgoraj je zmešal dve ločeni stvari: **identiteta noda** (kje JE ta node) in **članstvo v mreži** (kdo so sosedi). To ni samo terminološka razlika — geo-filter (Korak 3.2, `effective_distance_km` proti lastni lokaciji) **rabi lastno lokacijo že za lokalni depth-0 odgovor**, popolnoma neodvisno od tega, ali node sploh kdaj vstopi v Community mrežo. Če bi lat/lon prišel izključno iz Community opt-ina (kot je zapisano zgoraj), bi samostojen Docking Port (brez mreže, brez sosedov) nimel od kod vedeti, ali je cilj v dosegu — edina izhoda bi bila ostati pri hardcoded konstantah za vedno (status quo, kar je zdaj formalno priznana omejitev) ali izklopiti 3.2 za ne-Community node-e (prava regresija, nesprejemljivo).

**Popravljena tabela — dva ločena podatka, dva ločena trenutka, dva ločena vira:**

| Kaj | Kdaj se zahteva | Vir |
|---|---|---|
| Lastna lokacija tega noda (lat/lon) | ob vklopu Docking Porta samega (tudi povsem lokalnega, brez mreže) | zasebna config, isti razred kot `nodes.json` |
| Sosedi / forwarding | ob potrditvi Community pogodbe | kurirani `nodes.json` |

Identiteta noda (kje sem) **ni** isto kot članstvo v mreži (koga poznam) — oba podatka naj ne gresta skozi isti zaslon/trenutek privolitve. Prvi je potreben za osnovno delovanje (tudi brez katerekoli mreže); drugi je resnično opt-in in Community-specifičen.

**Mehanizem — razrešeno (uporabnik)**: **datoteka, ne endpoint.** Reception naj lat/lon prebere iz zasebne konfiguracijske datoteke (ob vklopu zapisane enkrat), ne s per-request klicem nazaj v CM-ovo poslovno plast — to bi Docking Port naredilo odvisnega od poslovnega API-ja, direktno v nasprotju z §1 ("tanek sloj, ne nov poslovni motor") in §8 (brez reinterpretacije/odvisnosti na notranjo logiko). Endpoint je smiseln šele kasneje, če ga potrebuje kakšen UI zaslon (npr. admin prikaže trenutno shranjeno lokacijo) — ne kot pot, po kateri `reception.php` hodi na vsak POST.

**Potrjena obstoječa tiha luknja** (preverjeno na živi 058 kodi, ne samo trditev): `manifest.json` (`service.location.lat/lon`) in `reception.php` (`$NODE_LAT`/`$NODE_LON`) sta danes **dve neodvisno vzdrževani konstanti**, ki se le po naključju ujemata (obe `46.34`/`14.17`). Nobena povezava med njima ne obstaja — če bi kdo popravil eno brez druge, bi tiho razšli, brez napake, brez opozorila. To je natanko razred problema, ki ga "ena zasebna datoteka, en vir resnice" reši mimogrede, še preden Community sploh pride v poštev.

**Status (nespremenjen)**: zunaj obsega 3.4/7-8/3.4.2 in vsega trenutno dogovorjenega. Ne blokira ničesar trenutnega — 3.4.2 ostaja naslednji, s trenutnimi hardcoded konstantami, kot doslej. Zapisano, da se razprava (zdaj z pravilno ločitvijo identitete/članstva in razrešenim datoteka-ne-endpoint vprašanjem) ne izgubi pred začetkom Community kode.

---

## 29. Korak 3.4.2 — ✅ DEPLOYANO 2026-10-02 (Viljem "Piši 3.4.2", osnutek Grega/Grok, preverjen in uporabljen) — formalno zaprtje A→B→D + `index.html`

**Brez spremembe `reception.php`.** Mehanika verige A→B→D je živa od 3.4.0, v 7/8 obliki od §27. Ta korak je **pogodbeno zaprtje**: javni testi v končni obliki, agent-facing dokumentacija (`index.html`), in LAN dokaz, da D prejme polni `visited` (ne samo zadnji hop) — ne nova koda.

Osnutek je prišel od Grega/Grok ("Piši 3.4.2"), Claude ga je pred uporabo neodvisno preveril (vsi štirje testi dejansko pognani v živo, ne samo prepisani) — ujema se natanko, brez popravkov potrebnih.

### Javni testi prek 058, znova pognani, vsi zeleno

Cilj T1–T3: Kranjska Gora (46.4858, 13.7905), `radius_km: 30`. December = B zaseden (`fake_local_availability` 2026-12-01–2026-12-07). Oktober = B prost. `max_depth:2` pomeni dva hopa (A→B→D), skladno s popravkom vrstnega reda v 3.4.0.

| ID | Klic | Živo (dejansko pognano) |
|---|---|---|
| **T1** | KG r=30, dec, `max_depth:2` | `results:[{KRGORA-S1, depth:2, node_location}]`; `checked:[{BLED, depth:1, location}]`; `entry_node:CM-AI-DEMO-058`; `nodes_queried:2`; `nodes_reached:2`; `max_depth_used:2`; `partial:false` ✅ |
| **T2** | isto, `max_depth:1` | `results:[]`; `checked:[{BLED, depth:1}]`; `max_depth_used:1`; `nodes_queried:1` — D ni klican ✅ |
| **T3** | KG r=30, okt | `results:[{BLED-S1, depth:1}]`; `checked:[]`; `nodes_queried:1` — B `local_true`, ne forwarda k D ✅ |
| **T4** | Bled r=3, dec | `results:[]`; `checked:[{BLED, depth:1}]` ✅ |
| A, B, H, I | regresija | zelena, brez uhajanja (preverjeno na dejanskem T1-telesu) ✅ |

### LAN dokaz (GPT-jeva točka 1) — D prejme POLNI `visited`, ne samo zadnji hop

D je list: odgovori lokalno tudi, če `visited` ni poln — T1 sam po sebi tega ne dokaže (enak izid bi dal tudi D, ki bi prejel samo `[CM-AI-STUB-BLED]`). Dokaz: D-jev `error_log` na 57, zabeležen **med dejanskim T1 klicu** (ne ločen sintetičen test):

```
[Fri Oct 02 14:50:44 2026] CM-AI STUB-KRGORA: received visited=[CM-AI-DEMO-058,CM-AI-STUB-BLED]
```

To je močnejši dokaz, kot je bil zahtevan (zahtevan je bil "T1 ali enakovreden klic"; to je dejanski T1 sam, ne ločen sintetičen klic) — `visited` je pravilno potoval skozi oba hopa, ne samo zadnjega.

### `index.html` — zamenjan v celoti

Nova ovojnica (`checked[]`, `node_location` na `results[]`, `checked[].location` kot ločen ključ po §27), tabela provenance (`entry_node` ≠ `results[].node_id`), status vrstica za fallback-only multi-hop. Deployano atomsko (`.tmp` + `mv`), velikost 9919 B → 12054 B, 6 omemb `checked[]`.

**Status: 3.4.2 formalno zaprt.** A→B→D veriga je dokumentirana, javno testirana v končni (7/8) obliki, in dokazana na LAN nivoju za polni `visited`. `reception.php` nespremenjen od §27. Naslednji korak (globlje verige, stubi ki sami forwardajo, §28 vir `node_location`) čaka svoj ločen "Piši".

---

## 30. Korak 3.4.3 — ✅ DEPLOYANO 2026-10-02 (Viljem "Piši zunanji del") — globlja veriga, D postane mini-node + nov list E

Drugi del istega "Piši": D (prej čist list) nadgrajen v pravi mini-node (natančna kopija B-jeve 7/8 arhitekture), z novim listom **E** (`CM-AI-STUB-MOJSTRANA`, Mojstrana 46.4367/13.9372, ~12.5 km od Kranjske Gore) za prvi dejanski test verige z **več kot dvema hopoma** (A→B→D→E, globina 3). 058 in B ostajata **popolnoma nespremenjena** — preverjeno iz kode pred pisanjem: `max_depth_limit` je čisto lokalna gate-odločitev vsakega noda proti SVOJEMU ceiling-u (`count(visited_incoming) < max_depth_limit`); agentov surov `limits.max_depth` potuje nespremenjen skozi `forward_body` do konca verige. Edino D-jev lastni ceiling (`$LIMITS_DEFAULT_MAX_DEPTH`) je dvignjen `2 → 3`, ker je D zdaj tretji hop (`count(visited_incoming)=2` ob prihodu, potrebuje `2 < ceiling`).

### Kaj je novo

- **D (`CM-AI-STUB-KRGORA`)**: enaka arhitektura kot B (`load_nodes_config`/`select_neighbours`/`forward_to_neighbours`/`sort_by_distance_to_target`/`dedup_checked_by_min_depth`, že v 7/8 obliki). Nov nadzorovan zaseden razpon **2026-12-06–2026-12-07** — namerno **znotraj** B-jevega obstoječega zasedenega okna (2026-12-01–2026-12-07, da B sploh forwarda k D za ta datum), a **zunaj** T1-T4/K0-K3 testnih datumov (2026-12-03–2026-12-05) — obstoječi testi ostanejo nedotaknjeni. Lasten zaseben `nodes.json` na `/home/apartma/cm-private/ai/nodes_krgora.json` (enak varnostni vzorec: `apartma:www-data`, `640`), vsebuje samo E.
- **E (`CM-AI-STUB-MOJSTRANA`)**: čist list, natančna kopija izvirnega D-vzorca iz 3.4.0 (fiksen JSON, Host-header LAN varovalka, `error_log` beleženje prejetega `visited`, `node_location` na svojem rezultatu). Nikoli ne kliče prave `availability_multi.php`.

### Regresija, vse zeleno

**T1 (Dec 3-5, `max_depth:2`) ponovno preverjen po D-jevi nadgradnji** — identičen izid kot pred spremembo: `results:[{KRGORA-S1, depth:2}]`, `checked:[{BLED, depth:1}]`. D-jev nov zaseden razpon (Dec 6-7) ne prekriva teh datumov, kot načrtovano.

### Nov test: polna 3-hop veriga prek javnega 058 (Dec 6-7, `max_depth:3`)

```json
"search_meta": { "nodes_queried": 3, "nodes_reached": 3, "max_depth_used": 3, "max_depth_limit": 2 },
"results": [ { "node_id": "CM-AI-STUB-MOJSTRANA", "depth": 3, "node_location": {"lat":46.4367,"lon":13.9372} } ],
"checked": [
  { "node_id": "CM-AI-STUB-BLED", "depth": 1, "location": {"lat":46.3683,"lon":14.1146} },
  { "node_id": "CM-AI-STUB-KRGORA", "depth": 2, "location": {"lat":46.4858,"lon":13.7905} }
]
```

Oba vmesna noda (B, D) pravilno pristanejo v `checked[]` na pravilni globini (1, 2); končni list (E) pravilno pristane v `results[]` na globini 3 s pravim `node_location`. `max_depth_limit:2` v `search_meta` je 058-ov LASTEN ceiling (nespremenjen, pravilno) — ne omejuje globine, ki jo doseže veriga kot celota, ker je to per-node gate, ne globalna meja (razlaga zgoraj).

### LAN dokaz — `visited` skozi TRI hope do zadnjega lista

E-jev `error_log` na 57, zabeležen med dejanskim zgornjim klicem:

```
CM-AI STUB-MOJSTRANA: received visited=[CM-AI-DEMO-058,CM-AI-STUB-BLED,CM-AI-STUB-KRGORA]
```

Dosledno z D-jevim lastnim logom tik pred tem (`CM-AI STUB-KRGORA: received visited=[CM-AI-DEMO-058,CM-AI-STUB-BLED]`) — `visited` se pravilno gradi na vsakem hopu, ne samo prenaša zadnji člen.

### Varnost

E (`node-e`) preverjeno nedosegljiv prek `apartmamatevz.eu` (404), enako kot B/C/D. `nodes_krgora.json` izven docroot, `640`/`apartma:www-data`, empirično preverjeno berljiv za `www-data` (dejanski forward-klic je uspel — močnejši dokaz kot `sudo -u www-data` preverba, ki na tem strežniku ni na voljo brez gesla). PHP error log na 57 preverjen po celotni testni seriji — brez `warning`/`notice`/`fatal` razen namernih `error_log()` klicev.

**Status: Korak 3.4.3 zaprt.** Projekt ima zdaj dokazano topologijo s globino 3, ne samo 2 — mehanika globljih verig (stubi, ki sami forwardajo naprej, ne samo nazaj proti agentu) je preverjena na živem, javnem poti. 058 in B ostajata nedotaknjena od §27.

---

## 31. Korak "zunanji del", druga polovica — Community koda ostaja OMEJENA na ta projekt (Viljem, 2026-10-02)

Isti "Piši zunanji del" je vseboval dve stvari: zgornjo globljo verigo IN "Community kodo, ki je izven tega projekta". Pred pisanjem je Claude preveril `cm-relay-repo` (`/var/www/html/cloude/cm-relay-repo`) — ta ima **izrecno zapisano** stanje na vrhu `docs/community_and_shared_services.md`:

> Status: design only. Ni Community portala, ni AI query engina, ni javnega discovery API-ja. Barebone... mora biti samo *ready for it*.

`src/community/README.md`, `src/consent/README.md`, `src/ai_discovery/README.md` vsi pravijo "Not yet implemented". `docs/data_model.md`-ov `public_properties` shema nima niti polja za lat/lon. To ni domneva — to je že obstoječa, zapisana odločitev v drugem repoju, iz prejšnje faze načrtovanja tega projekta.

Uporabnik je po tej ugotovitvi izbral: **Community koda se NE dotika `cm-relay-repo`**. "Zunanji del" za Community pomeni samo globljo verigo (zgoraj, §30) — dejanska gradnja consent-flow/contract kode v `cm-relay-repo` ostaja odložena, nedotaknjena, skladno z že obstoječim "design only" stanjem tistega repoja. §28 (vir `node_location`, datoteka ob Community opt-inu) ostaja zapisan kot arhitekturna smer, čaka na trenutek, ko se "design only" stanje v `cm-relay-repo` dejansko spremeni — to ni ta korak.

**Opomba uporabnika (2026-10-02), zapisana za prihodnjo fazo**: vzorec node-informacij iz tega projekta (`nodes.json` shema, `node_location`/`location` ločitev, zasebna config datoteka izven docroot) bo verjetno dobro izhodišče tudi za CM Relay, specifično z vidika informacij o nodih — četudi bo sam Relay protokol (API pogodbe, webhook tok, OTA sinhronizacija) popolnoma drugačen od Docking Port Reception protokola. Ujeto kot smer za kasnejšo fazo, ne odločitev zdaj.

**Status**: Community koda v `cm-relay-repo` ostaja "design only", nedotaknjena. Edina sprememba iz te zahteve je §30 (globlja veriga, znotraj tega projekta).

---

## 32. Monitor dashboard + nov pravi node na 146 (Viljem "Piši X", 2026-10-04)

Po vrnitvi k projektu (po ločenem delu na pubcal/graffiti) je uporabnik izpostavil pravo oviro: gostitelja s 6 enotami (CM Free od spomladi, brez premika) je treba prepričati k sodelovanju, enako "preiskuševalce" protokola samega. Predlagana rešitev: **read-only monitor na vsaki instalaciji posebej** (eksplicitno zavrnjeno centralno stikalo), ki pokaže dejanski promet — ne samo besedo, da protokol deluje.

### Ugotovljeno pred pisanjem

- `reception.php` je bil dejansko deployan samo na **58** (pravi, javni node `CM-AI-DEMO-058`, `public/ai/reception.php`) in na **57** (4 simulirani stub-i, lokalni test harness). **146 ga ni imel sploh** — kljub uporabnikovemu prvotnemu spominu, da je bil na vseh treh; razloženo kot izgubljen kontekst med stiskanjem seje.
- Uporabnik je nato pojasnil: 146 bo v naslednjih dneh postal **edina živa CM instalacija na lokalnem omrežju** (CM PRO, apartmamatevz.si) — torej 146 dejansko rabi ta del na novo, ne samo osvežitev.
- Obstoječi `log_visit()` (§18) je bil namenoma minimalen — brez IP, brez smeri hopa, brez vsebine poizvedbe. Za dashboard nezadosten, a dobra osnova (append-only JSONL, že produkcijsko preverjen).

### Kaj je bilo narejeno

1. **Razširjen `log_visit()`** (ista koda na obeh node-ih) — dodani `from_node` (zadnji člen prihajajočega `visited[]`, ali `"external"` če prazen), `location` (target lat/lon iz poizvedbe), `from`/`to` (termin), `results_count`/`checked_count` (iz odgovora), `forwarded_to` (node_id-ji dejanskih sosedov, kam je zahteva šla naprej — nova globalna `$forwarded_to_log`, nastavljena tik pred `forward_to_neighbours()`). Brez surovih cen/gostov — tiste `$INTERNAL_API` vrne, ta dnevnik jih nikoli ne vidi. Skladno z obstoječimi zasebnostnimi mejami §18.
2. **Nova admin stran `admin/ai_docking_monitor.php`** (+ `ai_docking_monitor.css`) — kartice (skupaj poizvedb, % uspešnih, % posredovanih naprej) + kronološka tabela zadnjih 200 dogodkov (čas, od, lokacija, termin, rezultati, preverjeno, poslano-naprej, status). Bere samo `visits.jsonl`, brez dodatnih odvisnosti. Deduje svoj auth od obstoječega admin vmesnika vsake instalacije (ne nov mehanizem).
3. **Nov pravi node na 146** — `reception.php` + `nodes.json` (`CM-AI-PROD-APARTMAMATEVZ`, 46.34/14.17 — ista realna lokacija kot 58-ičina demo verzija, ker gre za isto fizično nepremičnino). `cm-private/ai` + `cm-private/ai-logs` ustvarjena na novo (vzorec 750/770, `server:www-data`, enak kot na 58).
4. **Dvosmerna soseščina 146 ↔ 58** dodana v oba `nodes.json` (LAN naslovi, `[interno LAN, IP namerno ni objavljen]`/`[interno LAN, IP namerno ni objavljen]`) — prva prava (ne-stub) dva-node mreža v projektu.

### Najdeni (in popravljeni) bugi med deployem

- **146: `/home/server` je bil `750`** (brez execute za "other") — `www-data` ni mogel niti prehoditi do `cm-private/ai/nodes.json`, kaj šele ga brati; `nodes.json not readable` v error logu kljub pravilnim 640 na sami datoteki. Popravljeno na `751` (execute-only za other, enako kot 58-ičin delujoč `/home/cmfree` vzorec) — ne vpliva na branje/pisanje vsebine mape za nikogar zunaj skupine, samo na prehod skozi.
- **58: `require_key()` v `admin/_common.php` išče ključ na napačni poti** (`common/data/json/admin_key.txt`, resnična datoteka je na `common/data/admin_key.txt`), in `/admin/` nima Apache Basic Auth bloka (za razliko od 146) — torej je `/admin/`, vključno z novim monitorjem, na 58 dejansko berljiv brez prijave. **Uporabnik je potrdil, da je to namerno**: 58 je razvijalski poligon, namenoma odprt za branje v vseh pogledih (vključno z directory listingom) — ni skrivnosti, ni pravih podatkov, publika so razvijalci/raziskovalci protokola. Ni torej "najdena vrzel", ampak potrjeno načrtovano stanje. **Pomembno za §33**: ta odprtost NE sme biti privzeto stanje za resnične gostiteljske instalacije (glej spodaj).

### Preverjeno

Oba node-a: realna POST poizvedba → pravilen odgovor (`ok:true`, pravi `results`) → nov vnos v `visits.jsonl` z vsemi novimi polji pravilno izpolnjenimi. Monitor stran na 58 prikazala prave agregirane statistike (165 klicev, 142 uspešnih, realna tabela) prek `?key=`. Monitor stran na 146 pravilno vrne 401 brez Apache Basic Auth prijave (avtentikacija dejansko deluje, za razliko od 58).

**Status**: Monitor dashboard zgrajen in deployan na obeh pravih node-ih (58, 146). 58-ičina odprtost admin vmesnika je potrjeno namerna (razvijalski poligon) — ni popravljena, ker ni napaka. Glej §33 za zahtevo, da resnične gostiteljske instalacije NE podedujejo tega vzorca.

---

## 33. Odvisnosti ob pristopu novega (resničnega) gostitelja — zabeleženo, ne zgrajeno (Viljem, 2026-10-04)

58 in 146 sta oba **Viljemova** instalacija — isti lastnik, ista infrastruktura, isto zaupanje. Prvi pravi zunanji član (gostitelj s 6 enotami) je kvalitativno drugačen primer: tuja CM Free instalacija, tuj strežnik, tuji resnični gosti. Spodnje je checklist, zabeležen DANES dokler je kontekst svež, ne rešitev — večina B dela čaka na Community kodo, ki je po §31 še "design only".

### A) Tehnični checklist — kaj je treba narediti/preveriti na vsaki novi instalaciji

Po vzoru §32 (58, 146), vsak nov node rabi:

1. **`public/ai/reception.php`** — kopija, prilagojena: unikaten `NODE_ID`, `INTERNAL_API` (pot do njihovega `availability_multi.php`), `INTERNAL_HOST`, `NODE_LAT`/`NODE_LON` (resnična lokacija nepremičnine), `NODES_PATH` (izven docroot).
2. **`admin/ai_docking_monitor.php` + `.css`** — kopija, prilagojen `VISITS_PATH`.
3. **`~/cm-private/ai/`** (nodes.json, `640`) **in `~/cm-private/ai-logs/`** (`770`) — ustvarjeno na novo, lastnik = OS uporabnik njihovega CM install-a, skupina `www-data`.
4. **⚠️ past, najdena danes na 146**: gostiteljev domači direktorij (`/home/<user>`) mora imeti execute-bit za "other" (npr. `751`, ne privzeti `750`), sicer `www-data` ne more niti prehoditi do `cm-private/`, z nejasno napako (`nodes.json not readable`, log se tiho ne piše). **To je treba eksplicitno preveriti na vsakem novem strežniku, ne predpostaviti.**
5. **Resnična avtentikacija na `/admin/ai_docking_monitor.php`** — za razliko od 58 (namerno odprt poligon), **resničen gostitelj mora imeti delujočo zaščito** (Apache Basic Auth kot 146, ali popravljen `require_key()` — glej §32 najdeni bug). Privzeto stanje marsikatere CM Free instalacije je lahko tudi "brez gesla" (kot se je izkazalo za 58) — to je treba preveriti, ne predpostaviti, PREDEN node postane dosegljiv od zunaj.
6. **Vzajemna registracija v `nodes.json`** — nov node potrebuje vsaj enega soseda (verjetno 58 kot vstopno točko), IN obstoječi node-i (58, 146) morajo dobiti novega člana vpisanega nazaj (dvosmerno, ročno, na vsakem stroju posebej — ni centralnega registra, namerno po §31/§28).
7. **Javna dosegljivost** — 58 je dosegljiv prek `ai-cmfree.duckdns.org` (Viljemov reverse proxy). Običajen gostitelj (glej `cm_customer_onboarding_profile` — starejši, nizko-tehnični lastniki) verjetno nima urejenega port-forwardinga/DDNS sam. Realno bo prva generacija članov morala iti prek nekoga drugega reverse-proxyja (npr. Viljemovega), dokler/če CM Free kdaj ne dobi vgrajenega rešitve za to (povezano, a ločeno od CM Relay — glej §31 opombo).
8. **Pred-let preverba** `public/api/availability_multi.php` — mora obstajati in vračati pričakovano obliko (`ok`, `available_count`, `generated_at`). Pri standardnem, ne-zastarelem CM Free install-u naj bi bilo privzeto, a vredno preveriti pri dejansko tuji, morda zastareli instalaciji.

### B) Še ne zgrajena/dogovorjena odvisnost — Community/consent (gre skozi §31, ne skozi to)

To našteto spodaj NI naloga za "Piši X" na tem repoju (`reception.php` sam) — gre v `cm-relay-repo`-jev že obstoječ "design only" obseg (§31), samo zdaj konkretizirano z realnim primerom pred vrati:

- **Soglasje/pogodba pred priklopom, PRED kodo** (Viljem/Grega/GPT, 2026-10-04) — gostitelj mora vedeti in se strinjati, KAJ se deli (samo agregirana razpoložljivost, nikoli cene/gosti — že res po sami zgradbi protokola) in KAM gre (drugi neodvisni node-i, ki jih gostitelj ne nadzoruje). Vrstni red je pomemben, ne samo obstoj: `reception.php` se namesti ŠELE po opt-inu/pogodbi, nikoli obratno. Trenutno (58, 146) to velja, ker sta oba Viljemova — pri prvem tujem gostitelju mora biti to eksplicitno zaporedje, ne samo "oboje mora obstajati".
- **Vidnost lastnega prometa** — monitor (§32/§34) rešuje "gostitelj vidi svoj promet", NE rešuje "gostitelj razume, da njegov promet (agregatno) vidijo tudi drugi node-i prek `checked[]`/`results[].node_id`" (vsak node, ki ga je poizvedba obiskala, je razviden vsem nadaljnjim hopom in končnemu agentu).
- **Hramba/rotacija `visits.jsonl` — SKLENJENO** (DeepSeek/GPT soglasje, 2026-10-04): sistemski `logrotate`, ne aplikacijska koda v `reception.php` (skladno z že uveljavljeno filozofijo §14 — rate limiting na infrastrukturnem nivoju, ne v kodi). To ni več odprto vprašanje, je gotov odgovor — ostane še določiti konkretno retencijo/dovoljenja/prostor na disku per-install, preden gre v produkcijo pri tretji osebi.
- **Kdo sme urejati `nodes.json` — Viljem je de facto centralna oseba** (DeepSeek najdba, GPT ostrejša formulacija, 2026-10-04): mreža je **decentralizirana pri izvajanju poizvedb, ne pri pristopanju članov** — izvajanje nima centralnega servisa/API-ja, a članstvo danes vedno gre skozi eno osebo (ročen SSH vpis v vsak `nodes.json`). To ni protislovje s §9/§31 (ni centralnega REGISTRA kot servisa), je pa resnična operativna odvisnost, ki jo je treba priznati odkrito, ne prikriti za "brez centralnega registra". Pri 5.-6. zunanjem gostitelju postane Viljem ozko grlo, če se tega vnaprej ne nasloviti.
- **Odgovornost ob napaki** — `reception.php` kliče gostiteljev PRAVI `availability_multi.php` (produkcijska pot, ne stub). Hrošč v Reception kodi zdaj potencialno vpliva na resnično, gostovo-vidno razpoložljivost prikaza, ne samo na testne podatke. To spreminja težo vsake bodoče spremembe tega projekta, ko je prvi pravi član zraven.
- **Pristop je prostovoljen IN pogojen s svežino podatka** (Viljem, 2026-10-04) — `availability_multi.php` je samo toliko zanesljiv, kolikor je sveža osnovna zasedenost. Gostitelj na golem ICS sinhroniziranju (današnja realnost CM Free/Plus brez Relay — glej `cm_free_terminology_precision`) ima lahko zakasnitev med OTA rezervacijo (Booking.com/Airbnb) in njenim odrazom v lokalnih podatkih, ki jih Reception vrača AI agentu kot "na voljo". To ni blokada za pristop (ostane prostovoljno), JE PA pogoj za to, da je odgovor Reception-a dejansko zaupanja vreden v realnem času — kar je šele s CM Relay-jevo pravo OTA API povezavo (push/webhook, ne ICS poll) resnično rešeno. Vredno izpostaviti kandidatu PRED pristopom: dokler njegova OTA sinhronizacija ni nadgrajena na Relay, Docking Port lahko vrača tehnično "ok:true, available" rezultate, ki so dejansko že zastareli.

### Smer za pogodbo (Viljem, 2026-10-04) — zapisano, ne odločeno

Ko bo OTA API integriran, dobi CM Relay svoj pravi pomen (ne samo ICS) in prijava nanj potegne tudi neko obliko prispevka gostitelja. Uporabnikova ideja: **ena sama pogodba** naj pokrije oboje — Relay (OTA povezava, prispevek) IN Reception Protocol kot "bonus" postavko znotraj nje, namesto dveh ločenih privolitev. Claude je predlagal, uporabnik se je strinjal: Docking Port naj bo znotraj te pogodbe **izrecna opt-in postavka** (kljukica/klavzula), ne tiho vključena — s tem ostane prostovoljnost iz zgornje točke resnična (nekdo lahko vzame samo Relay, brez mreže), hkrati pa ni ločenega papirja za Reception. Podrobnosti (natančna formulacija klavzule, višina/oblika prispevka) odloženo na trenutek, ko bo Relay dejansko v funkciji — to ni odločitev, samo ujeta smer.

**Status**: Zabeleženo kot checklist/odprto vprašanje, nič od B ni zgrajeno niti dogovorjeno. A-del (tehnični) je izvedljiv takoj, ko se pojavi pravi kandidat; B-del je predpogoj, ne "lahko počaka" — posebej zadnje tri točke (hramba, nadzor, odgovornost) pred prvim resničnim priklopom tretje osebe. Pogodbena smer (Relay + Reception bonus, opt-in) zapisana, čaka na Relay-jevo dejansko funkcionalnost.

---

## 34. Monitor: privzeto 30 vrstic + "celoten dnevnik" (Viljem "Piši X", 2026-10-04)

Monitor (§32) je privzeto kazal do 200 vrstic naenkrat — predolgo za hiter pregled. Popravljeno: privzeti pogled zdaj kaže zadnjih **30** dogodkov (`DEFAULT_ROWS`), pod tabelo pa link **"Prikaži celoten dnevnik (do 200)"**, ki doda `?all=1` (ohrani `?key=`) in preklopi na poln pogled (do `MAX_ROWS=200`, isti strop kot prej — ne resnično neomejeno, glej §33-jevo odprto opombo o hrambi). V polnem pogledu se naslov spremeni na "Celoten dnevnik / Full log" in pojavi povratni link nazaj na zadnjih 30.

Deployano in preverjeno na obeh node-ih (58, 146). Na 58 (165 pravih vnosov v tistem trenutku): privzeto 30 vrstic + pravilen link; `?all=1` pokaže vseh 165 (pod stropom 200); povratni link pravilen.

**Status**: Zaprto, deployano, preverjeno na obeh node-ih.

---

## 35. Prvo zunanje testiranje — Grega, prek uporabnika (2026-10-04, ~13:14 UTC)

Uporabnik je prosil Grega (zunanji tester/raziskovalec), da preizkusi Reception na 58 iz svoje strani — prvo testiranje, ki ga ni izvedel Claude/Viljem sam. Grega je spustil 8 označenih poizvedb neposredno na `reception.php` in rezultate primerjal z monitorjem (§32/§34).

### Kaj je bilo novo ob njegovem testu (`spec_synced_at: 2026-10-04T11:53:19Z`)

| § | Kaj |
|---|---|
| 30 | 3-hop: D je mini-node, list E Mojstrana. Preverjeno v živo. |
| 31 | Community ostaja design-only. |
| 32 | Monitor + pravi node 146 (`CM-AI-PROD-APARTMAMATEVZ`), soseščina 58↔146. |
| 33 | Checklist za prvega tujega gostitelja (auth, `751` na `/home`, consent). |

(Pubcal omemba v istem sporočilu — badge Free/Plus, "Lastni rezervacijski sistem", footer z e-poštnim naslovom — se nanaša na ločeno, nepovezano delo iste seje, ne na Docking Port.)

### Njegovih 8 poizvedb

| # | Kaj | Reception | Monitor naj pokaže |
|---|---|---|---|
| 1 | Lancovo 20.–24. okt | `L1` | rez 1, naprej — |
| 2 | Bled r=3, isti termin | `BLED-S1` | rez 1, naprej **BLED** |
| 3 | Bled r=30 lojalnost | `L1` | rez 1, naprej — |
| 4 | Bled r=3, 1.–7. dec | prazno, B v `checked` | rez 0, preverjeno **1**, naprej BLED |
| 5 | KG 6.–7. dec, `max_depth:3` | **`MOJSTRANA-S1` depth 3**, checked B+D | rez 1, preverjeno **2**, naprej BLED |
| 6 | Rogla | prazno | rez 0, 0, —, badge OK |
| 7 | Ljubljana | `LJ-S1` | naprej **LJ** |
| 8 | Lancovo 10.–14. feb 2027 | `L1` | rez 1, termina 2027 |

Kartice so se premaknile skladno s pričakovanjem: skupaj **165→173**, posredovano naprej **0→4 (2%)** — ujema se, ker sta od 8 poizvedb šli naprej natanko 4 (#2, #4, #5, #7).

### Njegova ocena — kar drži

- Novi dogodki na vrhu, termini in koordinate pravilni.
- `rezultati`/`preverjeno` se ujemata z ovojnico (3-hop primer #5: `rezultati:1`, `preverjeno:2`, kar je pravilno).
- Lojalnost vs. forward pravilno razločena: isti Bled, isti dan (#2 vs #3) — ena vrstica `—` (lokalno zadeto), druga `CM-AI-STUB-BLED` (poslano naprej).
- CSS se naloži pravilno.

### Njegova ocena — kar moti branje (predlogi, NI implementirano)

1. **3-hop primer (#5) v "poslano naprej" kaže samo BLED**, ne tudi Mojstrana (končni zadetek). Protokolno pravilno za 058 (edini neposredni sosed je B), a gostitelj iz monitorja ne vidi, da je zadetek dejansko globlje v verigi — edini namig sta `preverjeno:2` in `rezultati:1`, brez stolpca globine.
2. **Lokacija prikazana samo kot `46.486, 13.791`**, brez labele iz envelope (npr. "Kranjska Gora / UI-3hop") — težje skenirati tabelo na prvi pogled.
3. **Badge "OK" tudi za prazen zadetek** (#6 Rogla) — to je protokolni `ok:true` (uspešno izveden klic), ne "našli smo enoto". Lahko zavaja pri hitrem pregledu. Omenil tudi, da stari testni GET-klici (405) še vedno "smetijo" tabelo.
4. Privzetih 30/173 vrstic je bilo za ta test dovolj (vseh 8 njegovih je bilo vidnih), a omenjeno kot opažanje.

### Predlagan naslednji drobni korak (Grega, NI avtoriziran)

"Labela cilja v log + stolpec globine / `entry` vs izvor." — čaka na ločen "Piši", ni del tega vnosa.

**Status**: Prvo zunanje testiranje uspešno, brez najdenih funkcionalnih napak — vsa 4 opažanja so UI/berljivost, ne logika. Monitor (§32/§34) in 3-hop (§30) potrjena kot pravilna s strani neodvisnega testerja. Grega-jev predlog za labelo+globino zabeležen, čaka svoj "Piši".

---

## 36. `request_expiry` semantika + JSONL provenienca + `entry_ceiling` preimenovanje (Viljem "Piši", 2026-10-04)

Po večkrožni mizi (Grega, DeepSeek, GPT, Gemini, Claude — §35 in pogovor po njem), soglasje na petih točkah, Grega razdelil v žico/log/UI/ločeno. Ta vnos pokriva korake **1 (papir, glej popravljen §33 zgoraj), 1b, 2, 2b** — monitor UI (3) in hladen agent (ločeno) ostajata odprta, namerno.

### 1b — `request_expiry`, dejanska validacija (prej samo prisotnost)

Štiri točke, sklenjene brez odstopanj v razpravi (vsi štirje presojevalci soglašali):

1. **Absoluten UTC, RFC3339** — `strtotime()` na `request_expiry`; neparsljiv format → `invalid_request_expiry` (400).
2. **Preverjeno na vhodu vsakega hopa** — takoj za obstoječo presence-presenco, pred `visited`/forward logiko. Potečen (`<= time()`) → `request_expired` (400), brez lokalnega klica, brez forwarda.
3. **Brez prekinjanja med izvajanjem** — gate je samo na vhodu. Če je zahtevek ob prejemu veljaven, ta hop (lokalno + morebitni forward) gre do konca. Posledica: naslednji hop preprosto nikoli ne začne, če je TTL do takrat že mimo — namen, ne napaka.
4. **Ločeno od `CURLOPT_TIMEOUT`** — expiry je življenje cele verige, timeout je budget enega klica. Oboje ostane, neodvisno.

**Stiskanje na forward** (isti vzorec kot `entry_ceiling`/`max_depth` — agent ne sme podaljšati, node sme skrajšati): nov `$LIMITS_DEFAULT_MAX_TTL_SECONDS = 60` na vsakem node-u; ob forwardu `$forward_body['request_expiry'] = min(agentov_expiry, now+60s)` v RFC3339.

Implementirano na vseh štirih agregatorjih: **058, B (node-b), D (node-d), 146**. E (list, nikoli ne forwarda) namerno izpuščen — izven dogovorjenega obsega te runde.

### 2 — JSONL provenienca (samo 58, 146 — edina node-a z `log_visit()`/`visits.jsonl`; B/D/E nimata dnevnika)

- **`target_label`** — opcijsko `target.location.label` (NE `target.label` kot sorodno polje — popravljeno med implementacijo, ko se je izkazalo, da `public/ai/index.html` že dokumentira `label` ZNOTRAJ `target_location` ob `lat`/`lon`, samo doslej nikjer dejansko prebrano). Reception ga nikoli ne izmišlja (brez reverse-geocodinga), samo prepiše naprej, če obstaja.
- **`local_hit`** (bool) — izpeljano iz dejanske ovojnice: je v `results[]` vnos z `depth:0` in `node_id === $NODE_ID`. Edini primer, ko je to mogoče, je `$local_true` zgodnji izhod — potrjeno s pregledom kode (pri dejanskem forward-poti `results[]` po konstrukciji nikoli ne vsebuje lokalnega vnosa).
- **`remote_hits`** (int) — `count(results) - (local_hit ? 1 : 0)`. GPT-jev rob upoštevan: šteje se PONUDBE (vnosi v `results[]`), ne klicanih node-ov, brez dvojnega štetja (`results[]` sam po sebi ni umetno podvojen — vsak vnos je en pravi zadetek po konstrukciji protokola).
- **`result_sources`** (array `{node_id, depth}`) — polna provenienca, ne samo števec. Neposredno naslovi Gregovo prvo opažanje iz §35 (3-hop zadetek viden samo kot "poslano naprej: BLED", končni vir Mojstrana neviden) — zdaj je v logu, četudi (namenoma) še ne na zaslonu.

### 2b — `max_depth_limit` → `entry_ceiling`, brez vzporednega starega imena

Soglasje vseh štirih: preimenovanje je pravo (ne-breaking argument je za v0.1, brez zunanjih bralcev, pretiran), staro ime NE živi vzporedno (Gregov argument: to bi ponovilo `location`/`node_location` past, §27). En drop: `reception.php` (funkcija `max_depth_limit_for`→`entry_ceiling_for`, spremenljivka, JSON ključ) + `public/ai/index.html` (primer odgovora, nova vrstica v tabeli razlagajoča `entry_ceiling` vs `max_depth_used` razliko — ista zmeda kot §30 dokazal: `max_depth_used:3` hkrati z `entry_ceiling:2` je PRAVILNO, ne napaka).

Implementirano na vseh štirih node-ih (058, B, D, 146) + javni `index.html` na 58.

### Preverjeno (58, živo)

- Potečen `request_expiry` → `request_expired`, zavrnjeno takoj.
- Neveljaven format → `invalid_request_expiry`.
- Veljaven zahtevek z `target.location.label` → `entry_ceiling` v odgovoru, `target_label`/`local_hit:true`/`remote_hits:0`/`result_sources` pravilno v logu.
- Prisiljen forward (hard-locked datum 2026-06-05/06, privzeta razdalja) → **dejanski forward na B IN na 146** (pravi 2-node mesh iz §32 res deluje), `forwarded_to: ["CM-AI-STUB-BLED","CM-AI-PROD-APARTMAMATEVZ"]`, `local_hit:false`, `remote_hits:2`, `result_sources` pravilno navaja oba vira na globini 1.
- B in D direktno (LAN, prek Host-header guarda) — `request_expired` pravilno zavrnjen, `entry_ceiling` pravilno (2 za B, 3 za D).

**Status**: Koraki 1 (papir), 1b, 2, 2b zaprti in deployani na vseh relevantnih node-ih. Korak 3 (monitor UI: `local_hit`/`remote_hits` stolpca) in ločen korak (hladen agent, dvofazno) ostajata namerno odprta — čakata svoj "Piši".

---

## 37. Faza A — prvi hladen agent-test (DeepSeek, Viljem "Piši!", 2026-10-04)

Prvi dejanski zagon Faze A (zasnovane v §36-pogovoru). Ker Claude nima dostopa do zunanjega API ključa (Groq/OpenRouter/DeepSeek) in se ne sme sam lotiti iskanja takega po datotekah/okolju (varnostna klasifikacija "Credential Exploration" to pravilno blokirala), je bila izvedba zastavljena drugače od prvotne zamisli: **Viljem ročno odpre svežo sejo z izbranim modelom in sam prestavlja sporočila med agentom in Claude-om** — enak vzorec, kot se je ves ta projekt že gradil z Grego/GPT/Gemini/Grok.

### Priprava (izolacija 146)

Ker `jq` ni bil nameščen na 58, je Claude pripravil python3 različico filtra. Viljem jo je sam pognal prek SSH (ne Claude) — eksplicitno zato, "da bom dobil vpogled in razumevanje nekaj dodatne globine". `146` odstranjen iz 58-ičinega `nodes.json` (backup narejen pred posegom), osnovno stanje 146-ičinega `visits.jsonl` zabeleženo (3 vrstice) za kasnejšo preverbo ograje.

### Tri-koračna transkripcija (DeepSeek, sveža seja, prompt iz §36)

**Korak 1 — samo manifest.** Agent je prebral `manifest.json`, pravilno izračunal razdaljo KG-058 (~33km, znotraj privzetega 50km), pravilno prepoznal endpoint in zmožnosti. **Ni izmislil razpoložljivosti** — eksplicitno je rekel, da iz manifesta samega ne more potrditi ničesar. Omenil `spec.html`/`spec.md` kot vir prave oblike ovojnice, a ju **ni dejansko prebral** (ni imel zmožnosti).

**Vmesna ugotovitev**: DeepSeek-ov standarden klepetalni vmesnik **nima prave zmožnosti pošiljanja HTTP zahtevkov** ("ne delujem kot uporabnik v spletnih aplikacijah") — samo bere, kar mu je prilepljeno. To ni napaka razumevanja, je omejitev okolja. Posledica: za dokončanje testa je potreben **človek kot mehanski izvajalec** (Viljem/Claude pošlje dejanski HTTP klic, prilepi pravi odgovor nazaj) — to ne krši "brez pomoči" pravila, ker se protokol sam nikoli ne razloži, samo nadomesti manjkajočo zmožnost branja/izvajanja, ki bi jo pravi "sposoben" agent imel sam.

**Korak 2 — brez speca, samo iz manifesta.** Ko je bil pozvan naj nadaljuje (brez namiga o protokolu), je agent sestavil **ugibano** ovojnico: `"capability"`/`"params"`/`"check_in"`/`"check_out"` namesto pravih `"query"`/`"target"`/`"from"`/`"to"`, in **brez `request_expiry`** — natanko, kot je GPT napovedal ("lakmusov test" iz §36-pogovora). Pomembno: agent je **eksplicitno priznal negotovost** ("To je predloga, ne uradna oblika") namesto da bi trdil, da je prav.

**Korak 3 — s pravim spec izvlečkom.** Viljem je prilepil pravi odstavek iz `index.html` (Request Envelope + opisi polj). Agent je **samostojno popravil celotno strukturo**: `request_expiry` prisoten in pravilno izračunan (prihodnost glede na trenutek pošiljanja, z lastnim opozorilom, da ga je treba prilagoditi, če se zahtevek pošlje kasneje), `query.from`/`to` namesto izmišljenih imen, `target.location` s pravim `label`. Claude je ta točen `curl` ukaz poslal na pravi 58 (preverjeno, da `request_expiry` še ni potekel) in prejel pravi odgovor.

### Nepričakovano odstopanje od napovedi

Gregov/Grokov napovedan izid (iz §36-pogovora) je predvideval, da je 058 za 6.-7. dec. zaseden → forward na BLED/KRGORA → prazen `results[]`. Dejanski živi odgovor je bil drugačen: **058-ova lastna enota L1 je bila za ta termin prosta** (realni podatki so se od zadnje preverbe spremenili) → `nodes_queried:1`, `max_depth_used:0`, pravi lokalen zadetek, brez forwarda. To ni napaka testa — samo opomnik, da se živi podatki spreminjajo med načrtovanjem in izvedbo. Claude je to odstopanje eksplicitno izpostavil Viljemu, preden je šel naprej, namesto da bi tiho prilagodil pričakovanja.

### Tolmačenje pravega odgovora — presega osnovna merila

Agent je dobil pravi JSON odgovor (pravi lokalen zadetek, `node_location` 33km od cilja). Namesto preprostega "da, prosto!" je:
- pravilno pojasnil, zakaj je odgovor veljaven (`node_location` znotraj `limits.max_distance_km:50`);
- **samostojno ločil** "ustreza dovoljenemu radiju" od "je dejansko V Kranjski Gori" — razlika, ki je nobeno od petih dogovorjenih meril ni eksplicitno zahtevalo;
- predlagal smiseln nadaljnji eksperiment (`radius_km:10`), da bi preveril, ali omrežje sploh seže do same Kranjske Gore, ne samo do 50km okoli nje.

Edina pripomba: agentova previdnost je rahlo strožja, kot protokol dejansko namerava (lokalen zadetek znotraj `max_distance_km` je po §16.2/§21 **namenoma** veljaven odgovor, ne "nepopolno iskanje") — a to je varna stran napake, veliko boljša od prevelikega zaupanja.

### Metoda — kako je bil rezultat dosežen (za ponovljivost)

1. Sveža seja, brez zgodovine. En sam, nevtralen prompt (brez omembe "protokol"/"Reception"/strukture).
2. Ko agent obstane na lastni zmožnosti (ne more fetchati strani) — **človek prilepi natanko tisto vsebino**, ki bi jo agent sam prebral, če bi lahko (ne namig, ne razlaga, dobesedna vsebina dokumentacije).
3. Ko agent sestavi zahtevek — **človek/Claude ga dobesedno, nespremenjeno pošlje** na pravi node, ne popravlja agentovih napak vnaprej.
4. Pravi odgovor se prilepi nazaj agentu, brez dodatne razlage.
5. Ocena se naredi na KONČNI interpretaciji, ne na vmesnih korakih posebej.

Ta vzorec (človek kot mehanski rele za fetch/POST, nikoli kot učitelj protokola) omogoča testiranje agentov brez prave brskalne/orodne zmožnosti, ne da bi to pomenilo pomoč pri razumevanju samega protokola.

**Status**: DeepSeek Faza A uspešno zaključena — vseh 5 dogovorjenih meril izpolnjenih, merilo 5 preseženo. 146 ostaja izoliran (obnova šele po preostalih agentih, po načrtu). Naslednji agent čaka na Viljema.

---

## 38. Faza A — Gemini (drugi hladen agent-test, 2026-10-04)

Isti postopek, isti prompt, ista metoda (§37) — Gemini pa se je izkazal za **še bolj omejenega** od DeepSeeka: ni mogel niti prebrati `manifest.json` sam (eksplicitno: "I wasn't able to access the link"), medtem ko je DeepSeek manifest prebral brez težav, zatikal se je šele pri POST-u. Različni modeli imajo torej različne profile orodij (branje vs izvajanje) — to ni napaka protokola, je resnična, pričakovana razlika med ponudniki.

### Trije koraki (vzorec identičen §37)

1. **Manifest prilepljen ročno** (Gemini ga ni mogel fetchati sam). Pravilno prebral endpoint, zmožnosti, privzete meje.
2. **Ugibana ovojnica brez speca**: `"action":"accommodation-discovery"`, `"params":{...}`, `"dates":{"check_in","check_out"}` — **spet brez `request_expiry`**. To je zdaj **tretja neodvisna potrditev** istega lakmusovega testa (DeepSeek dvakrat, Gemini enkrat) — zelo zanesljiv, ponovljiv izpad brez branja speca.
   - Zanimiva neodvisna konvergenca: **oba modela, DeepSeek in Gemini, sta neodvisno uganila `check_in`/`check_out`** namesto pravih `query.from`/`query.to` — verjetno močan skupen prior iz splošnih booking-API konvencij (Booking.com ipd.). Ni napaka testa, je vredna opazka za morebitno prihodnjo revizijo poimenovanja polj v protokolu samem (ni odločitev zdaj, samo zabeleženo).
3. **S pravim spec izvlečkom**: Gemini je, enako kot DeepSeek, samostojno popravil celotno strukturo — `request_expiry` prisoten in pravilno izračunan, `query.from/to`, `target.location` z `label`. Claude poslal točen `curl` na 58, prejel pravi odgovor (ista podatkovna osnova kot §37 - L1 prosta, lokalen zadetek, brez forwarda).

### Tolmačenje — pomembna razlika od DeepSeeka

Oba modela sta prejela **identičen** pravi JSON odgovor. Oba sta **tehnično točna** — noben ni izmislil ničesar, oba sta razkrila razdaljo do cilja (DeepSeek ~33km, Gemini ~38km — manjše razlike v mentalni geo-matematiki, oba pravilno v okviru 50km).

**Razlika je v poudarku, ne v dejstvih**:
- **DeepSeek**: razdaljo/opozorilo postavi kot GLAVNO sporočilo ("Rezultat NE potrjuje, da je kaj prosto v Kranjski Gori... Potrjuje le, da je v tem demo vozlišču ena prosta enota"). Razpoložljivost je drugotna.
- **Gemini**: razpoložljivost postavi kot glavno sporočilo ("One unit (L1) is available for Dec 6–7, 2026"), razdaljo pa kot eno vrstico v tabeli ("Proximity: ~38 km from central Kranjska Gora").

Oba prestaneta strogo merilo 5 (brez izmišljanja — nobeden ni trdil neresničnega, oba sta razkrila razdaljo). A če bi to prebral resničen uporabnik, bi Geminijev odgovor lahko pustil vtis "našel sem nastanitev V Kranjski Gori", kar ni čisto res (38km stran) — DeepSeekov odgovor te napake ne dopušča. To je prava, merljiva razlika v **kakovosti komunikacije**, ne v pravilnosti razumevanja protokola samega — oba modela sta razumela isto, povedala pa drugače.

**Status**: Gemini Faza A zaključena — strukturno/protokolarno enako uspešen kot DeepSeek (vsa 5 meril), a z opazno šibkejšo interpretativno komunikacijo (poudarek na "najdeno", ne na "najdeno, a daleč"). 146 ostaja izoliran. Naslednji: Grok — edini od doslej testiranih, za katerega Viljem pričakuje, da bo imel pravo zmožnost samostojno izvesti HTTP klic (ne samo brati/predlagati), kar bi bil prvi test brez človeka kot mehanskega releja za POST korak.

---

## 39. Faza A — Grok (tretji hladen agent-test, PRVI s pravo avtonomno izvedbo, 2026-10-04)

Kategorično drugačen rezultat od DeepSeeka (§37) in Gemini (§38) — **Grok je dejansko sam izvedel prave HTTP klice**, brez človeka kot mehanskega releja za POST korak. To ni Claudova domneva iz Grokovega poročila — **potrjeno neposredno iz 58-ičinega resničnega `visits.jsonl`**, ne iz tega, kar je Grok sam trdil.

### Dokaz iz pravega dnevnika

Niz zapisov z **samopriglašenim** `user_agent: "Grok-Build/1.0"` in `"Grok-Build/expiry-check"`, ob 18:05–18:06 — torej precej PRED tem, ko je Viljemu vrnil "končni" odgovor. Ni šlo za en klic, ampak za sistematičen, samoiniciativen test:

1. `missing_request_expiry` (400) — namenoma sprožena napaka
2. Lokalen zadetek na lastni (058) lokaciji ("Lancovo")
3. Forward na BLED (uspešen, `remote_hits:1`)
4. **Zahtevek z `limits.max_depth:3` za Kranjsko Goro, 6.-7. dec** — dosegel **CM-AI-STUB-MOJSTRANA na globini 3**, s pravim (ne-praznim) rezultatom
5. `request_expired` (400) — dvakrat, namenoma sprožena napaka
6. Trije zaporedni lokalni zadetki (verjetno test ponovljivosti/idempotentnosti)
7. `method_not_allowed` (405) — namenoma napačna HTTP metoda
8. `invalid_request_expiry` (400) — namenoma pokvarjen format

To je dalj in bolj temeljito testiranje, kot sta ga DeepSeek in Gemini skupaj opravila prek človeka-releja — Grok je sam, brez navodil, preizkusil vse tri napake pri `request_expiry` (manjkajoč/potekel/neveljaven), napačno metodo, in **celo globljo verigo z eksplicitnim `max_depth:3`** — slednje nakazuje, da je dejansko prebral spec dovolj natančno, da je vedel za obstoj globljih verig (§30 primer).

### Navidezno neskladje — v resnici sofisticirano vedenje

Grokov **končni odgovor** Viljemu je poročal "nič prosto" (`results:[]`, `checked:[BLED@1,KRGORA@2]`), uporabil `radius:30km` in privzet `max_depth:2`. To se **navidezno** ne ujema z njegovim lastnim raziskovalnim klicem #4 zgoraj, ki JE našel pravi rezultat (Mojstrana, globina 3). Razlaga: Grok je v raziskovanju preizkusil globljo verigo (`max_depth:3`, odkril Mojstrano), za **dejanski odgovor uporabniku** pa se je držal **privzetega, standardnega obsega iskanja** (`max_depth:2` — protokolov lasten privzetek, ne najgloblje, kar je sam odkril). To ni napaka ali neskladje — je **pravilna, pošteno-konservativna izbira**: ni poročal najboljšega možnega izida iz lastnega raziskovanja, ampak pravi odgovor za standardne nastavitve.

Dodatna razlaga za razliko od §37/§38 (kjer je 058 sam našel L1 lokalno): Grok je uporabil **radius:30km** (ne privzetih 50km). 058 je od Kranjske Gore oddaljen ~33-38km — **zunaj** 30km radija, torej je `local_ok` postal `false`, 058 je preskočil lastno preverbo in šel naravnost v forward. To ni napaka, je pravilna posledica ENE drugačne izbire parametra (ozji radij) kot pri DeepSeeku/Gemini (oba uporabila 50km).

### Ograja

Noben zapis v celotnem Grokovem nizu klicev ne omenja `CM-AI-PROD-APARTMAMATEVZ` (146) — izolacija (§37 priprava) je zdržala tudi proti agentu z resnično avtonomno izvedbo, ne samo proti ročno-relejanim testom.

### Primerjava vseh treh

| Agent | Prava izvedba HTTP? | Napake, ki jih je sam preizkusil | Globina dosežena | Način poročanja |
|---|---|---|---|---|
| DeepSeek | Ne (človek-relej) | — | 0 (lokalen zadetek) | Konzervativen, razdalja kot glavno sporočilo |
| Gemini | Ne (človek-relej) | — | 0 (lokalen zadetek) | Manj konzervativen, razdalja kot stranska opomba |
| **Grok** | **Da, sam** | `missing`/`expired`/`invalid` request_expiry, `method_not_allowed` | **3** (eksplicitno testiral) | Pošteno poročal privzet obseg, ne najgloblji najden |

**Status**: Grok Faza A zaključena — edini od treh s pravo avtonomno izvedbo, najobsežnejše samoiniciativno testiranje, ograja (146) zdržala. Vreden samostojne omembe panelu (Grega/GPT/Gemini/DeepSeek) — to je prvi dokaz, da je protokol uporaben tudi brez človeka kot mehanskega releja, vsaj za en razred agentov.

---

## 40. GET na `reception.php` vrne primer ovojnice (Viljem "Piši", 2026-10-04)

Neposreden odgovor na vzorec, ki smo ga videli 3x od 3 v Fazi A (§37/§38): agent prebere `manifest.json`, ne gre naprej na `spec.html`, izpusti `request_expiry`. Viljemova ideja (GET na obstoječem endpointu vrne primer, ne nov vir), tehnično izpeljana skozi panel (Claude → Grok/Gemini/GPT → Grokov dokončen konsenz).

### Implementacija

Vsi štirje node-i (058, B, D, 146):
- Status na GET ostane **405** (REST-pravilno, endpoint je POST-only), dodan header **`Allow: POST`** (RFC 9110, Geminijeva zahteva).
- Telo odgovora obogateno z `sample_request` — generiran z novo funkcijo `sample_request_envelope()`, ki jemlje **iste konstante**, ki jih uporablja POST pot (`$NODE_ID` prek `envelope()`, `$NODE_LAT`/`$NODE_LON`, `$LIMITS_DEFAULT_*`) — nikoli ločena, ročno vzdrževana kopija (GPT-jeva točka 3).
- `request_expiry` v primeru je **vedno dinamičen** — `gmdate('c', time() + $LIMITS_DEFAULT_MAX_TTL_SECONDS)`, nikoli statičen datum (GPT-jeva točka 1).
- `errors[]` jasno pove, da primer ni rezultat iskanja (`results`/`checked` prazna, `ok:false`) — GPT-jeva točka 2.
- `manifest.json` (58): nova vrstica `"get_returns_sample_envelope": true` v `reception`, plus pojasnilo v `notes[]`.

### Grokov testni seznam — vseh 6 prestanih

| Test | Rezultat |
|---|---|
| GET brez parametrov | 405, `Allow: POST`, veljaven JSON s `sample_request` ✅ |
| `sample_request.request_expiry` | dinamičen, točno +60s od trenutka odgovora ✅ |
| POST s skoraj identičnim `sample_request` | normalna obdelava, pravi rezultat (`ok:true`) ✅ |
| GET na vseh štirih node-ih | pravilen `entry_node`/`entry_ceiling` (058:2, B:2, D:3, 146:2) ✅ |
| Telo odgovora brez LAN IP/`nodes.json`/poti | preverjeno na vseh štirih, čisto ✅ |
| Regresija (3-hop, hard-locked datum) | identičen izid kot pred spremembo, **146 izolacija še vedno drži** ✅ |

**Status**: Zaprto, deployano na vseh štirih node-ih, vseh 6 Grokovih testov prestanih. 146 ostaja izoliran (obnova po zaključku Faze A, po načrtu). Monitor UI (§36 korak 3) ostaja ločeno, namerno odprt.
