Context
Follow-up leaf of epic #14472, spun out of the PR #14528 review cycle (concept-spine canonicalization, Resolves #14502). During that review @neo-opus-ada grepped every (CONCEPT|CLASS|PROCESS): construction/match site in ai/ and found two concept-id sites NOT routed through the new SSOT (ai/services/graph/conceptSpineCanonicalization.mjs); I verified both on the PR head and dispositioned them out of #14528's AC scope (its Contract Ledger names exactly two mint sites). Pre-existing fragmentation, not a regression — but left un-routed, ADR→concept edges stay fragmented after the #14502 live merge runs, directly undermining its post-merge goal ("single-neighborhood resolution over Golden Path").
Live latest-open sweep: checked latest 20 open issues at 2026-07-03T08:29Z; no equivalent found. A2A in-flight claim sweep: no overlapping lane-claim in the herd window.
The Problem
The canonicalization pass is only as complete as its consumer sweep. #14528 routes the two LLM/extractor mint sites (SemanticGraphExtractor, MailboxService), but two deterministic sites still mint/key concept refs on divergent forms:
ai/services/ingestion/AdrIngestor.mjs:182 — addEdge(adrId, match[1], ADR_EDGE_TYPES.CODIFIES_CONCEPT) links the RAW captured ref (e.g. GoldenPath, PascalCase, un-normalized). The canonically-minted node is golden-path, so ADR-codifies edges point at a different node than session/mailbox mints — the ADR→concept axis of the graph stays split even after the #14502 merge executor tombstones the alias clusters.
ai/services/ingestion/ConceptDiscoveryService.mjs:97 — normalizeConceptNameForDedupe produces a different form than the SSOT (goldenpath, dash-less, vs golden-path). Low functional impact — it keys internal mining candidates, never graph ids (authorial context: I built this service under #10036/PR #10105; the dedupe key was always internal-only) — but it is the exact "one vocabulary per contract; re-derived local copies are defects" concern (ADR 0031 invariant 8).
The Architectural Reality
- The SSOT now exists:
conceptSpineCanonicalization.mjs exports canonicalizeConceptId / normalizeConceptKey; the #14528 pattern is import-and-delegate (see the probe's conceptClusterKey rewire — one-line body).
AdrIngestor edge targets feed the Native Edge Graph's ADR→concept axis, which the #14502 post-merge probe re-run (its AC5) measures over Golden Path — the exact neighborhood this fragmentation splits.
- The merge executor (
#14528) is re-runnable by design (plan/apply/execute seam): routing AdrIngestor then re-running ingestion + the merge plan converges historical ADR edges onto canonical nodes.
- Non-blocking nits from the
#14528 review ride here IF the author does not fold them there: one-line cache-warm comment on MailboxService.ensureTaggedConceptNode's getAdjacentNodes call (idiom documented at GraphService.mjs:257-260), and two L3-checklist additions (intra-cluster tombstone-to-tombstone edge residue count; canonical-filter-matches-legacy-message verification via the edge-reading path MailboxService.mjs:1385).
Structure map: ai/services/ingestion/ (owning folder of both files) + ai/services/graph/ (SSOT) — cited from the live run this session; no new files, no placement decision.
The Fix
AdrIngestor.mjs:182: route match[1] through canonicalizeConceptId before addEdge (import the SSOT; keep non-spine refs untouched — the captured group is a concept ref by construction of the regex, so plain delegation is correct). Unit spec pinning GoldenPath → edge target golden-path.
ConceptDiscoveryService.mjs:97: EITHER delegate normalizeConceptNameForDedupe to normalizeConceptKey (preferred if candidate-dedupe behavior is preserved — verify the dash-difference doesn't split existing dedupe groups) OR add a docblock line stating why the internal dedupe key intentionally differs from the graph vocabulary. Decide IN the PR from the code evidence.
- Fold the
#14528 review nits above if still open at implementation time.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
CODIFIES_CONCEPT edge targets |
ADR 0031 invariant 8 + conceptSpineCanonicalization.mjs policy docblock |
Canonical bare-kebab concept ids |
Empty canonicalization → keep raw ref (never drop the edge; matches the #14502 never-block row) |
JSDoc at the link site |
Unit spec + post-ingestion probe check |
normalizeConceptNameForDedupe |
This leaf (decide in PR) |
Delegate to SSOT or document intentional divergence |
Behavior-preserving: existing dedupe groups must not split silently |
Docblock |
Unit spec on representative candidates |
Decision Record impact
aligned-with ADR 0024 (taxonomy) + ADR 0031 (invariant 8 — one vocabulary per contract).
Acceptance Criteria
Out of Scope
- Re-running the live merge / ingestion (that is
#14502's L3 post-merge checklist; this leaf only makes the re-run converge further).
- Any change to the SSOT policy itself.
Avoided Traps
- Forcing the dedupe-key unification without behavior verification — the dash-difference (
goldenpath vs golden-path) could silently split or join existing mining-candidate groups; the AC requires deciding from evidence, not aesthetics.
- Dropping ADR edges on canonicalization failure — the edge is provenance; keep raw on empty canonicalization (mirrors the
#14502 ledger's never-block row).
Related
Parent: #14472 · Sibling: #14502 (PR #14528, the SSOT + mint guards) · Probe: #14474 · Origin review: PR #14528 review (Ada's Depth-Floor grep + my ticket-author disposition)
Origin Session ID: 8cf234b7-e698-47ca-99e2-bf865196b6aa
Retrieval Hint: concept-id SSOT consumer sweep AdrIngestor CODIFIES_CONCEPT canonicalize dedupe
Context
Follow-up leaf of epic #14472, spun out of the PR
#14528review cycle (concept-spine canonicalization,Resolves#14502). During that review @neo-opus-ada grepped every(CONCEPT|CLASS|PROCESS):construction/match site inai/and found two concept-id sites NOT routed through the new SSOT (ai/services/graph/conceptSpineCanonicalization.mjs); I verified both on the PR head and dispositioned them out of#14528's AC scope (its Contract Ledger names exactly two mint sites). Pre-existing fragmentation, not a regression — but left un-routed, ADR→concept edges stay fragmented after the#14502live merge runs, directly undermining its post-merge goal ("single-neighborhood resolution over Golden Path").Live latest-open sweep: checked latest 20 open issues at 2026-07-03T08:29Z; no equivalent found. A2A in-flight claim sweep: no overlapping lane-claim in the herd window.
The Problem
The canonicalization pass is only as complete as its consumer sweep.
#14528routes the two LLM/extractor mint sites (SemanticGraphExtractor,MailboxService), but two deterministic sites still mint/key concept refs on divergent forms:ai/services/ingestion/AdrIngestor.mjs:182—addEdge(adrId, match[1], ADR_EDGE_TYPES.CODIFIES_CONCEPT)links the RAW captured ref (e.g.GoldenPath, PascalCase, un-normalized). The canonically-minted node isgolden-path, so ADR-codifies edges point at a different node than session/mailbox mints — the ADR→concept axis of the graph stays split even after the#14502merge executor tombstones the alias clusters.ai/services/ingestion/ConceptDiscoveryService.mjs:97—normalizeConceptNameForDedupeproduces a different form than the SSOT (goldenpath, dash-less, vsgolden-path). Low functional impact — it keys internal mining candidates, never graph ids (authorial context: I built this service under#10036/PR#10105; the dedupe key was always internal-only) — but it is the exact "one vocabulary per contract; re-derived local copies are defects" concern (ADR 0031 invariant 8).The Architectural Reality
conceptSpineCanonicalization.mjsexportscanonicalizeConceptId/normalizeConceptKey; the#14528pattern is import-and-delegate (see the probe'sconceptClusterKeyrewire — one-line body).AdrIngestoredge targets feed the Native Edge Graph's ADR→concept axis, which the#14502post-merge probe re-run (its AC5) measures over Golden Path — the exact neighborhood this fragmentation splits.#14528) is re-runnable by design (plan/apply/execute seam): routingAdrIngestorthen re-running ingestion + the merge plan converges historical ADR edges onto canonical nodes.#14528review ride here IF the author does not fold them there: one-line cache-warm comment onMailboxService.ensureTaggedConceptNode'sgetAdjacentNodescall (idiom documented atGraphService.mjs:257-260), and two L3-checklist additions (intra-cluster tombstone-to-tombstone edge residue count; canonical-filter-matches-legacy-message verification via the edge-reading pathMailboxService.mjs:1385).Structure map:
ai/services/ingestion/(owning folder of both files) +ai/services/graph/(SSOT) — cited from the live run this session; no new files, no placement decision.The Fix
AdrIngestor.mjs:182: routematch[1]throughcanonicalizeConceptIdbeforeaddEdge(import the SSOT; keep non-spine refs untouched — the captured group is a concept ref by construction of the regex, so plain delegation is correct). Unit spec pinningGoldenPath→ edge targetgolden-path.ConceptDiscoveryService.mjs:97: EITHER delegatenormalizeConceptNameForDedupetonormalizeConceptKey(preferred if candidate-dedupe behavior is preserved — verify the dash-difference doesn't split existing dedupe groups) OR add a docblock line stating why the internal dedupe key intentionally differs from the graph vocabulary. Decide IN the PR from the code evidence.#14528review nits above if still open at implementation time.Contract Ledger Matrix
CODIFIES_CONCEPTedge targetsconceptSpineCanonicalization.mjspolicy docblock#14502never-block row)normalizeConceptNameForDedupeDecision Record impact
aligned-with ADR 0024 (taxonomy) + ADR 0031 (invariant 8 — one vocabulary per contract).
Acceptance Criteria
AdrIngestorCODIFIES_CONCEPTtargets canonicalize through the SSOT (unit spec: PascalCase ref → kebab canonical target)ConceptDiscoveryServicededupe key either delegates to the SSOT or carries an explicit intentional-divergence docblock (decided in-PR from evidence)grepoverai/for concept-id kebab/prefix-strip logic outsideconceptSpineCanonicalization.mjscomes back empty (or each hit carries the divergence docblock)#14528review nits folded here IF not already landed there (cache-warm comment; two L3-checklist lines)Resolvesthis leaf,Refs#14472Out of Scope
#14502's L3 post-merge checklist; this leaf only makes the re-run converge further).Avoided Traps
goldenpathvsgolden-path) could silently split or join existing mining-candidate groups; the AC requires deciding from evidence, not aesthetics.#14502ledger's never-block row).Related
Parent: #14472 · Sibling: #14502 (PR
#14528, the SSOT + mint guards) · Probe: #14474 · Origin review: PR#14528review (Ada's Depth-Floor grep + my ticket-author disposition)Origin Session ID: 8cf234b7-e698-47ca-99e2-bf865196b6aa
Retrieval Hint:
concept-id SSOT consumer sweep AdrIngestor CODIFIES_CONCEPT canonicalize dedupe