RFC-007 Phase 3: wiki-link + composes-with edges with slugify() resolution and dangling tracking (fixes #589)#617
Conversation
Plan for RFC-007 Phase 3, reviewed round 1 by a pi/GLM-5.2 seat (HOLD, 7 findings, all dispositioned in section 19.1; 6 applied). Corrects the stale line anchors #589 inherited from before Phase 2, and records three operator decisions: prose regex at RFC-007:150 is canonical, the dangling bucket is wiki-link, and #603 is not folded. Adds a filename-basename alias index for target resolution after measuring that the literal spec resolves 13/132 wiki-links and 0/76 composes-with bullets against the live corpus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…gration Adds the wiki-link edge type (opt-in, not in DEFAULT_EDGES), the shared target-resolution path, and per-edge-type dangling tracking. slugify is imported from lib/rule-nodes.mjs rather than reimplemented (P2). Integrates rule nodes into the three modes that assumed they had no edges: --orphans now reports a rule only at degree 0, --hubs seeds rule ids into the degree map, and --from falls through to the BFS when the root has adjacency, making good on the comment Phase 2 left at em-graph.mjs:290. Target resolution tries the name-derived id first, then a filename-basename alias index. Ambiguous basenames are dropped rather than guessed. Live corpus: 81 of 127 rule nodes now carry edges, 9 links dangle. Default output verified byte-identical to 8e6e699 by cmp on --hubs and --orphans, not by proxy. Also corrects the plan's EC5, which claimed an unclosed fence swallows the remainder. stripFenced is non-greedy and needs a closing fence, matching shipped cites at em-graph.mjs:207; parity is the requirement, so the test now asserts the real behavior. Tests: tests/test-em-graph-links.mjs, 21 cases. Negative control --break-wikilink exits 1 with 11 failures. Refs #589 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Parses top-level bullets under a "## Composes with" heading, reusing S1's target resolution and dangling scaffold. Nested bullets ignored, section terminated by the next "## " or EOF. The caller passes stripFenced() only and deliberately NOT stripInline(): real bullets write their target inside backticks, so stripping inline code here would delete every target and emit zero edges across all 21 files in the corpus that use the section. RFC-007:149 scopes the inline-backtick exclusion to wiki-link and cites, so this matches the spec. A bullet that is neither a markdown link nor backtick-led names no target and is skipped rather than slugified, which avoids manufacturing garbage dangling refs from prose. Only the first backtick run is used. Live corpus: 31 rule nodes carry composes-with edges, 1 link dangles. Tests: 8 new cases, suite now 29 passed, 0 failed. Phase 1 and Phase 2 suites unchanged and green. Refs #589 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flips the wiki-link and composes-with edge rows and both machine-readable status fields to SHIPPED, with verified em-graph.mjs anchors. Resolves the two internal contradictions #589 asked to settle: - The dangling bucket is "wiki-link" everywhere. The prose at :103 said "wiki-link-dangling", a spelling that appeared nowhere else in the file and disagreed with both the contract at :677 and the kind enum at :237. - The machine-readable regex now matches the prose form character for character. Verified by JSON-unescaping the contract and comparing against both the prose span and the literal compiled into em-graph.mjs; all three agree. Adds a Target resolution subsection documenting the filename-alias index, which reconciles the "resolved by file basename" clause with the frontmatter-name node-id rule. The node-id rule itself is untouched. Also corrects two pre-existing stalenesses found while working here: the mermaid diagram still labelled Phase 2 UNBUILT while the phases table and the ledger both recorded it SHIPPED, and slugify._shipped_use still claimed the function was unused by the shipped script although scanRuleNodes has called it since Phase 2. Wires the Phase 3 suite into tests.yml; test-ci-suite-registration passes 16/16, so the suite is genuinely step-wired rather than silently skipped. grep -c UNBUILT on the RFC: 17 before this PR, 10 after. No Implementation ledger row yet. Its provenance must come from git log --follow on the merge commit, and writing a branch-tip SHA is the defect #582 was filed for and #605 had to repair. Filled post-merge. Refs #589 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, RFC coherence) Round 2 on the frozen diff returned HOLD with 5 findings, all accepted. CRLF line endings silently dropped every composes-with edge: zero edges, zero dangling, exit 0. The heading matched because that test trims, but the bullet kept a trailing \r and the bullet regex did not match it. Same silent-zero signature REQ-6 traps for empty sections, reachable through a portable line ending, and inconsistent with lib/rule-nodes.mjs:67 which already tokenizes frontmatter CRLF-aware. Split on /\r?\n/. A markdown-link target carrying a #fragment slugified the fragment into the target and dangled; the fragment is now stripped before basename. The section start test trimmed while the terminator used a raw anchor, so an indented "## Composes with" could start a section that an indented "## Next" could not end, wrongly absorbing later bullets. Both now test the trimmed line. RFC Scope's "Unbuilt" list still enumerated Phase 2 and Phase 3 work, contradicting the phases table, the edge-status table and the contract block in the same file. That was a REQ-12 miss: REQ-12 covers every place the RFC makes a claim, and the first sweep only covered the places enumerated in the plan. The two new contract entries lacked the shipped_at field all five older entries carry. Tests: t_composes_crlf_line_endings and t_composes_markdown_link_fragment_stripped. Suite 31 passed, 0 failed. The CRLF guard is not vacuous: the bullet regex returns false against a trailing \r, so the test fails without the split fix. Refs #589 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
lantiscooperdev
left a comment
There was a problem hiding this comment.
Tiered multi-agent review — RFC-007 Phase 3, two rounds, ACCEPT
Covers the full #617 diff (5 files, +1638/-34) across a scout stage, a plan review and a frozen-diff review. Both review rounds returned HOLD; all 12 findings are dispositioned and the actionable ones applied.
Verdict trail
| Round | Scope | Seat | Verdict | Blockers |
|---|---|---|---|---|
| Scouts | RFC anchors, em-graph.mjs runtime state, test/CI wiring, governance + disposition sweep |
4 parallel Sonnet | n/a | n/a |
| 1 | The plan, with Appendix A frozen | pi / neuralwatt glm-5.2 | HOLD → folded | 1 BLOCKER, 2 MAJOR, 4 MINOR |
| 2 | Frozen diff main...HEAD |
pi / neuralwatt glm-5.2 | HOLD → fixed in f76dc6b |
1 MAJOR, 4 MINOR |
Seat spend for the whole arc: $2.71. No Fable seats, no openrouter.
Round 1 findings, closed before any code was written
| # | Class | Resolution |
|---|---|---|
| 1 | BLOCKER | Step 1.8's payload called danglingOut(), defined by step 1.9. Verbatim application would have thrown ReferenceError and dropped Phase 2 to 27/28 with no clean in-step fix. Stray call removed. |
| 2 | MAJOR | Step 1.4's verify asserted a dangling key nothing emits until 1.9. Rewritten to a loop-presence grep. |
| 3 | MAJOR | Step 1.9 expected grep -c 'danglingOut()' → 4 counting the definition, but the definition reads danglingOut = () so the substring never matches it. Corrected to 3. |
| 4 | MINOR | Step 3.5's relative UNBUILT count was ambiguous. Restated absolutely: 17 at 8e6e699, 15 after 3.2, 10 after 3.5. |
| 5 | MINOR | composesTargets slugified whole prose bullets into garbage refs. Now yields no target. |
| 6 | MINOR | Two backticked spans on one bullet: first wins, made explicit. |
| 7 | MINOR | Presence-only greps, assessed as back-stopped by the functional suites. No change. |
Round 2 findings, closed in f76dc6b
| # | Class | Resolution |
|---|---|---|
| 1 | MAJOR | CRLF line endings silently dropped every composes-with edge — zero edges, zero dangling, exit 0. The heading matched (that test trims) but the bullet kept a trailing \r and the bullet regex did not match. Same silent-zero signature REQ-6 traps for, and inconsistent with lib/rule-nodes.mjs:67 which already handles CRLF. Fixed by splitting on /\r?\n/. |
| 2 | MINOR | A markdown-link target with a #fragment slugified the fragment into the target. Stripped before basename. |
| 3 | MINOR | Section start trimmed, terminator did not, so an indented ## Composes with could start a section an indented ## Next could not end. Both now test the trimmed line. |
| 4 | MINOR | RFC Scope's "Unbuilt" list still enumerated Phase 2 and Phase 3, contradicting three other places in the same file. A REQ-12 miss: the first sweep only covered the sites the plan enumerated. |
| 5 | MINOR | The two new contract entries lacked the shipped_at field all five older entries carry. |
RFC contract changes
- Edge-status rows
:84-85and both contractstatusfields → SHIPPED, with verified anchors. - Dangling bucket unified to
wiki-link; thewiki-link-danglingspelling appeared nowhere else in the file. - Machine-readable regex rewritten to match the prose form. Verified programmatically: the contract JSON-unescaped, the prose span, and the literal compiled into
em-graph.mjsare character-identical. - New Target resolution subsection defining the filename-alias index, reconciling the "resolved by file basename" clause with the frontmatter-name node-id rule.
:142itself untouched.
Orchestrator re-verification (independently executed, not taken from seat reports)
- Suites: links 31/31, phase1 10/10, phase2 28/28, ci-suite-registration 16/16. PASS.
- Negative control
--break-wikilink→ exit 1, 11 failures, and the passing tests are exactly the ones not dependent on wiki-link extraction. PASS. - REQ-8 against the real baseline: worktree at
8e6e699,cmpon--hubsand--orphansoutput → byte-identical. The suite's own before/after check is the proxy layer one short of this. PASS. - RFC anchors
239, 251, 314, 320, 332each verified to point where claimed, re-checked after the S4 line shifts. PASS. - CRLF guard proven non-vacuous: the bullet regex returns
falseagainst a trailing\r, so the new test fails without the fix. PASS. - Payload fidelity: every §A.5 block diffed against the plan, verbatim including comments. PASS.
Live corpus behavior
| Edge | Nodes with edges | Dangling |
|---|---|---|
wiki-link |
81 of 127 rule nodes | 9 |
composes-with |
31 | 1 |
--from pi-minimax-m3-builder-seat --depth 2 returns 5 nodes and 4 edges; 8e6e699 returned count: 1.
Follow-ups filed (Rule 18 step 9)
- #618 — fill the Implementation ledger row post-merge from
git log --follow. Deliberately omitted here: the provenance SHA does not exist until merge, and a branch-tip SHA is the defect #582 was filed for and #605 had to repair. Carries all 5 DEFER fields. - #619 —
em-console.mjs:114flag whitelist omits--nodes/--edges, so rule/rfc nodes and Phase 3 edges are unreachable through the console. Pre-existing since Phase 2, recorded as an explicit non-goal. - #620 — an unclosed code fence does not suppress extraction, affecting shipped
citesas well as newwiki-link. Design question surfaced by the build, deliberately not decided inside this slice.
CI
8/8 green: validate ×3, substrate, gates-classifiers, p12-invariant-gate, recall-activation, clerk-consolidation.
What user approval is checking
Per Rule 17, your approval in the UI is the review-required gate. Substantive review is complete and both rounds are closed. Worth a specific look before approving: the filename-alias index is a genuine spec extension, adopted because the literal spec resolved 13/132 wiki-links and 0/76 composes-with bullets against the live corpus. It does not change node ids. If you disagree with that call, it is the one decision here that is architectural rather than mechanical.
🤖 Posted by lantiscooperdev (bot account) — Claude Opus 5 (1M context).
…622) PR #617 shipped without an Implementation ledger row, deliberately: the row's SHA must be the merge commit, which did not exist until it landed. Provenance derived from git against the files the row claims, not from an adjacent row and not from a branch tip: git log --follow --oneline scripts/em-graph.mjs -> dafc3c8 git log --follow --oneline tests/test-em-graph-links.mjs -> dafc3c8 That is the #582 / #605 class this defers to avoid. CI anchor cited as tests.yml:407-408, verified against the merged file. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements RFC-007 Phase 3. Closes #589.
What ships
Two rule-to-rule edge types on
em-graph.mjs, both opt-in (registered inEDGE_TYPES, absent fromDEFAULT_EDGES):wiki-link—[[name]]in rule bodies, fenced blocks and inline backticks excludedcomposes-with— top-level bullets under## Composes with, nested bullets ignoredPlus the shared target resolution both depend on, per-edge-type dangling tracking, and a 31-case suite wired into CI.
The issue understated the work
#589 described two parsers and a fixture. Adding edges to rule nodes broke three assumptions Phase 2 had made explicit in its own comments:
--orphans--from <rule-slug>em-graph.mjs:290--hubsdegreemap was episode-onlyEvery line anchor #589 cited was also stale by +10 to +25 lines, drifted by Phase 2 (#604). Several pointed at blank lines; two pointed at the wrong JSON object entirely. All corrected.
Target resolution needed a decision
Built literally, the spec resolves almost nothing. Measured against the live corpus before writing any code:
wiki-linkcomposes-withNode ids are
slugify(frontmatter name)perRFC-007:142, but authors link by filename.RFC-007:151separately requirescomposes-withmarkdown links to resolve "by file basename", which needs an index:142disclaims. The RFC contradicted itself, functionally rather than cosmetically.Resolution now tries the name-derived id first, then a filename-basename alias index; ambiguous basenames are dropped rather than guessed. Node ids are unchanged and
:142is untouched. Documented in a new Target resolution subsection.Two RFC contradictions settled
wiki-linkeverywhere. The prose saidwiki-link-dangling, a spelling appearing nowhere else in the 737-line file and disagreeing with both the contract and thekindenum.em-graph.mjsare now character-identical.Verification
node tests/test-em-graph-links.mjs31 passed, 0 failednode tests/test-em-graph.mjs10 passed, 0 failednode tests/test-em-graph-nodes.mjs28 passed, 0 failednode tests/test-ci-suite-registration.mjs16 passed, 0 failednode tests/test-em-graph-links.mjs --break-wikilink8e6e699,cmpon--hubsand--orphanssed -n '239p;251p;314p;320p;332p'REQ-8 was checked against the baseline commit directly rather than via the suite's own before/after comparison, which is the proxy layer one short of the claim.
Live corpus: 81 rule nodes carry
wiki-linkedges (9 dangling), 31 carrycomposes-withedges (1 dangling).--from pi-minimax-m3-builder-seat --depth 2returns 5 nodes and 4 edges where8e6e699returnedcount: 1.Review
Two rounds, both HOLD, all 12 findings dispositioned.
Round 1 on the plan caught a blocker: a step referenced a symbol a later step defined, so verbatim application would have thrown
ReferenceError. Round 2 on the frozen diff caught a MAJOR: CRLF line endings silently dropped everycomposes-withedge — zero edges, zero dangling, exit 0. Latent on this corpus but the same silent-zero signature REQ-6 traps for, and inconsistent withlib/rule-nodes.mjs:67which already handles CRLF. Fixed with a regression test proven to fail without the fix.Round 2 also caught a REQ-12 miss: the RFC's Scope "Unbuilt" list still enumerated Phase 2 and Phase 3.
Pre-existing stalenesses fixed in passing
slugify._shipped_useclaimed the function was unused by the shipped script, thoughscanRuleNodeshas called it since Phase 2.grep -c UNBUILTon the RFC: 17 before, 10 after.Deliberately not included
No Implementation ledger row. Its provenance must come from
git log --followon the merge commit, which does not exist yet. Writing a branch-tip SHA there is the defect #582 was filed for and #605 had to repair. It will be filled post-merge.🤖 Generated with Claude Code