|
| 1 | +# ADR-105 — `urn:visionclaw` Convergence and `urn:ngm` Cutover |
| 2 | + |
| 3 | +| Field | Value | |
| 4 | +|-------|-------| |
| 5 | +| Status | Accepted (2026-06-11) | |
| 6 | +| Supersedes | the implicit "legacy scheme, no rip-out yet" prose in `src/uri/mod.rs` header and ADR-063 §1 | |
| 7 | +| Relates to | ADR-100 (ontology IRIs; keeps `urn:ngm:graph:*`), ADR-063 (URN-traced operations), ADR-077 P3 (canonical mint module) | |
| 8 | +| Affected repos | `VisionClaw` (+ the `agentbox` BC20 boundary it federates with) | |
| 9 | +| Authoritative code | `src/uri/mod.rs` (the converged minter) | |
| 10 | + |
| 11 | +## Context |
| 12 | + |
| 13 | +VisionClaw `main` carries **two** durable-identifier namespaces simultaneously: |
| 14 | + |
| 15 | +1. **The converged `urn:visionclaw:<kind>` grammar** — a complete, typed, |
| 16 | + fail-closed minter shipped at **`src/uri/mod.rs`**. It mints 7 URN kinds |
| 17 | + (`concept`, `kg`, `bead`, `execution`, `group`, `room`, `avatar`) plus |
| 18 | + `did:nostr:<hex-pubkey>` for sovereign identity, with content-addressing |
| 19 | + (`sha256-12-<12 hex>`) and 64-char BIP-340 x-only hex pubkey scopes. It is the |
| 20 | + VisionClaw-side counterpart of the agentbox `urn:agentbox:*` grammar, and it is |
| 21 | + already wired into the live provenance/ingest paths |
| 22 | + (`src/handlers/enrichment_proposals_handler.rs`, `src/agent_events/provenance.rs`, |
| 23 | + `src/agent_events/ingest.rs`). |
| 24 | + |
| 25 | +2. **The legacy `urn:ngm:*` scheme** — the pre-convergence persistence identifiers |
| 26 | + still live in ~20 sites across 5 source files. These are *not* incidental: they |
| 27 | + are the storage-IRI surface bound 1:1 to the Oxigraph named graphs |
| 28 | + (`urn:ngm:graph:knowledge`, `urn:ngm:graph:agent`, `urn:ngm:graph:ontology:*`) |
| 29 | + that **ADR-100 deliberately keeps unchanged**, plus the node/edge/domain IRIs |
| 30 | + that round-trip through SPARQL `FILTER (STRSTARTS(...))` clauses and the |
| 31 | + `iri_to_node_id` parser. |
| 32 | + |
| 33 | +The `src/uri/mod.rs` header has admitted since it shipped that "VisionClaw `main` |
| 34 | +still carries the legacy `urn:ngm:*` scheme, which is left intact here to coexist |
| 35 | +(no rip-out yet)." The 2026-06-11 ADR-gap inventory flagged this as a **critical |
| 36 | +unratified decision**: the convergence had no ADR, no cutover plan, and no statement |
| 37 | +of what migrates versus what stays. This ADR ratifies the convergence and records the |
| 38 | +cutover boundary. |
| 39 | + |
| 40 | +> **Stale-citation correction.** ADR-063 §1 cites the minter as |
| 41 | +> `src/uri/{mint.rs, parse.rs, kinds.rs}`. That layout never landed — the converged |
| 42 | +> minter is a single module at **`src/uri/mod.rs`** (mint constructors, `parse`, |
| 43 | +> `Kind`, BC20 ingest, and now `parse_dual` all colocated). All references should |
| 44 | +> point at `src/uri/mod.rs`. |
| 45 | +
|
| 46 | +## Decision |
| 47 | + |
| 48 | +### D1 — New durable identifiers mint as `urn:visionclaw` via `src/uri/mod.rs` |
| 49 | + |
| 50 | +Every **new** durable identifier minted on the VisionClaw side is produced through |
| 51 | +the typed constructors in `src/uri/mod.rs` (`concept`, `kg`, `bead`, `execution`, |
| 52 | +`group_members`, `room`, `avatar`, `did_nostr`). Ad-hoc `format!("urn:ngm:…")` / |
| 53 | +template-literal construction of **new** durable IDs is prohibited, mirroring the |
| 54 | +`uris.js` mandate on the agentbox side. New mints are converged-only; no new |
| 55 | +identifier is ever produced under the `urn:ngm:` prefix. |
| 56 | + |
| 57 | +### D2 — Parsers dual-read BOTH namespaces (persisted `urn:ngm` keeps resolving) |
| 58 | + |
| 59 | +The resolve/lookup path accepts **both** the converged grammar and legacy |
| 60 | +`urn:ngm:*`. This is implemented as `src/uri/mod.rs::parse_dual`, which: |
| 61 | + |
| 62 | +* delegates to the strict `parse` for `did:nostr:*` / `urn:visionclaw:*`, and |
| 63 | +* returns `ParsedUri::LegacyNgm { sub }` for any `urn:ngm:<sub>` identifier, carried |
| 64 | + opaquely so it round-trips its string form without being re-minted under the |
| 65 | + converged grammar. |
| 66 | + |
| 67 | +The strict `parse` is **unchanged** — it still rejects `urn:ngm:*`, because the mint |
| 68 | +path must stay converged-only. `parse_dual` is the entry point for surfaces that |
| 69 | +must resolve historically-persisted IDs. This guarantees that every already-stored |
| 70 | +`urn:ngm:node:*`, `urn:ngm:edge:*`, and `urn:ngm:domain:*` identifier keeps |
| 71 | +resolving after the cutover with zero data movement. |
| 72 | + |
| 73 | +### D3 — `urn:ngm:graph:*` named graphs stay (per ADR-100) |
| 74 | + |
| 75 | +The Oxigraph named-graph IRIs (`urn:ngm:graph:knowledge`, `urn:ngm:graph:agent`, |
| 76 | +`urn:ngm:graph:ontology:inferred`, …) are **not** changed. ADR-100 scopes ontology |
| 77 | +IRIs and explicitly leaves the `urn:ngm:graph:*` named graphs in place; this ADR |
| 78 | +reaffirms that. They are persistence-layer dataset coordinates, not federation-facing |
| 79 | +durable identifiers, and renaming them would require rewriting the entire stored |
| 80 | +quad set with no external benefit. |
| 81 | + |
| 82 | +### D4 — BC20 is the cross-namespace anti-corruption boundary |
| 83 | + |
| 84 | +The `urn:agentbox:*` ↔ `urn:visionclaw:*` translation at the federation boundary is |
| 85 | +governed by the **BC20 anti-corruption layer**. The executable specification is |
| 86 | +agentbox `management-api/lib/bc20-provenance-bridge.js` (its `toVisionclaw` map); the |
| 87 | +VisionClaw-side counterpart is `src/uri/mod.rs::cross_from_agentbox`. The closed kind |
| 88 | +map is authoritative on both sides: |
| 89 | + |
| 90 | +| agentbox kind | VisionClaw target | Notes | |
| 91 | +|---|---|---| |
| 92 | +| `agent` | `did:nostr:<pubkey>` | identity; structural round-trip | |
| 93 | +| `activity` | `urn:visionclaw:execution:<sha256-12>` | unscoped; owner in `owner_did` | |
| 94 | +| `thing` | `urn:visionclaw:kg:<pubkey>:<sha256-12>` | owner-scoped, content-addressed | |
| 95 | +| `memory` | `urn:visionclaw:concept:<domain>:<slug>` | needs elevation `{domain,slug}`; absent on the hot path → recorded as crossing-without-translation (`None`) | |
| 96 | +| _other_ | — | unmapped → `None` (closed-map discipline) | |
| 97 | + |
| 98 | +`did:nostr:*` is already converged on both sides and passes through unchanged. The |
| 99 | +BC20 boundary is the **only** cross-namespace importer; nothing else translates |
| 100 | +between `urn:agentbox` and `urn:visionclaw`. |
| 101 | + |
| 102 | +### D5 — Bulk rewrite of stored IDs is a deferred Phase-2 (OUT OF SCOPE here) |
| 103 | + |
| 104 | +Rewriting the **already-persisted** `urn:ngm:node:*` / `urn:ngm:edge:*` / |
| 105 | +`urn:ngm:domain:*` identifiers in the Oxigraph store into the converged grammar is an |
| 106 | +explicit **Phase-2 data migration** and is **out of scope for this sprint**. It |
| 107 | +requires: a converged kind for node/edge persistence IRIs (the current minter has no |
| 108 | +`node`/`edge`/`domain` kind — `kg` is content-addressed by pubkey, not by the legacy |
| 109 | +`u32` node id), a stop-the-world or online migration of the stored quad set, a |
| 110 | +rewrite of every SPARQL `FILTER (STRSTARTS(STR(?s), "urn:ngm:edge:"))` and the |
| 111 | +`iri_to_node_id` round-trip parser in lockstep, and a re-derivation of node ids under |
| 112 | +the converged content-addressing scheme. None of that is attempted now. Dual-read |
| 113 | +(D2) is precisely what makes deferring it safe: nothing breaks while the legacy IDs |
| 114 | +remain in storage. |
| 115 | + |
| 116 | +## Phase-2 backlog (sites left on `urn:ngm` deliberately) |
| 117 | + |
| 118 | +These sites mint or round-trip `urn:ngm:*` and are **intentionally not converted** in |
| 119 | +this sprint because each is structurally bound to the ADR-100-protected named-graph |
| 120 | +storage and would desync stored quads from their FILTERs/parser if flipped in |
| 121 | +isolation: |
| 122 | + |
| 123 | +| Site | What it is | Why deferred | |
| 124 | +|---|---|---| |
| 125 | +| `src/adapters/oxigraph_graph_repository.rs::node_iri` (`urn:ngm:node:{id}`) | persistence node IRI mint | round-trips via `iri_to_node_id`; no converged `node` kind exists; tied to stored quads | |
| 126 | +| `src/adapters/oxigraph_graph_repository.rs::edge_iri` + `remove_edge` (`urn:ngm:edge:*`) | persistence edge IRI mint | matched by 3 SPARQL `STRSTARTS` FILTERs; converting desyncs stored triples | |
| 127 | +| `src/adapters/oxigraph_graph_repository.rs` SPARQL FILTERs (lines ~296/327/347) | `STRSTARTS(STR(?s), "urn:ngm:edge:")` | must move in lockstep with the edge-IRI mint | |
| 128 | +| `src/adapters/oxigraph_graph_repository.rs::iri_to_node_id` (`strip_prefix("urn:ngm:node:")`) | node-IRI → `u32` round-trip | the inverse of `node_iri`; Phase-2 with it | |
| 129 | +| `src/services/github_sync_service.rs::ensure_domain_roots` (`urn:ngm:domain:{slug}`) | ontology `owl_class_iri` mint | joined against ontology named-graph quads + `enrich_node_from_quads` ngm-prefix match; ADR-100 territory | |
| 130 | +| `src/adapters/oxigraph_graph_repository.rs` / `src/handlers/ontology_handler.rs` `urn:ngm:graph:*` | named graphs | **stays permanently** per ADR-100/D3 (not a Phase-2 item — a permanent exclusion) | |
| 131 | + |
| 132 | +## What was executed in this sprint (low-risk half) |
| 133 | + |
| 134 | +* **Added `parse_dual` + `ParsedUri::LegacyNgm`** to `src/uri/mod.rs` (D2): the |
| 135 | + resolve path now accepts both schemes; strict `parse` stays converged-only so the |
| 136 | + mint path is unchanged and the existing ngm-rejection test still passes. New tests |
| 137 | + cover converged round-trip, legacy round-trip, empty-sub rejection, and |
| 138 | + foreign-namespace rejection. |
| 139 | +* **Confirmed** the converged minter is already the mint path for the live |
| 140 | + provenance/ingest surfaces (`enrichment_proposals_handler`, `agent_events`). |
| 141 | +* **Left** all persistence-IRI mints on `urn:ngm` (the Phase-2 table above), because |
| 142 | + converting any one in isolation desyncs the ADR-100-protected stored quad set from |
| 143 | + its FILTERs and round-trip parser. |
| 144 | +* **Corrected** the stale ADR-063 `src/uri/{mint,parse,kinds}.rs` citation to the |
| 145 | + real `src/uri/mod.rs`. |
| 146 | + |
| 147 | +## Numbering |
| 148 | + |
| 149 | +This ADR also anchors the **one-number-one-decision** numbering convention recorded |
| 150 | +in `docs/README.md`. ADR-105 is the next free number after the on-disk max (ADR-104). |
| 151 | +The same sweep that ratified this convergence renumbered four colliding duplicate-pair |
| 152 | +files to ADR-106…ADR-109 (see `docs/README.md` → "Numbering convention"). |
| 153 | + |
| 154 | +## Consequences |
| 155 | + |
| 156 | +**Positive** |
| 157 | + |
| 158 | +* The convergence is ratified; `src/uri/mod.rs` is the named authoritative minter. |
| 159 | +* Persisted `urn:ngm` IDs keep resolving (dual-read) with zero data movement. |
| 160 | +* The BC20 boundary and its closed kind map are recorded as the single |
| 161 | + cross-namespace anti-corruption spec. |
| 162 | +* The migration is explicitly deferred and scoped, not silently dropped. |
| 163 | + |
| 164 | +**Negative / residual** |
| 165 | + |
| 166 | +* Two durable namespaces coexist until Phase-2; readers must call `parse_dual` |
| 167 | + (not the strict `parse`) when they may encounter persisted legacy IDs. |
| 168 | +* `urn:ngm:graph:*` named graphs remain `ngm`-prefixed permanently (by ADR-100 |
| 169 | + design, not by omission). |
| 170 | + |
| 171 | +**Risks** |
| 172 | + |
| 173 | +* A surface that calls strict `parse` where it should call `parse_dual` would fail to |
| 174 | + resolve a legacy ID. Mitigation: `parse_dual` is documented as the resolve-path |
| 175 | + entry point; the strict `parse` carries an explicit "rejects `urn:ngm:*`" contract |
| 176 | + and test. |
0 commit comments