GENERATED VIEW · PRIVATE ARCHIVE · DO NOT SERVE
RobCo Industries · Archive Museum

KNOWLEDGE_GRAPH_PLAN.md



RELEASE

planning/2.8.5/plans/KNOWLEDGE_GRAPH_PLAN.md

sha256 9458c8e5dc2a6378 · 39466 bytes · original held in the private archive

# Knowledge-Graph / Retrieval-Topology — IMPLEMENTATION PLAN (R11, minimum version) _Planning pass. Written 2026-07-21 against `dev` @ `e92f8e8`. This is STEP 2 of the locked execution sequence (R10 doc-fixes → **R11 graph** → A3 → ship 2.8.5). **No implementation code is written in this pass** — this document is the design of record for the build that follows, and the owner's steering point before any parser exists._ **Companion documents:** the adopted spec is [`KNOWLEDGE_GRAPH_SPEC.md`](KNOWLEDGE_GRAPH_SPEC.md) (design of record — GPT-5.6 Sol, Dispatch-amended); the queue home is **R11** in [`QUEUE.md`](../../../QUEUE.md). This plan does **not** modify the spec; where the real files differ from the spec's assumptions, the correction is recorded here (§2b and §9) and must be carried into the code, not back into the spec. **Scope of THIS plan — the MINIMUM version only.** The retrieval topology and its diagnostics. The DATA LAYER — the extractor and its JSON output. **NOT** in scope and deliberately deferred: protocol nodes, suite/guard nodes, queue-item nodes, museum/atlas nodes, the `claims_checked_by` / `invokes` edges, the Fable **topology view**, and the session-facing **query answerer**. The last two are named downstream consumers (§10); they are designed elsewhere, after the extractor exists. --- ## 0. The one-paragraph shape A single Node script in the app repo reads six kinds of source text — CLAUDE.md's retrieval-map table, CLAUDE.md's pointer index, the ten `rules/*.md` note headers, `library/MANIFEST.txt`, and the tracked file tree — and emits **one JSON artifact**: a set of typed **nodes** (with an observation state) and typed **edges**, plus a **diagnostics** block and a per-extractor **status** block. The two retrieval edges (`routes_to`, `claims_scope_over`) are derived **independently** from two different source texts so their disagreement falls out as data rather than being hunted for. Every extractor reports how many records it saw, emitted, and failed to parse, so a parse that silently returns nothing announces itself as `empty_parse` instead of rendering a collapsed project. The script never emits an empty-but-healthy graph: a missing source degrades the graph loudly. --- ## 1. The node set The minimum node set, its `key` scheme, and its `observation` state. Keys are **baseline-local** (scoped to the current checkout) for everything here — none of these nodes are protocols or queue IDs, so none carry the cross-release identity contract (§ spec "baseline-local keys… cross-release identity ONLY for protocol and queue IDs"). Cross-release identity is reserved for the deferred protocol/queue node types. **Observation states** (from the spec): `observed` = readable on this checkout; `declared` = named by a source but no file is expected on disk to confirm it (a generic external node); `manifested` = named **and** listed in `library/MANIFEST.txt` but gitignored, so legitimately absent on a clean checkout; `unavailable` = named but neither present nor manifested (a real dangling target). | # | Node | `key` | `observation` (on this `dev` checkout) | Notes | |---|------|-------|----------------------------------------|-------| | 1 | The skill | `skill:SKILL.md` | `observed` (repo copy `skill/SKILL.md` is tracked) | The **installed** skill is control-plane state (R10 finding L), NOT this node — the node is the repo file, which is what is observable. Divergence between repo copy and installed copy is out of graph scope. | | 2 | The universal contract | `doc:CLAUDE.md` | `observed` | The routing hub. Source of `routes_to` and `references`. | | 3–12 | The ten rule notes | `rule:<basename>` e.g. `rule:state-and-save` | `observed` (all ten present, verified) | One node each: state-and-save, deploy-and-cache, auth-and-cloud, ui-and-mobile, audio, game-data, ai-contract, file-layout, testing-and-gates, docs-and-library. Source of `claims_scope_over`. | | 13 | Architecture deep-dive | `doc:ARCHITECTURE.md` | `observed` | Leaf node in the minimum version. Its **internal sections are NOT parsed** (that is R10 finding A / Architecture-by-section, explicitly deferred). Inbound `references` only. | | 14 | The queue | `doc:QUEUE.md` | `observed` | | | 15 | The queue log | `doc:QUEUE_LOG.md` | `observed` | Append-only companion. Governed by the docs note's contract but — verified — **absent from the docs map row and the docs note header** (see §2b, §3). | | 16 | The library group | `group:library` | `observed` (the group node itself always exists) | A parent node for the manifested stubs. Source/target of `manifests` + `contains`. | | 17–22 | The manifested library stubs | `lib:<relpath>` e.g. `lib:CODE_MAP.md` | **`manifested`** on a clean checkout; `observed` if the local file is present | The six lines of `library/MANIFEST.txt`: `BRAIN_DUMP.md`, `CODE_MAP.md`, `PROMPT_LIBRARY/RobCo_Engineering_Playbook.md`, `PROMPT_LIBRARY/RobCo_Prompt_Library.md`, `PROMPT_MULTI_AI_REMAKE.md`, `TEST_CATALOG.md`. The state is decided **per run** by testing disk presence — this is the whole point of the `manifested` vs `observed` distinction. | | 23 | The manifest | `doc:library/MANIFEST.txt` | `observed` (committed exception) | Source of `manifests`. | | 24 | Private archive | `ext:private-archive` | `declared` | Generic placeholder node (`_RobCo-Archive`) — never ingests private content. Named by CLAUDE.md/Protocol 48 prose. | | 25 | Orchestrator memory | `ext:memory` | `declared` | Generic placeholder — the `memory/` store, outside both repos. | | 26 | The museum | `ext:museum` | `declared` | Generic placeholder — release-pinned history, out of the minimum retrieval chain but named. | | 27 | The Atlas | `ext:atlas` | `declared` | Generic placeholder — shares the schema later (item I), link-don't-fuse. | | 28+ | Literal scope-selector nodes | `selector:<slug>` | `declared` (they name no file) | The prose scope clauses that are **not paths**: "any change a user can see", "any new sound", "any push that reaches a live site", "any Fallout fact entering the app", etc. Modeled as first-class nodes so a `claims_scope_over` edge to a non-path scope is recorded as an edge to a selector, **not** discarded and **not** mistaken for a dangling file path. | **Why the selector nodes matter (grounding, not theory).** Every note header and every map row mixes concrete backticked paths with prose scope clauses. If the extractor only kept backticked tokens it would silently drop half of what the notes actually claim, and if it treated the prose as a file path it would emit false dangling-reference diagnostics. Making the prose clauses explicit `selector` nodes is how the graph stays honest about scope it cannot resolve to a file. **Confirm against the spec:** the spec's enumerated minimum node set is "the skill, `CLAUDE.md`, the ten rule notes, `ARCHITECTURE.md`, `QUEUE.md`, `QUEUE_LOG.md`, the library group + manifested stubs, generic private archive/memory/museum/atlas nodes, and literal scope-selector nodes." **All present above.** The one addition this plan makes explicit is node #23 (`MANIFEST.txt` itself as the source of `manifests`) and node #16 (the library **group** as distinct from its stubs) — both are structural necessities implied by the spec's `manifests`/`contains` edges, not scope creep. --- ## 2a. The edge set — types and, for each, the EXACT extraction rule Five edge types. For each: the source file, the exact syntax parsed, and where the parse is fragile. Everything below is grounded in the **real files as they stand at `e92f8e8`**, not the spec's description of them. ### `routes_to` — CLAUDE.md retrieval map → note - **Source:** `CLAUDE.md`, the "Retrieval map" table, **lines 27–38** (verified). - **Syntax:** a GFM pipe table. Header row `| If you are touching… | Also read |`, separator row of dashes, then one row per note. Left cell = a `·`-separated (U+00B7 MIDDLE DOT) list of scope segments; right cell = a single backticked note path, e.g. `` `rules/state-and-save.md` ``. - **Extraction rule:** locate the table by its two column headers (`If you are touching` / `Also read`) — **not** by the heading text above it, which contains a non-ASCII ellipsis `…` (U+2026) that is a fragile anchor. For each body row: the right cell yields the **target note node**; split the left cell on U+00B7; for each segment extract every backticked token and classify it (§2c). Each path-classified token → a `routes_to` edge `doc:CLAUDE.md —routes_to→ rule:<note>` **carrying the routed path as an edge property**. Prose segments with no backtick → a `routes_to` edge to the note via a `selector` node. - **Fragility:** (1) the `·` separator is non-ASCII — the reader must be UTF-8 and Protocol 39 applies to the script and its output. (2) A left cell mixes `` `backtick` ``, `**bold**`, and plain prose in one segment (e.g. `` `js/` (any **served/precached** file) ``) — the classifier must pull the backtick token and ignore the bold/paren prose without treating them as separate paths. (3) The map expresses directory scope as bare `` `css/` `` / `` `js/` `` (trailing slash) — a **prefix**, not a file; must be classified as a directory selector, not resolved as a file. ### `claims_scope_over` — note header → path/selector (DERIVED INDEPENDENTLY) - **Source:** each `rules/*.md` file's opening blockquote header (verified across all ten; format is uniform). - **Syntax:** line 1 is `# Subsystem note — <Title>`. Beginning at line 3, a blockquote: `> **Load this when touching:** <segments>` where `<segments>` is the same `·`-separated backtick+prose format, and **may span several `>` lines**. The scope declaration **ends** at the sentinel line `> Universal rules live in \`CLAUDE.md\`…` (present verbatim in all ten notes). - **Extraction rule:** read the blockquote block; capture from after `**Load this when touching:**` up to (not including) the `Universal rules live in` sentinel; strip leading `> `; join continuation lines; split on U+00B7; classify each segment's backticked tokens (§2c). Each path token → a `claims_scope_over` edge `rule:<note> —claims_scope_over→ <path>`; each prose segment → an edge to a `selector` node. - **Fragility (this is the hardest parse — see §8):** (1) the block is **multi-line and variable-length** — anchoring on the sentinel is essential; anchoring on a fixed line count would break. (2) A single `·` segment can hold **zero** backticks (`any new sound`), **one**, or **many** — `game-data.md` has `` anything under `js/data/` (`db_nv.js`, `db_fo3.js`, `reg_nv.js`, `reg_fo3.js`, `registry-core.js`) `` = **five** backticked tokens in one segment, four of them inside parentheses. The extractor must emit an edge for **every** backticked path token in a segment, not one-per-segment. (3) Two backticked tokens can appear in one segment as "the X in Y" — audio.md's `` the `AudioSettings` cache in `ui-core.js` `` holds a **symbol** and a **file** in one segment; both must be extracted and classified differently. > **The independence is the design.** `routes_to` reads **only** CLAUDE.md; `claims_scope_over` reads > **only** the note headers. Neither extractor is allowed to consult the other's output. The gap > diagnostics (§3) are computed by a **third** step that diffs the two edge sets. This is load-bearing > idea (a) and the entire reason the graph can find what a single checker would miss. ### `references` — CLAUDE.md pointer index (+ closed-world cross-refs) → artifact - **Source:** `CLAUDE.md`, the "Reference Pointer Index" table (**lines 49–72**, verified) and the larger reference table (header at line 227) — both `| … | … |` GFM tables whose right cell holds backticked target paths, `§ Section` anchors, and prose. - **Extraction rule:** for each row, extract every backticked **path-classified** token from the right cell (a cell can hold several — line 55 references `api-directive.js`, `api-import.js`, `api.js`, `rules/ai-contract.md`, and `ARCHITECTURE.md` in one cell) → a `references` edge `doc:CLAUDE.md —references→ <node>`. **Closed-world restriction:** a `references` edge is emitted **only** when the target resolves to an already-declared node key (§1). An arbitrary backticked identifier in prose does **not** manufacture a node or a dangling edge — this is the guard against format drift inventing false references. - **Fragility:** right cells are dense (multiple paths + `§` anchors + parentheticals). `§ Section` anchors are **not** file paths and must be dropped (the minimum graph does not model ARCHITECTURE/CODE_MAP sections). Section anchors are R10 finding A territory — deferred. ### `manifests` — MANIFEST.txt → library stub - **Source:** `library/MANIFEST.txt` (verified — a committed exception file). - **Syntax:** plain text; lines beginning `#` are comments; every other non-blank line is one library-relative path (forward slashes, may be nested). Six data lines. - **Extraction rule:** for each non-comment non-blank line → node `lib:<relpath>` (obs state decided by disk presence, §1) and edge `group:library —manifests→ lib:<relpath>`. **This is the one clean, low-fragility parse** — no markup, no separators, no multi-line blocks. - **Fragility:** minimal. The only trap is treating a `#` comment or blank line as a path — trivially guarded. Nested paths (`PROMPT_LIBRARY/…`) are values, not a reason to recurse. ### `contains` — structural parent → child - **Source:** derived, not parsed from prose. `group:library —contains→ lib:<stub>` for each manifested stub (mirrors `manifests` but as a pure structural containment edge, so the topology view can draw the library as a cluster). Optionally `doc:QUEUE.md —contains→ doc:QUEUE_LOG.md` is **NOT** modeled as `contains` — QUEUE_LOG is a separate node reached by `references`, not containment. - **Extraction rule:** emitted directly from the node set; no text parse. Kept deliberately minimal in the minimum version — `contains` exists so the renderer has a grouping edge, nothing more. If it earns no use, it is a candidate to drop (stopping rule). **The three load-bearing ideas — where each lives in this edge set:** - **(a) independent derivation** → `routes_to` (CLAUDE.md only) vs `claims_scope_over` (note headers only), diffed by a third step. **Preserved.** - **(b) `claims_checked_by` vs `invokes` separate** → these edges belong to the **deferred** protocol/ suite layer, **not** this minimum set. Recorded here as explicitly out of scope so a later session does not fold them into `references` (the spec's warning that "naming is not running"). - **(c) records seen/emitted/unparsed/status per extractor** → §4, the status block. **Every** extractor above emits it. ## 2b. Where the REAL files already disagree with each other (the graph's built-in acceptance test) This is the most valuable finding of the grounding pass. Running the two independent derivations against the files **as they stand** must reproduce these **already-existing** disagreements. They are the implementing session's red-line acceptance anchor: **if the first run reports zero gaps, the parser is lying** (load-bearing idea c, operationalized). Every one below was verified by reading the real bytes, and each corresponds to a known R10 finding D item. | Disagreement (verified at `e92f8e8`) | Kind | R10-D? | |---|---|---| | `scripts/cf-staging-build.mjs` — **claimed** by the deploy note header, **not routed** by any map row (the broad `scripts/` scope routes to the testing note). | `claimed_but_not_routed` | ✔ | | `firebase.json` — **claimed** by the auth note header, **not routed** by the auth map row. | `claimed_but_not_routed` | ✔ | | `QUEUE.md` — **routed** by the docs map row, but the docs note header does **not** list it. | `routed_but_not_claimed` | (adjacent to ✔) | | `QUEUE_LOG.md` — governed by the docs note's append-only contract, but **neither routed nor claimed** by the docs note; reachable only via a `references` edge from the pointer index (line 52). | `unrouted_graph_bearing_artifact` (but NOT a false orphan — it has an inbound `references` edge) | ✔ | | `.github/workflows/` — routed to the deploy note **and** claimed by the testing note header. | **legitimate multi-claim, NOT a defect** (§3) | ✔ (D notes both govern it) | | `skill/SKILL.md` — routes nowhere and is claimed by nothing. | `unrouted_graph_bearing_artifact` | ✔ | **These six are the golden fixtures.** The build sequence (§6) requires the diff step to reproduce them before any further edge is added. This is also a Protocol 42 opportunity: the disagreements are R10 finding D's actual content — R11 is the **detector**, R10 step 2 remains the **fixer** (do not double-own; §7 open question 4). ## 2c. The backtick-token classifier (shared by every path-bearing extractor) One function, used by `routes_to`, `claims_scope_over`, and `references`. Given a backticked token, classify it: - **file path** — contains `/` and a file extension, or is a bare `name.ext` (e.g. `api.js`, `db_nv.js`, `js/services/api-import.js`, `firestore.rules`, `repro.config.json`). → resolvable against the tracked tree. - **directory prefix** — ends in `/` (e.g. `css/`, `js/`, `js/ui/`, `js/data/`, `tests/`, `scripts/`, `rules/`). → resolved as "does at least one tracked file live under this prefix", never as a file. - **symbol** — ends in `()` or is a known code identifier (`getSystemDirective()`, `autoImportState()`, `migrateState()`, `GAME_DEFS`, `AudioSettings`, `RobcoEvents`, `state`). → **never** resolved against the file tree; recorded as a symbol scope, no dangling diagnostic. - **prose / unclassifiable** → treated as a `selector` node (non-path scope), no dangling diagnostic. **This classifier is the single most important defense against false diagnostics.** A symbol resolved as a path, or a directory resolved as a file, produces a fake "dangling reference" that trains the owner to ignore the map — the exact failure the spec warns against. The classifier's rules must be unit-tested directly (§6 step 1) with the real tokens harvested from the actual files as fixtures. --- ## 3. The diagnostics the minimum graph emits For each: the mechanical definition, and — critically — **whether it is genuinely WRONG or legitimately FINE**. The spec's warning is explicit: an orphan that is actually correct trains the owner to ignore the map, so the FINE cases must be **rendered in a separate, non-alarming class**, never mixed with real defects. | Diagnostic | Mechanical definition | Verdict | |---|---|---| | **`claimed_but_not_routed`** | A path `X` has a `claims_scope_over` edge from note `N`, but no `routes_to` edge routes `X` to `N`. | **WRONG** — a real routing gap. (firebase.json, cf-staging-build.mjs.) | | **`routed_but_not_claimed`** | A path `X` is routed by the map to note `N`, but `N`'s header has no `claims_scope_over` edge for `X`. | **WRONG** — a real scope gap. (QUEUE.md → docs.) | | **`unrouted_graph_bearing_artifact`** | A graph-bearing node has **no inbound edge of _any_ type** (`routes_to`, `references`, `claims_scope_over`, `manifests`, `contains`). | **WRONG** — genuinely unreachable. (skill/SKILL.md.) **Key subtlety:** a node reached by `references` but not `routes_to` (QUEUE_LOG.md, CHANGELOG.md, ARCHITECTURE.md) is **NOT** flagged — those are legitimately reference-reached, and flagging them is the false orphan the spec warns about. The diagnostic keys on "no inbound edge **of any kind**", not "no route". | | **`dangling_explicit_reference`** | A path-classified backtick token resolves to no tracked file. | **WRONG _unless_** the target is a `manifested` library stub absent because `library/` is gitignored → then it is `manifested`, **FINE**. So the resolver must consult MANIFEST before flagging. | | **`manifest_only` / `unavailable` node** | A node whose obs state is `manifested` (gitignored, absent) or `unavailable`. | **`manifested` = FINE** (expected on a clean checkout — informational, its own class). **`unavailable` = WRONG** (named, not present, not manifested — a real dangling target). | | **`parser_failure`** | Any extractor's `parser_status != ok` (see §4). | **ALWAYS SURFACED LOUDLY** — blocks the "healthy" verdict and forces a non-zero exit. | | **`multi_claim` (informational)** | A path with `claims_scope_over` from **two or more** notes (`.github/workflows/`). | **FINE** — surfaced as information, explicitly **not** a defect. A path can legitimately be governed by several surfaces. Modeling this as fine (not a conflict) is a §9 failure-mode guard. | **The FINE/WRONG split is a rendering contract, not just a label.** The diagnostics block must group output into three bands: **DEFECTS** (the WRONG rows), **EXPECTED** (`manifested`, `multi_claim` — the FINE rows), and **PARSER STATUS** (§4). A session or the owner reading the report must be able to see at a glance that the six golden disagreements are defects while eight absent library stubs are expected. --- ## 4. The per-extractor status block (load-bearing idea c, mechanized) Every extractor returns, and the graph JSON carries, for each source: ``` { "source": "CLAUDE.md#retrieval-map", "records_seen": 10, // rows/segments the parser located "records_emitted": 34, // edges/nodes actually produced "records_unparsed": 0, // located but un-extractable (logged with the raw text) "parser_status": "ok" } // ok | empty_parse | source_missing | degraded ``` **The four statuses and what each means:** - **`ok`** — source present, located its structure, emitted a non-zero record count consistent with `records_seen`. - **`empty_parse`** — **source present and non-empty, but the parser matched its structure zero times.** This is the renamed-heading / changed-format case. It must be **impossible** for this to render as a healthy empty graph — it is a `parser_failure` diagnostic and forces non-zero exit. (E.g. the retrieval map header is reworded and `routes_to` finds no table → `empty_parse`, NOT "10 orphaned notes".) - **`source_missing`** — the file does not exist / is unreadable. Distinct from `empty_parse`. Also a `parser_failure`; all nodes/edges that extractor would have produced are marked `unavailable`. - **`degraded`** — a partial parse: some rows extracted, some `records_unparsed > 0`. Surfaced, does not necessarily fail exit (owner decision, §7), but always printed. **The whole-graph invariant:** the graph carries a top-level `graph_status`. It is `healthy` **only if every** extractor is `ok`. If any is `empty_parse` / `source_missing`, `graph_status = broken` and the script exits non-zero with the parser-status band printed first. **There is no code path that emits a green, empty, healthy graph.** --- ## 5. Where it lives, how it runs, how it degrades **Recommendation (owner confirms — §7 Q1):** - **A script in the app repo:** `scripts/knowledge-graph.js`, alongside the existing derived-check family (`scripts/cache-bump-guard.js`, `scripts/queue-drift-check.js`, `scripts/backup-nudge.js`). Pure Node, no new dependencies (matches the project's no-new-deps instinct; the parsers are hand-rolled string/line work, not a markdown-AST library — see §8 on why hand-rolled is safer here). - **Generated on demand, NOT committed.** `node scripts/knowledge-graph.js` prints the diagnostics bands to stdout and writes the full JSON to a **gitignored** path (`library/knowledge-graph.json`, added to `library/MANIFEST.txt` so Suite 220 still accounts for it, OR a dedicated gitignored `build/` path — §7 Q1). **Rationale:** the output is GENERATED-class (derived from source). Committing it re-introduces the exact gitignored-`library/`-can't-gate-diff-on-clean-CI tension that item D must still resolve; generating on demand sidesteps it entirely and guarantees the artifact is never stale. The consumers (§10) run in a session that has the repo checked out, so on-demand is sufficient for them. Revisit committed-output when item D settles the GENERATED-class gate-diff question for the whole class (Protocol 22 — don't invent that machinery twice). - **No gate wiring in the minimum version.** The script is stand-alone and run manually. This honors the spec's **stopping rule** ("extend only if the first map is actually used to catch drift; if it becomes wallpaper, stop") and is a deliberate departure from the project's usual "every guard earns a suite" reflex — flagged as §7 Q6 because it is a conscious exception. A Suite guard and pre-push nudge come **later**, only once the map has caught real drift. **How it degrades when a source file is missing (the spec's "silent empty parses" section, honored):** - A missing source → that extractor returns `source_missing`, its nodes/edges become `unavailable`, and `graph_status = broken` with a non-zero exit. **It does NOT emit an empty healthy graph.** - A present-but-unmatched source → `empty_parse`, same broken status. This is the distinction that makes a renamed heading loud instead of silent. - A clean checkout with `library/` gitignored → the six stubs are `manifested`, `graph_status` stays `healthy` (their absence is expected, not a failure). This is the one absence that is fine, and the MANIFEST is what lets the graph know the difference. --- ## 6. The build sequence (incrementally verifiable) Each step is provable before the next begins. No step trusts a later one. 1. **The classifier + node inventory + MANIFEST parse.** Build §2c's backtick classifier and unit-test it against real tokens harvested from the actual files. Build the node registry (§1) and the `manifests` edge from `library/MANIFEST.txt`. **Prove:** all 28+ nodes enumerate; the six stubs are `manifested` on a clean checkout and `observed` when present; the classifier correctly sorts `api.js` (file), `css/` (dir), `getSystemDirective()` (symbol), and `any new sound` (prose). This step establishes the closed-world node key set everything else resolves against. **Lowest fragility — do it first.** 2. **The `routes_to` extractor, alone.** Parse the CLAUDE.md retrieval-map table with full status reporting. **Prove:** exactly 10 target notes; the routed paths per note match the real left-cell contents; `records_seen == 10`; and — the red-then-green proof — feeding it a **renamed table header** yields `empty_parse`, NOT an empty edge set (Protocol 42 self-test). 3. **The `claims_scope_over` extractor, alone and independent.** Parse the ten note headers (the hardest parse — §8), with status reporting. **Prove:** every backticked path in each header becomes an edge, including all five tokens in game-data's parenthesized segment; prose segments become selector edges; the sentinel-terminated multi-line block is captured in full. 4. **The diff step → the gap diagnostics.** Diff `routes_to` against `claims_scope_over`. **Prove:** it reproduces **all six golden disagreements** in §2b. **This is the acceptance gate for the whole idea** — zero gaps means the parser is lying, and the step fails. 5. **The `references` extractor + dangling-reference diagnostic.** Parse the pointer index (closed-world). **Prove:** QUEUE_LOG.md gets its inbound `references` edge (so it is NOT a false orphan); a fabricated stale backtick path is flagged `dangling_explicit_reference`; a `manifested` stub is NOT. 6. **Graph assembly + JSON emit + diagnostics bands + degradation/exit-status.** Wire `contains`, assemble the full node/edge/diagnostics/status document, implement the three-band report and the `graph_status` invariant. **Prove:** deleting a source file yields `broken` + non-zero exit, never an empty healthy graph; the three bands render the six defects separately from the eight expected absences. --- ## 7. Open questions for the owner 1. **Output location & committed-vs-generated.** Recommendation: **generated on demand, gitignored, uncommitted** (§5). The alternative — commit the JSON so a session on a clean checkout can read it without running Node — buys cross-machine readability but re-opens the GENERATED-class gate-diff tension (item D). Which do you want for the minimum version? (My rec: on-demand now, revisit with D.) 2. **Gitignored output path.** If gitignored: `library/knowledge-graph.json` (then add one line to `library/MANIFEST.txt`, so Suite 220 accounts for it) **or** a dedicated `build/` scratch path (no MANIFEST touch, but a new ignored dir)? (My rec: `library/…` + MANIFEST line — reuses existing machinery.) 3. **Scope of the `references` edge.** Minimum = CLAUDE.md pointer index + big reference table, closed-world (only edges between already-declared nodes). Do you also want inline note-to-note backtick references (e.g. a note mentioning another note)? (My rec: **no** for v1 — closed-world pointer-index only; it is bounded and non-fragile. Add cross-refs only if the topology view needs them.) 4. **Detector vs fixer.** R11 is the **detector**; R10 step 2 already owns **fixing** finding D's gaps. My rec: R11 surfaces the six disagreements and does **not** edit the map — the fix stays R10 step 2 (don't double-own). Confirm, or do you want R11 to also emit suggested corrected map rows? (My rec: surface only.) 5. **Gate wiring.** Minimum version is stand-alone, **no** Suite guard, **no** pre-push nudge — per the spec's stopping rule (build it, see if it catches real drift, only then wire it in). This is a conscious exception to "every guard earns a suite". Agree to keep it un-gated until it proves useful? 6. **The skill node.** The node is the **repo copy** `skill/SKILL.md` (observable); the **installed** skill is control-plane state (finding L) and out of graph scope. Confirm you don't want the graph to try to model installed-vs-repo divergence (it can't observe the installed copy anyway). --- ## 8. The hardest parse, and why **`claims_scope_over` from the note headers (build step 3) is the hardest, and it is where a parser is most likely to lie.** Three compounding reasons, all verified against the real files: 1. **Variable-length multi-line blocks.** The scope declaration is a blockquote that runs across a different number of `>` lines in different notes, terminated only by the `Universal rules live in CLAUDE.md` sentence. Any parser that assumes a fixed shape (one line, N lines) breaks on the next note. The sentinel is the only reliable terminator — and if the sentinel wording ever changes, the parser must fail **loud** (`empty_parse`/`degraded`), not silently truncate. 2. **N backticks per segment, including inside parentheses.** `game-data.md` packs five backticked filenames into one `·` segment, four in a parenthetical. A one-token-per-segment assumption silently drops four real scope claims — a **22%-style silent undercount**, which is precisely the failure class this project already measured and the spec explicitly cites (the git-rename undercount). The extractor must emit an edge per backtick token and its status count must reflect that (segments seen vs tokens emitted will differ, and that difference is expected, not an error). 3. **Mixed token kinds in one segment.** `` the `AudioSettings` cache in `ui-core.js` `` holds a symbol and a file; `` `js/` (any **served/precached** file) `` holds a directory prefix and bold prose. The classifier (§2c) has to be right per-token, or a symbol becomes a fake dangling path. **Why hand-rolled string parsing, not a markdown-AST library.** A general markdown parser would tokenize these into inline nodes but still leave the domain classification (path vs dir vs symbol vs prose, sentinel detection, `·` segmentation) entirely to us — and it adds a dependency against the project's no-new-deps instinct. The fragile part is **domain semantics, not markdown structure**, so a focused hand-rolled reader with explicit status reporting is both lighter and easier to make fail-loud. This mirrors how the existing `scripts/*.js` guards are written. --- ## 9. Failure modes — how the derivation can lie, and the guard for each The spec's most important section. For every way the minimum graph could lie, the plan's defense: | How it could lie | Guard in this plan | |---|---| | **Silent empty parse** (renamed heading → zero routes → "10 orphaned notes"). | Per-extractor `parser_status`; `empty_parse` is distinct from `ok` and forces `graph_status = broken` + non-zero exit (§4). Plus the §2b golden-fixtures acceptance test: a run that finds zero known gaps fails. | | **Silent undercount** (one-token-per-segment drops game-data's 4 paren'd filenames — the 22% class). | Emit an edge per backtick token, not per segment; `records_seen` (segments) and `records_emitted` (tokens) are reported separately so a collapse of emitted-toward-seen is visible (§8.2). | | **Prose mistaken for a file path** → fake dangling diagnostic that erodes trust. | The §2c classifier: prose → `selector` node, symbol → never resolved, dir prefix → resolved as prefix. No non-path token is ever resolved against the tree. | | **False orphan** (a reference-reached node flagged as unreachable). | `unrouted_graph_bearing_artifact` keys on "no inbound edge **of any kind**", so QUEUE_LOG/CHANGELOG/ARCHITECTURE (reference-reached) are not flagged (§3). | | **Two legitimate claimants read as a conflict** (`.github/workflows/`). | `multi_claim` is modeled as an **informational, FINE** diagnostic in the EXPECTED band, never a defect (§3). | | **Manifested-but-absent read as missing** (gitignored `library/` stub on clean CI). | Node state `manifested` (distinct from `unavailable`); the dangling-reference resolver consults MANIFEST before flagging; `manifested` sits in the EXPECTED band (§1, §3). | | **Git-rename similarity undercount** (the recorded 22% failure). | The minimum graph is **single-snapshot** — it does **no** cross-commit diffing and **never** invokes git rename detection. A rename is simply the new tree state. Sidesteps the heuristic entirely (spec-aligned). | | **Public-projection leak** (private data ingested then redacted). | The private archive / memory / museum / atlas nodes are `declared` **generic placeholders** — the extractor reads only public repo files, never private stores, so there is nothing to redact. Fail-closed by construction (spec-aligned). Note: the full public projection belongs to the deferred public-view consumer; the data layer simply never reads private content. | | **The report itself trains the owner to ignore it** (defects buried among expected absences). | The three-band rendering contract (DEFECTS / EXPECTED / PARSER STATUS) — a hard requirement of §3, not cosmetic. | --- ## 10. The two downstream consumers (named, NOT designed here) Recorded so the extractor's JSON is shaped to serve them, per the owner's "one derivation, three renderings" ruling — but **their design is out of this plan's scope**: - **The Fable TOPOLOGY view** (human, visual) — reads the node/edge JSON and draws skill → contract → notes → architecture → library as a readable shape. A **Fable design job**, after the extractor exists. - **The session-facing QUERY answerer** (machine-readable, the owner's highest-value consumer) — answers "what governs this path?" with **both** the routed note **and** the claiming note **and** the fact that they disagree, and "is this guarded?" once the deferred `claims_checked_by`/`invokes` edges exist. It is a **consumer of** this JSON, built after it. The data layer must therefore keep, per edge, enough provenance (which source, which line/row) that a consumer can cite it — cheap to carry now, expensive to retrofit. --- ## 11. Corrections to the brief / spec (recorded, spec NOT edited) - **Spec assumption vs reality — the note headers are NOT clean path lists.** The spec describes each note's "load this when touching" header as declaring "what it covers"; the **real** headers interleave backticked paths, backticked **symbols**, directory prefixes, and free prose within a `·`-separated, multi-line, sentinel-terminated blockquote — with up to five tokens in a single segment. This is the single biggest gap between the spec's idealized description and the files, and it is why §2c (the classifier) and §8 (the hardest parse) carry the weight they do. **The spec is not wrong in intent — the derivation it wants is exactly right — but an implementer taking its "each note declares its scope" line literally would build a parser that silently drops half the claims.** Recorded here per the brief; the spec is left unedited (design of record). - **The retrieval-map row and the note header for the same surface already disagree in the shipped files** (§2b) — this is not hypothetical; it is R10 finding D, and the graph's first run must reproduce it. That the two sources already diverge is the strongest possible evidence that deriving them independently is the right call. - **`·` is U+00B7 and `…` is U+2026 — the source is non-ASCII by design** (Protocol 39). The parser, and its output file, must be UTF-8 clean, and the table anchor must be the column-header text, not the ellipsis-bearing section heading. - **No correction to the brief's scope** — the MINIMUM-only, data-layer-only framing is correct and honored throughout. The brief's node-set enumeration matches the spec and the files; this plan only made the library **group** node and the `MANIFEST.txt` **source** node explicit (structural necessities). --- ## 12. Definition of done for the BUILD that follows this plan (Protocol 26) Not this planning pass — the implementation session. Recorded now so the acceptance criteria are fixed before code: 1. `node scripts/knowledge-graph.js` emits a JSON document with typed nodes (each carrying an observation state) and typed edges (`routes_to`, `claims_scope_over`, `references`, `manifests`, `contains`). 2. `routes_to` and `claims_scope_over` are derived from **different source files** with **no** cross-read. 3. The diff reproduces **all six** §2b golden disagreements; a run reporting zero gaps **fails**. 4. Every extractor reports `records_seen / records_emitted / records_unparsed / parser_status`; a renamed heading yields `empty_parse` (proven red-then-green, Protocol 42), never an empty healthy graph. 5. `graph_status` is `healthy` only if every extractor is `ok`; a missing source yields `broken` + non-zero exit. 6. Diagnostics render in three bands (DEFECTS / EXPECTED / PARSER STATUS); `manifested` absences and `multi_claim` sit in EXPECTED, never among defects. 7. The §2c classifier is unit-tested against real tokens from the actual files. 8. No new runtime dependency; the script is UTF-8 clean (Protocol 39); the output path is decided per §7 Q1/Q2. 9. Per the stopping rule, the minimum version is **not** gate-wired — stand-alone until it demonstrably catches drift.
STAMP · generated for RELEASE v2.8.5 commit 06e5180 (06e51801b38a) · archive input-tree hash c07fbfbdd2e1ddeb · 754 files · no wall-clock timestamp (regenerates identically when nothing changed).