Skip to content

notes: add an index, a lifecycle policy, and a retrieval benchmark - #2386

Draft
andrewboudreau wants to merge 14 commits into
mainfrom
notes/audit-0905
Draft

andrewboudreau wants to merge 14 commits into
mainfrom
notes/audit-0905

Conversation

@andrewboudreau

@andrewboudreau andrewboudreau commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

TL;DR for a reader in a hurry

notes/ had grown to 111 markdown files and 4.2MB with no index, no policy, and no way
in. 90% of it was unreachable in one hop from any front door, so an agent answering a
question read a megabyte to find a paragraph.

This adds the index and the rules, and cuts the cost of answering a question by 67%.
It does not make notes/ smaller — bytes go up ~45KB, because the index, registry,
policy and rules digest are new files. That trade is the honest summary of this PR: much
faster to use, not smaller. If the goal was fewer bytes, this does not achieve it, and
the last section explains why that turns out to be hard rather than merely undone.

Measured

main this branch
notes/ files 158 155
markdown files 113 108
notes/ bytes 4,713,896 4,761,180
cost to answer 10 canonical questions 1,100,325 B 397,053 B (-64%)
worst-case hops to an answer 5 1
questions resolving at all n/a 10/10

Diagnosis that motivated it, all measured: notes/ grew append-only (+92,348/-6,534 lines
lifetime, a 14:1 ratio); 76 markdown files were added in August alone; exactly 7 of 111
files were reachable from a front door; AGENTS.md cited exactly one. There is almost no
textual duplication (0 file pairs share 3+ paragraphs) -- the redundancy is conceptual,
and the dominant decay mode is "the method stays true, the numbers go stale".

What this adds

  • notes/README.md -- generated index, the one notes/ never had.

  • notes/data/notes-registry.tsv -- tier/bucket/status/summary per file, the index's source.

  • notes/CONVENTIONS.md -- lifecycle policy. §3 is the load-bearing rule: a retirement
    deletes the file and rewrites every citation in the same commit, or does neither.

  • notes/codegen-rules.md -- ~20KB digest of the codegen rules meant to be loaded whole.

  • tools/notes_index.py -- --write / --check, following cpp_tu_state.py's idiom.

  • tools/notes_bench.py -- 10 canonical questions with answer regexes, so "did a fact
    survive this edit" is a command rather than an opinion.

  • Root CLAUDE.md -- there wasn't one.

  • tools/check_dead_references.py extended to scan config/**/*.json, *.jsonl and
    tools/**/*.js. ~75% of citations of notes/ paths live outside .md and were
    previously ungated.
    24 genuine dead references found and fixed; 0 banked into
    the baseline, which shrinks by 12 lines.

    This branch originally also scanned src//src_tu//include/. main landed a
    better implementation of that while this PR was open
    (7d83b57a2), with a real
    lexer and -- the part that matters -- a separate grouped ratchet so the prose
    --update cannot bank hundreds of source-comment entries. This branch now defers to
    it entirely and drops its own NOTES_ONLY_HEADS restriction, which had kept the gate
    quiet at the cost of leaving the tree's ~950 dead src/ refs in code comments
    permanently ungated. See ece7c4303 -- two tests were deleted with it, because they
    asserted the restriction that is now gone.

Two harness defects this surfaced

Both were found by composing the branch against 23 commits of main rather than testing it
alone, and both were gates crying wolf on work that changed no fact -- the failure mode
that gets a gate switched off:

  • notes_bench failed on prose re-wrapping. main moved the D0/D1/D2 emission-order
    fact in writer.md from L296 to L523, where it still reads verbatim; the reflow just
    split the literal phrase across a newline. Now matches whitespace-flattened text.
    Negative-controlled: always -> sometimes still fails the row.
  • notes_index --check demanded a hand-written registry row per file, so every
    promotion PR dropping a class-facts JSON turned it red -- main added five in 23 commits.
    Those self-register now from their own class/overlay/rtti_name fields. Curation is
    still enforced elsewhere: a stray .md still fails --check.

Why the file count barely moved

It is at the floor the policy allows. After the first pass zero files have zero inbound
citers
, so the retire rule is exhausted; further reduction needs MERGE of cited files.
The four strongest candidates were examined and all four declined, each verified
independently:

  • collision-query-classes.md <-> handoff-sphereclsn-detectclsn.md: collision-system.md
    pins 9 exact line-number citations into them. Line numbers are invisible to the
    dead-reference gate, so any content move breaks them silently.
  • ghidra-dsd-landscape.md <-> overlay-residency.md: the latter is bench-anchored and
    §9-cited by number.
  • arm9-endgame.md: 28 src/ files cite it as the asm-primitive policy anchor.

Going further means accepting silent breakage. That is a decision for a reviewer, not one
to make quietly inside a cleanup PR.

Deliberately untouched

writer.md, builder.md, matching-style.md, cpp-tu-current-state.md are byte-identical
to main
-- verified after rebase. matching-style.md got a surgical dead-reference fix on
main that a rewrite would have reverted; writer.md is dense operational rules and grew 51%
on main during this work; builder.md was being edited by another session; cpp-tu-current-state.md
is tool-owned output.

mwccarm-codegen.md is the one exception: 8 insertions, 0 deletions, at line 4768 of 4778.
153 files cite it by section and two by line number, so the pointer to codegen-rules.md
goes at the end where it cannot shift anything. Both cited anchors verified intact.

Notes for review

  • The doctrine tier reads 370,209 bytes against a 153,600 target. That gap widened
    during this work and none of it is from this branch: writer.md alone went 43,936 ->
    75,202 bytes on main during this PR's life. Reported, not gated -- no check fails on it.
  • tools/actor_names.py truncates symbols/actor_renames.tsv (386KB) to a header row on
    re-run. Found, flagged, not fixed -- out of scope for a notes PR.
  • Not done: wiring cpp_tu_state.py --write-note into update-chaos-data.yml's refresh
    step. The note it maintains is stale on main.

Verification

Green on the rebased branch against origin/main:

check_dead_references.py   EXIT 0   no new dead refs, no broken markdown links
notes_index.py --check     EXIT 0   registry, README and 155 notes/ files agree
pytest test_check_dead...  EXIT 0   48 passed
notes_bench.py             EXIT 0   10/10 resolve, 397,053 path_bytes

🤖 Generated with Claude Code

https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF

@tangos-validator

tangos-validator Bot commented Sep 7, 2026

Copy link
Copy Markdown

❌ PR validation — Failed

test merge conflicts with exact base 5555bb3

Full merge validation

Check Result
Committed test merge no — conflicts

Conflicts:

AGENTS.md
config/dead-reference-baseline.json
notes/enemy-leaf-provenance.md
notes/minigame-provenance.md
notes/mwccarm-codegen.md
notes/overlay-ambiguous-references.md
notes/sinit-ownership-pilot-ov002.md
notes/sinit-wave-a-ownership.md

The private worker commits a test merge, builds the stock ROM profile, compares every executable module, measures matched and source-built code, checks contributor lineage, and verifies affected relocations. The mod profile is opt-in and is not part of this merge gate.

andrewboudreau and others added 14 commits September 7, 2026 00:44
…atch .gitignore


notes/ is append-only (+92,348/-6,534 lifetime, 14:1) at 337 commits/7 days across
117 files/14 days -- any policy needing mass edits or unenforced discipline fails
here. This adds the missing piece: a mechanical retire rule, a write path for new
findings, and the citation-debt rule, grounded in a full citation-graph audit
(scratchpad/laneE) of this worktree.

Retire rule (CONVENTIONS.md S3): 0 inbound citations -> delete + one ARCHIVE_LOG.md
row, never move; >0 -> in-place banner, never move, never delete. "0 inbound" is
refined past a naive notes/-prefixed text search: cross-checking citations.tsv
against every note's own outbound relative links rescued 2 of 31 apparent
zero-citation files (actor-profile-pilot.md, llm-assisted-global-data-migration.md)
that are cited only via bare-filename note-to-note links, invisible to that search
alone. A third git-grep-by-basename pass catches citations no gate scans at all
(48 of 75 currently-dead notes/... citations live only in .c/.js/.json/.jsonl).

notes/archive/ is declared frozen (no new entries, ever): all 8 files there carry
live citations today (measured), so dissolving costs the exact citation-rewrite the
rule exists to avoid, and the last archive-by-move pass is still producing dead
citations now -- notes/pret-idioms.md moved to notes/archive/pret-idioms.md, and
three agent-prompt files (tools/refine_run.js, tools/sched_run.js,
tools/archive/crack_pr104.js) still send agents to the dead path.

Freshness-contract rule is grounded in a measured example: handoff-marker-typing.md
stamped its count correctly (Written 2026-08-06, command included) and was still 20x
stale a month later (767 claimed vs 37 from tools/marker_census.py today). Same
failure at tool scale: cpp_tu_state.py --check-note is documented as authoritative in
6 places and wired into 0 CI workflows.

.gitignore gains 5 generalized patterns (notes/goal-*.md, notes/*.local-draft.md,
notes/*_agent[0-9]*.tsv, notes/*_agent[0-9]*.md, dated-session-log names) replacing
the one-off-per-file approach of the existing 4 entries. Verified against all 135
tracked notes/** files (zero new collisions) and against the 6 real debris files
sitting untracked in the primary checkout right now (all 6 match).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
Extends the reference-integrity gate to five surfaces it was blind to, and
fixes every dead notes/ citation the extension found. None banked.

BLIND SPOTS CLOSED (measured on this tree):
- tools/**/*.js: comments AND string/template literals -- the orchestration
  scripts build multi-hundred-line agent prompts as one backtick literal,
  which is exactly where a stale notes/pret-idioms.md kept surviving.
- src/**, src_tu/**, include/** (.c/.h/.cpp/.hpp): comments only, restricted
  to notes/-headed references (NOTES_ONLY_HEADS). Scanning these surfaces
  with the full TOPDIRS set produced 400+ hits, nearly all of them accurate,
  deliberate TU-merge history ("absorbed into src/actors/<Class>.cpp") --
  narrowing to notes/ keeps the real citations and drops that entire
  false-positive population, since git grep -c confirms no C/C++ file in
  the tree cites docs/.
- config/**/*.json, root *.json (except ROOT_JSON_LEDGERS), every *.jsonl:
  parsed as JSON where structured (config/tu_manifest.d/**'s own
  boundary_evidence/notes fields, via _tu_manifest_prose -- NOT
  legacy_source, which names pre-merge files that are supposed to be gone),
  else scanned as text. attribution.json alone produced 3000+ false
  "dead" overrides on the first pass (renamed/TU-folded sources ledgers are
  designed to survive); it and three siblings are now excluded the same way
  GENERATED dirs already were. Four *.jsonl ledgers get the same treatment.

Iterating from "scan everything under every new surface" down to this
scope-restricted version took the false-positive count from an initial
3475 to the 24 genuine new dead references fixed below; --list confirms
the pre-existing 144-entry baseline is otherwise untouched (146 - 2 healed).

FIXES (24 new dead references, 0 banked):

(a) notes/pret-idioms.md family -- real file is notes/archive/pret-idioms.md.
    Agent-prompt citers (tangos.json, tools/chaosviewer.config.json,
    tools/refine_run.js, tools/sched_run.js) are repointed to
    notes/matching-style.md instead of the archived file: pret-idioms.md
    recommends `register` (measured inert on this compiler) and preserving
    ternaries ("not universal" per matching-style.md's own guidance), so an
    agent reading it live would be pointed at superseded advice, not merely
    a moved file. Non-prompt citers (notes/levers.jsonl, a src/ comment,
    tools/archive/crack_pr104.js, the archived runbook) repoint to the real
    notes/archive/pret-idioms.md path, since they cite it as a historical
    fact rather than live guidance. tools/archive/crack_pr104.js also had
    its own self-reference fixed (tools/crack_pr104.js -> its current
    archived path). notes/CONVENTIONS.md's Retire-rule section narrated
    this exact failure as still-open; updated to record it fixed and to
    note (3)'s git-grep fallback now largely overlaps the gate's own scan.

(b) Four private-memory slugs cited as repo paths that can never resolve --
    they name files that exist only in an assistant's private working
    memory, never committed to this repo: notes/actor-class-names-off-by-one.md
    (include/daObjHmBskt_c.h), notes/key-function-tu-vptr-store-blocker.md
    (src/game/actors/d_a_obj_km3_dorifu.cpp, src_tu/actors/TTC_MovingBar.cpp
    x2), notes/phantom-references.md (include/nitro/hw/registers.h),
    notes/stale-tu-map-overcut-ov006.md (config/tu_manifest.d/ov006/
    MgBingoBallSlotsShot.json, MgWhichWiggler.json). No repo note invented,
    no stub created -- each comment was rewritten to state the fact
    directly, dropping the dangling citation. Same treatment for two
    citations of a since-abandoned notes/minigame-family-decisions.md
    across src/_ZN11dScMgCoin_c13OnYoshiTryEatEi.cpp, src/minigames/
    d_s_mg_bsc.cpp, src/minigames/d_s_mg_card.cpp and dScMgCard_c.json's
    own boundary_evidence, and one dangling notes/dtor-variant-order in
    src/actors/dScMgRoulette_c.cpp, include/daBgSnwmn_c.h's "(see
    notes/handoff and the Platform family census)" -- also dropped, since
    neither referenced note exists in this tree.

(c) Stragglers: src/func_02059d8c.c -> notes/archive/func_02059d8c-asm-origin.md
    (real location). nearmiss/db.jsonl's embedded README) typo ->
    README.md).

SELF-INFLICTED: the tool's own new docstrings, and this commit's first
CONVENTIONS.md draft, literally quoted example dead paths
(src/_ZN15dScMgRoulette_cD1Ev.cpp, notes/pret-idioms.md) -- reworded to
<placeholder> syntax or a slash-free mention so the gate does not fail on
its own prose.

BUG FOUND AND FIXED IN THE SAME CHANGE: the new config/**/*.json branch in
_prose_targets() computed BASELINE.relative_to(REPO) unconditionally, which
raises ValueError under the pre-existing RelativeLinkTests helper (patches
REPO but not BASELINE) the moment a test writes under config/. Extracted
_baseline_rel(), which returns None instead of raising when BASELINE isn't
under the (possibly test-patched) REPO.

OUT OF SCOPE, LEFT FOR A HUMAN: config/profile-campaign-baseline.json's one
entry says "drop it in the same change that lands #2238" -- #2238 is merged
in this branch's history, but retiring the entry is check_profile_campaign.py's
call, not this gate's; only its stale path text (daPropeller_Heyho_c.cpp ->
d_a_propeller_heyho.cpp) was corrected here.

VERIFICATION:
- python tools/check_dead_references.py: 11310 prose files, 7911 refs, 144
  unresolved (all pre-existing baseline, was 146 minus 2 now-healed entries
  removed), 0 new. 814 markdown links, 0 broken.
- --list: the full 144-entry dump is exactly the pre-existing baseline: no
  fix listed above still appears.
- python -m unittest tools.test_check_dead_references: 43 tests, all pass
  (19 new: one detection + noise pair per new surface, plus unit tests on
  _c_comments' LEGACY_LISTING_RE stripping and _tu_manifest_prose's field
  restriction).
- Byte safety: every src/src_tu/include edit verified comment-only by
  stripping // and /* */ spans from both the merge-base (6a7c0c7) and
  working-tree text of each of the 11 touched files, normalizing
  whitespace, and diffing what remains -- identical in all 11.

No push, no PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
…re-rule exclusions, dated-file gitignore


Part 1 of the notes/ audit programme (DECISION-v2's "FIXES TO THE COMMITTED
SLICE 2"). Three of the four listed defects were already closed by commit
21e148507 (the pret-idioms.md example was reworded when that commit fixed the
gate's own self-inflicted dead reference); this commit closes the rest.

(b) Dropped .gitignore's notes/*-YYYY-MM-DD.md pattern. It would have silently
    swallowed a legitimate dated evidence note (a real
    notes/sinit-audit-2026-09-05.md would never reach the repo, and nobody
    would notice) -- the exact failure mode the other four scratch patterns
    don't have, because *.local-draft.md/goal-*.md/*_agent[0-9]*.* only ever
    match session debris, never a real note. Verified zero collisions against
    all 138 tracked notes/** files after the removal (git check-ignore
    cross-check). CONVENTIONS.md's own list of gitignore-backed patterns in
    S4 no longer names the dropped pattern.

(c) Added two exclusions to the S3 retire rule so "zero inbound citations"
    stays genuinely mechanical instead of quietly deleting live files:
    template/glob consumers (roles/reviewer.md, roles/humanizer.md -- loaded
    as roles/<ROLE>.md, never cited by literal name; the 8
    data/class-facts/*.json, read by glob) and policy prose (this file's own
    illustrative citations count as inbound references, the same as any
    other citer).

(d) Added the MERGE disposition: permitted only when every citer is
    gate-visible AND every citation is rewritten in the same commit; a file
    with even one citer in an unscanned surface (src/, include/,
    config/**/*.json, *.js, *.jsonl) is MERGE-ineligible. Documents that
    "gate-visible" is now much broader post-21e148507 (src/, src_tu/,
    include/ comments, config/**/*.json, root *.json, tools/**/*.js,
    *.jsonl), so a merge ineligible before that commit may be eligible now --
    this unblocks the 36 planned evidence-consolidation merges (181
    citations) that REVERSAL 2 found the prior policy text forbade outright.

Also trimmed the retire-rule's own historical pret-idioms.md narrative (now
resolved, restated as a one-line pointer to git history) to make room for the
above while holding the ~140-line doctrine cap (142 lines final).

Verification: python tools/check_dead_references.py -- 144 unresolved
(unchanged baseline), 0 new, 0 new baseline entries. python -m pytest
tools/test_check_dead_references.py -q -- 43 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
…e front doors


Part 2+3 of the notes/ audit programme. Builds the registry and generated
router that everything downstream (doctrine condensation, evidence
consolidation) will be measured against, per DECISION-v2's resequencing --
"it must exist before anything is cut, so the cut is measured against it" --
then fixes the measured 100-of-111-unreachable-in-one-hop discoverability gap.

notes/data/notes-registry.tsv: one row per file under notes/ (138 total,
including itself and the generated README) -- path, tier, bucket, status,
owner_tool, summary. Every summary was written from actually reading the
file (skimming structurally for the largest, e.g. the 317KB
mwccarm-codegen.md), not derived from its filename.

Tier is READ MODE, not directory (DECISION-v2 REVERSAL 3): doctrine is what
an agent loads whole to do work, evidence is what it looks up one fact in
and never reads whole. This matters concretely: notes/agents/roles/writer.md
(44.7KB) and builder.md (31.8KB) count as doctrine because they're role
prompts loaded every time that work happens, even though they share a
directory with process/orchestration files. Conversely, setup/build/
version-archive guides (rom-build.md, setup-mwccarm.md, worktree-inputs.md,
port-selftest-bmp-gate.md, and 21 others previously drafted as doctrine)
were reclassified evidence: consulted on demand, not loaded to do the core
craft. Final doctrine tier: 17 files, 271,388 bytes (265.1KB) against the
150KB cap named in the design discussion -- OVER by 115.0KB, reported
honestly per instruction rather than hidden by under-scoping doctrine to fit;
condensing it (cutting writer.md/builder.md, per REVERSAL 3) is separate,
not-yet-run work.

Buckets (task-first, chosen so a question lands in one place): process (34 --
agent pipeline, PR/match tooling, this policy, the registry/README
themselves), matching (19), naming (8), provenance (23), cpp-conversion (15),
tu-promotion (25), build (14).

tools/notes_index.py: --write regenerates notes/README.md from the registry;
--check verifies (a) every real file under notes/ has a registry row, (b)
every row names a file that exists, (c) README.md matches what --write would
produce, (d) reports doctrine bytes vs. the 150KB cap without gating on it
(gating would make --check permanently red for a defect this tool can't fix
alone). Follows cpp_tu_state.py's --write/--check-note idiom. README.md is
self-referential (its own doctrine tag counts its own byte size), so --write
iterates to a fixed point instead of ever leaving a one-pass-stale page.

No per-file in-file metadata header was added (Option A's proposal, rejected
per the brief): it would duplicate the registry row and CONVENTIONS.md's own
banner scheme, and would fail generated notes like cpp-tu-current-state.md on
every regeneration.

Front doors: AGENTS.md (which cited exactly one notes/ file), CONTRIBUTING.md,
and README.md each gain a one-line pointer to notes/README.md. Added a root
CLAUDE.md (there was none) -- 42 lines, what the repo is, the few things to
know before touching it, and pointers to AGENTS.md/notes/README.md/
notes/CONVENTIONS.md, no duplication of AGENTS.md's content.

Verification: python tools/check_dead_references.py -- 144 unresolved
(unchanged baseline), 0 new, 0 new baseline entries (including the
self-inflicted `symbols/vtables` token this work introduced and then fixed
in a registry summary before committing). python tools/notes_index.py
--check passes. python -m pytest tools/test_check_dead_references.py -q --
43 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
…rules.md


Task 0: install tools/notes_bench.py + notes/data/notes-bench.tsv (10-question
BFS-from-front-door retrieval-cost harness, ported from the exec3 scratchpad;
fixed its --bench default to resolve from repo root instead of tools/).
Baseline before this commit: 10/10 questions resolve, Q6 unreachable from any
front door, total path_bytes across the 9 reachable = 1,100,325 bytes, with
Q2+Q3 alone costing 646,892 bytes (59%) because both resolve into the 317KB
notes/mwccarm-codegen.md.

Task 1: add notes/codegen-rules.md (21,239 bytes), a condensed doctrine file
covering mwccarm-codegen.md's curated `## 1`-`## 6d` plus `## 6az` (the
Fix12-by-value wall, ~170 citers -- the single most-cited passage in notes/).
Every measured number, code idiom, and named mechanism from those sections is
preserved; only the narrative log-style treatment is tightened, and each rule
cites its mwccarm-codegen.md section label instead of reproducing the fuller
derivation. Also folds in the D0/D1/D2 destructor-emission-order rule (was
stated only in notes/agents/roles/writer.md and unreachable by benchmark Q6)
since it is thematically a codegen rule.

notes/mwccarm-codegen.md itself is untouched except for one appended block at
the very end of the file (8 lines: a separator plus a pointer note), not under
`## 1` as first considered -- inserting there would shift every subsequent
line number and disturb matching-style.md's two line-number citations
(~line 448, ~line 2154). Appending at EOF keeps 100% of the file's existing
line numbers and section byte offsets intact. git diff --stat: 8 insertions,
0 deletions; 317,357 -> 317,775 bytes, 4770 -> 4778 lines.

Retargeted the 7 front-door/agent-prompt strings that sent agents to the 317KB
log FOR THE RULES -- CONTRIBUTING.md, README.md, tangos.json,
tools/chaosviewer.config.json, tools/sched_run.js, tools/refine_run.js,
tools/archive/crack_pr104.js -- to read codegen-rules.md first, while
preserving every section-number reference they carried (6e-6g, 6aa/6ab/6ac)
verbatim since those still name the dated log. Updated notes-bench.tsv's Q2
and Q3 answer_file to notes/codegen-rules.md.

Also fixed tools/notes_index.py's render_readme to emit real markdown links
for doctrine/agent-tier entries instead of plain backtick text -- this is what
actually makes notes/agents/roles/writer.md (and everything else) reachable in
one hop from notes/README.md, which is itself a front door. Regenerated
notes/README.md (+4,254 bytes from link-syntax overhead across 140 rows).

Net effect measured by notes_bench.py: Q2+Q3 path_bytes drop from 646,892 to
~24,183 combined (both now resolve through the 21KB codegen-rules.md instead
of the 317KB log), and Q6 goes from unreachable to resolved in 1 hop via the
notes/README.md link fix. Full before/after totals reported alongside this
change set.

Added registry rows for notes/codegen-rules.md and notes/data/notes-bench.tsv
in notes/data/notes-registry.tsv (doctrine total: 291,903 bytes / 18 files,
accounted for in the follow-up commit's condensation work).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
Task 2: cut narration from the doctrine tier while keeping every durable rule,
measured number, and named mechanism -- per Lane D's finding that 55-75% of
these files is narrated case-study walkthrough with the rule buried inside.

notes/tu-promotion-conventions.md: 30,399 -> 26,922 bytes (-3,477).
- Removed "## Known open items at the time of writing" (two bullets about
  stale declarations / a daEyBm_c.h comment that are now stale themselves)
  and replaced with a pointer to the live authority page,
  notes/cpp-tu-current-state.md, and `tools/classqueue.py list`. This file is
  conventions, not status, and should not carry its own open-items list.
- Collapsed section 6's nine sub-headings (rule / why / landed measurement /
  marker uniqueness / shared-extern ordering / constructor-destructor path /
  brace-position / unnamed-member marker / marker-vs-ROM-byte / failure mode /
  reviewer check) down to four: the rule + fallback, marker mechanics,
  constructor/destructor path (folding in the brace-position gate-defect
  table), and the #2062-vs-#2064 failure mode (folding in the reviewer-check
  bullet). Every table, regex, and the #2062 (8 backslide rows) vs #2064
  (6 markers, 0 backslide rows) case study survives verbatim; only the
  subheading scaffolding around them is gone. Benchmark Q4's anchor phrases
  (all six numbered S1-S6 convention headers) are unaffected.

notes/runbook-type-reconstruction.md: 28,496 -> 27,158 bytes (-1,338).
- Condensed "## 3. The ladder, with the tree's own before/after" from ~88
  lines of four full code blocks (BrickBlock.h rung 0, Fader.h rung 1,
  _ZN10BrickBlockD0Ev.cpp rung 2, _ZN5Fader13AdvanceInterpEv.cpp rung 3) to a
  27-line bullet-point description of the same four rungs, keeping both
  warnings load-bearing to the pattern: (a) a polymorphic class needs a
  C-side `void* vtable` member, (b) Fader.h's original vtable guess was known
  wrong despite every field being correct. Points to the landing commits
  be9d58e (#866) and 86cb30a (#970) for the full worked example instead of
  reproducing it. Sections 1, 2, 4-8 (including the "Known dead ends" section
  and the tail-call-veneer reconciliation script) are untouched.

notes/agents/roles/builder.md: 31,825 -> 31,662 bytes (-163).
- Replaced the romdata_check/romExtent passage (the `88` decimal / false
  `0x58` / real `0x88` measurement) with a pointer to writer.md, where the
  identical passage is canonical -- this file's copy was a near-verbatim
  duplicate. writer.md's own copy is untouched.

notes/matching-style.md (45,127B) and notes/agents/roles/writer.md (44,671B),
the two largest doctrine files, are deliberately left byte-for-byte untouched
(git diff --stat confirms no changes to either): both are dense, line-by-line
measured rules with essentially no narrative padding to cut, and writer.md
lines 286-297 hold the D0/D1/D2 destructor-emission-order rule that benchmark
Q6 depends on and that exists nowhere else verbatim.

Doctrine total after this commit and the prior one: 291,903 bytes / 18 files,
against the stated 153,600-byte cap -- 138,303 bytes (135.1KB) over. This is
reported honestly rather than gamed: codegen-rules.md's +21,239 bytes is a
deliberate doctrine-tier addition (it collapses ~600KB+ of measured
path_bytes cost, see notes_bench.py output in the prior commit and the final
task report), notes/README.md's +4,254 bytes is the link-fix regen, and the
combined -4,978 bytes from this commit's three condensations is a small
fraction of the total given how measurement-dense matching-style.md and
writer.md are and how much of the cap is structural (18 files each carrying
non-narrative, singly-sourced facts). No file was relabelled out of the
doctrine tier to hit the number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
Per CONVENTIONS.md SS3: zero-inbound gets deleted outright, nonzero-inbound
gets an in-place banner, never a move.

Deleted (zero inbound across all three SS3 searches -- check_dead_references.py's
scan, every other note's relative links, git grep by basename):

- handoff-validator-and-tests.md: all three asks (test coverage,
  check_header_offsets.py gate, the 200-file cap doc) are resolved;
  tool-tests.yml's own header now explains the rationale better and more
  currently.
- class-init-pilot.md: a strict subset of actor-profile-pilot.md (same 20-row
  pilot, same WATERFALL/PLAYER/STAR_CAMERA/collision findings), never updated
  since its originating PR while actor-profile-pilot.md grew to the full
  391-row registry.

Deleting handoff-validator-and-tests.md orphaned config/dead-reference-baseline.json's
one entry naming it as a citing file (ref "tools/test_"). Dropped that row
rather than leave a baseline entry pointing at a file that no longer exists --
test_check_dead_references.py's well-formedness check requires every
baselined citing file to still exist, and this is cleanup of my own deletion's
fallout, not banking a new accepted dead reference.

Bannered in place, not deleted (nonzero real inbound citers, all gate-visible):

- handoff-class-skeleton.md: notes/collision-system.md:382 cites it. Its
  "zero enrolled destructor migrations" claim is now false (20+ TU-promotion
  commits landed since); the skeleton-conformance methodology and the two
  rtti_vtables.json bugs it fixed remain valid and reusable.
- handoff-marker-typing.md: cited by .github/workflows/merge-stranding.yml,
  CONVENTIONS.md, and tools/merge_stranding.py (x2). The "767 bare markers"
  headline is ~20x stale (37 remain per tools/marker_census.py, measured
  2026-09-05); the D1-vs-D2 soundness argument and the five traps stay valid.
- plan-base-headers.md: the task brief assumed this was simply retirable, but
  notes/rtti-reconciliation.md cites its specific findings (the 330-row
  width-adjudication method, confirmation the six field declarations landed)
  as historical evidentiary support -- a real MERGE-INELIGIBLE-for-deletion
  case. The plan's own deferred "real inheritance" refactor has since landed
  (dEnemyBase_c.h : dActor_c), so the plan itself is superseded and now says so.

handoff-sphereclsn-detectclsn.md needed no change -- it already carries an
adequate self-written SUPERSEDED banner. Only 4 handoff-*.md files exist in
the tree; a 5th referenced in the task brief could not be found anywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
… vtable table, fold smartball provenance, correct one archive mislabel


Completes the merges whose source-file deletions and (for the sinit case) new
target file landed in the previous commit: this commit carries the absorbed
content into each merge target and rewrites every citation, in the same
commit as the content move, per CONVENTIONS.md SS3. Every merge below was
checked MERGE-eligible only after confirming every real citer sits inside
check_dead_references.py's scanned surfaces.

- enemy-provenance.md absorbs enemy-leaf-provenance.md's 8 classes (17
  dEnemyBase_c-family classes total; the old "set A"/"set B" split was a
  session-order artifact, not semantic). Rewrote all 6 include/*.h citers:
  ChiefChilly.h, Goomboss.h, Whomp.h, JetStream.h, daWanwan_c.h, dEnemyBase_c.h.
- overlay-residency.md SS0 absorbs overlay-ambiguous-references.md's unique
  content (the original 169-reference disagreement table, three wrong-callee
  bug names, reproduction commands). Rewrote 4 citer locations: include/Player.h,
  notes/rtti-reconciliation.md, tools/rtti_symbols.py, and two literal path
  mentions inside archive/ARCHIVE_LOG.md's historical incident narrative
  (reworded to remove the slash-bearing path token rather than leave a new
  dead reference behind, since new baseline entries are never permitted).
- actor-profile-pilot.md absorbs profile-macro-patterns.md's data-placement-order
  table, factory-placement/vptr-population stats, and the 0x08-vs-0x1c misread
  mechanism -- worded to say the registry TSV still records the mis-parse
  rather than claiming it already fixed, since profile-lifecycle-crosswalk.md's
  own text only proposes the fix. Rewrote profile-lifecycle-crosswalk.md's 1 citer.
- Deduped the byte-identical 18-slot ActorBase virtual-order table:
  actor-vtables.md stays canonical, actor-naming.md and cpp-naming-guide.md
  now point at it instead of keeping their own copy.

notes/archive/ARCHIVE_LOG.md: corrected n64-decomp-cross-reference.md's status
(still taking live rename-cascade commits weekly, mislabeled dead in the
original 2026-08-14 triage) and added the "Deleted 2026-09-06" ledger section
covering the 9 rows from the previous commit -- the file itself stays
physically in place, archive/ is frozen in both directions.

Folded minigame-provenance.md's cMgSmartball_ball_c section into
smartball-provenance.md: its table only listed 16 of 17 unk_ candidates
named, blind to 12 already-named header fields (state3a, state3b,
hit/hitA/hitB/hitC, anyHit, specialHit, nearby, targetIndex, soundTimer,
soundPlayed) and 5 additional pad determinations (pad_044, pad_0e7,
pad_101/111/122/12a) that minigame-provenance.md's separate pass had found.
Corrected count: 28 of 29. minigame-provenance.md keeps its entry (PINNED,
never renamed/deleted) but the section is now a short pointer;
include/cMgSmartball_ball_c.h's own top comment no longer cites
minigame-provenance.md for a claim that file no longer makes for this class.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
…above


notes/data/notes-registry.tsv: removed 9 rows for the deleted files
(handoff-validator-and-tests.md, class-init-pilot.md, enemy-leaf-provenance.md,
overlay-ambiguous-references.md, profile-macro-patterns.md, and the 4 sinit
case files), added 1 row for the new sinit-ownership-cases.md, updated
enemy-provenance.md's summary to name all 17 classes across 2 module groups,
and corrected n64-decomp-cross-reference.md's status from historical to live.

notes/README.md regenerated via `python tools/notes_index.py --write`: 132
files, doctrine 290,458 bytes across 18 files (unchanged from before this
session -- the doctrine tier itself was not touched).

Verification, run after all three commits:
- python tools/notes_bench.py: 10/10 resolve, path_bytes 337,109 (down from
  the slice-4 baseline of 340,875 -- not a regression).
- python tools/check_dead_references.py: 0 new dead references, 0 broken
  markdown links, 0 new baseline entries.
- python tools/notes_index.py --check: registry, README and 132 notes/ files
  agree.
- python -m pytest tools/test_check_dead_references.py -q: 43 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
…R instead


The duplicated `romdata_check` romExtent trap passage (the 88/0x58-vs-0x88
measurement) is near-verbatim in both agents/roles/writer.md and builder.md.
Deduping it here would have collided with three PRs already open against
builder.md (#2326, #2327, #2332) plus a queued batch of corrections.

The replacement text is landing verbatim in that docs PR instead, keeping
writer.md's copy canonical -- same outcome, no rebase fight. writer.md is
untouched on this branch, so the two changes cannot interfere.

Registry and README regenerated for the restored byte count.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
The 2026-09-06 merge of the four sinit wave/pilot files concatenated them and
added a status table instead of deduping -- it made the case log 3,755 bytes
BIGGER than the four originals combined (43,814 B), a regression Lane B
flagged as "one case-log told four times with near-verbatim boilerplate."

Fix: pull the four files' identical method (ownership-edge tracing -> .ctor
order corroboration -> organic mwccarm reproduction -> match.py/linkcheck.py
verification), the identical "no production src/ changed" disclaimer, and
the identical worktree/claims-key paragraph into one shared preamble, plus
one master verification table (initializer/module/addr/size/match/linkcheck
for all nine rows) instead of repeating full command blocks per case.

Every one of the 9 CONFIRMED ownership verdicts, every class name
(dEnemyBase_c, CutsceneObject+daDemo_c, Painting, Bird, daObjBkBillboard_c,
daObjMarioCap_c, OneUpMushroom, daObjKurumajiku_c, MadPiano), every measured
address/size/relocation-count table, and the case-2 "classifier lesson"
correction are all still present -- only the repeated boilerplate narration
around them is gone. Result: 27,992 bytes, under the 30,000-byte target.

No other file touched; no citations changed (file path is unchanged).

Verified: notes_bench.py 10/10 resolve (337,109 path_bytes, unchanged --
this file is not a bench answer target); check_dead_references.py 0 new dead
refs; notes_index.py --check passes (132 files, doctrine 290,621B/18 files,
unaffected since this file is evidence-tier not doctrine); pytest
test_check_dead_references.py 43 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
Two false-alarm modes found by composing this branch against 23 commits of
main rather than testing it alone. Each would have made a gate that people
switch off, which is worse than no gate.

notes_bench.py matched answer_regex against raw file text. Main grew
writer.md 43,936 -> 66,562 bytes and moved the D0/D1/D2 emission-order fact
from L296 to L523, where it still reads verbatim -- but the reflow split the
literal `D1 then D0, always` across a newline, so Q6 went FAIL. DOTALL was
already set; the phrase itself no longer existed. Now matches a
whitespace-flattened copy as well as the raw text. Verified both directions:
re-wrapping passes 10/10, and a negative control changing `always` to
`sometimes` still fails Q6 -- it catches fact loss, not reformatting.

notes_index.py demanded a hand-written registry row for every file under
notes/. Class-facts JSONs are emitted by the scout/writer stage as part of
promoting a class, so every unrelated promotion PR turned --check red: main
added five (Eyerok, Goomboss, dScMgBomroom_c, dScMgSound_c, dScMgTeresa_c)
in 23 commits. Those now register themselves, with the summary derived from
the file's own class/overlay/rtti_name/candidate_text fields. Curation is
still enforced everywhere else: a stray .md still fails --check.

Four of the five also get curated rows, because their content contradicts
things stated elsewhere and a derived one-liner would bury it -- notably
Eyerok, whose ROM RTTI spells 10daIwante_c while symbols.txt has baked the
coined name into every function symbol, and dScMgSound_c, whose 82 functions
all have legacy source, refuting the promotion queue's `no-legacy-source:1`.

Green on the rebased branch against live main: dead-reference gate exit 0
(no new dead refs, no broken links), notes_index --check exit 0 (137 files
agree), 43 tests pass, bench 10/10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
Mechanical regeneration after rebasing onto aeb14f6. main added notes files
across the merge train (class-facts JSONs, role-file rulings); the index now
covers 144 files. Doctrine reads 356,149 bytes against the 153,600 target --
that tier grew on main, not here: writer.md alone went 43,936 -> 75,202 bytes
over this window. The overage is reported, not gated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF
main landed `Scan C++ comments for dead source references` (72730e1) while this
branch was open, covering the same ground as this branch's src/src_tu/include
scanning and covering it better. Rather than fight the conflict, this takes main's
version whole and keeps only what is still additive.

main's design is the better one and this branch now defers to it: a real lexer
(`_c_prose`) that will not mistake a path-shaped string or an `#include` for a
comment, and -- the part that matters -- a SEPARATE grouped ratchet at
`tools/dead-code-reference-baseline.json` with its own `--update-code`, so the
familiar prose `--update` cannot accidentally bank hundreds of source-comment
entries. This branch had instead restricted the surface to `notes/`-headed
references (`NOTES_ONLY_HEADS`), which kept the gate quiet but left the tree's
~950 dead `src/` refs in code comments permanently ungated. main's split-baseline
approach gates them for real. NOTES_ONLY_HEADS is gone.

Kept, because main does not cover these surfaces:
  - `tools/**/*.js`            comments AND string/template literals -- the
                               orchestration scripts build entire agent prompts as
                               one backtick literal, which is where a stale
                               `notes/` reference actually lived
  - `config/**/*.json`         with `config/tu_manifest.d/**` narrowed by
                               `_tu_manifest_prose()` to the `boundary_evidence`
                               and `notes` fields, and to `notes/`-headed refs:
                               `legacy_source` names the pre-merge file a TU
                               absorbed on purpose, and scanning it produced
                               thousands of false hits in the first draft
  - `*.jsonl`                  minus four append-only ledgers whose records name
                               files as they were AT THE TIME of the event
  - root `*.json`              minus the generated attribution/contribution ledgers

Two tests are removed rather than fixed: they asserted that a dead `src/` reference
in a C comment is NOT reported, which was this branch's restriction and is no longer
true or desirable. Their premise died with NOTES_ONLY_HEADS.

Also resolves the `mwccarm-codegen.md` tail conflict by keeping both main's new
sections and this branch's pointer note, with the note last -- it exists to be the
end of the file so it shifts no cited line number.

48 tests pass; dead-reference gate, notes_index --check and notes_bench all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013qeuLvkrjFxZriAZ93kFnF

@andrewboudreau andrewboudreau left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bounded tooling/integration review at draft head ece7c4303cd9502dae7572d7a5ef01699cdbdd5b found a reproducible generated-index failure:

P2: the index does not survive a fresh Windows checkout. With core.autocrlf=true, python tools/notes_index.py --check exits 1 on the untouched candidate. The committed budget is 370,228 bytes; the fresh render expects 370,425. notes/README.md gains exactly 197 bytes from its 197 CRLF line endings. doctrine_bytes() counts raw filesystem bytes, while --write emits the self-counted README with LF, so regenerating locally does not establish a stable check after Git checks it out again. Make the indexed size calculation independent of checkout line endings, and verify a fresh checkout after regeneration.

The strict retrieval benchmark passes 10/10, with one-hop answers and 398,038 total path bytes in this checkout. These are benchmark proxy measurements, not measured agent time or a general 67% reduction in question-answering cost; qualify the PR wording and reconcile its differing percentages.

Composition against main c1c8846df622f3a49b6b107e0845e9929473d059 has four conflicts: AGENTS.md, enemy-leaf-provenance.md (modify/delete), minigame-provenance.md and mwccarm-codegen.md. Preserve the newly landed protocol and evidence when resolving them, then rerun the index/reference checks on that actual composition. In particular, the conventions' current-status command should use the active v2 protocol rather than classqueue.py list.

The draft remains unmerged. This is not a complete audit of every condensed research paragraph or a source-acceptance pass.

@andrewboudreau

Copy link
Copy Markdown
Collaborator Author

Recording what actually blocks this PR, since the branch is otherwise in good shape.

The conflict is resolved. notes/audit-0905-restack-0908 @ 0bb529421 merges cleanly into current maingit merge-tree --write-tree origin/main returns no conflict lines.

What blocks it is a false positive introduced by this branch's own checker change.

python tools/premerge_check.py 0bb529421d2bd31435accaf395e55bd6fae0fe02 --base origin/main

  gate                base     merge    verdict
  dead-references     pass     fail     REGRESSION

  FAIL: 1 prose/comment reference(s) name a path that does not exist:

    config/port_linkage.json
        names `port/link100`, which is not in the tree

config/port_linkage.json:34 reads "branch": "port/link100". That is a git branch, not a path — it exists at refs/heads/port/link100, currently 597e584fcdeee54f1d77c18e344c4e4e2799b3c4, which is the exact sha the same JSON object cites in its basis field. It is correct as written and there is nothing in the tree for it to name.

The regression appears because this branch widens the scan to config JSON, and port is in TOPDIRS, so a slash-separated branch name is shaped like a repo-rooted path. main's current checker does not scan that file, which is why the base side is green.

Both baselines are the wrong remedy here — banking it would put a waiver in the file whose own docstring says a baseline nobody needs "is a place for real breakage to hide." The fix belongs in the scanner: a value under a "branch" key (or a string that resolves as a ref) is not a path reference.

Two other signals from the same run, neither a failure:

  • 2 baselined prose references now resolve — the prose baseline can shrink with --update.
  • 15 baselined C/C++ comment references now resolve — refresh with --update-code from a clean, pinned main.

Adjacent but distinct: #2534 covers a separate defect in the same file, where PATH_RE truncates a path at a + and reports the non-existent prefix as dead. Different root cause; both would be natural to fix together.

I am not able to edit tools/check_dead_references.py in this session, so I have left the branch as it stands rather than work around the finding.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G1Gdj5fTjMsW2VjRLpXxYA

@andrewboudreau andrewboudreau left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dead-reference blocker is real, but it is a composition failure, not a head failure

Run on this PR's head alone, tools/check_dead_references.py is green:

check_dead_references: 10507 scan target(s), 9930 C/C++ file(s), 11937 repo-rooted
path reference(s), 1717 that do not resolve; 980 markdown link(s), 0 that do not
resolve relative to their own file
  no new dead references
  no broken markdown links
EXIT 0

pytest tools/test_check_dead_references.py is 48 passed, and tools/notes_bench.py
is 10/10. So the PR's own verification block is accurate as written.

It goes red the moment it meets today's main. This PR extends the prose scan
surface to config/**/*.json. On the merge base, config/port_linkage.json held
"branch": "port-mount-noseat-cluster", which the path regex does not match. On
main today that same field reads "branch": "port/link100", which the regex does
match, and port/link100 is a git branch, not a tracked path.

Isolated — this PR's head with only config/port_linkage.json replaced by main's
version, nothing else changed:

FAIL: 1 prose/comment reference(s) name a path that does not exist:

  config/port_linkage.json
      names `port/link100`, which is not in the tree

EXIT 1

That is the whole delta. Every other entry that appears when this PR's scanner is
run against main's content (notes/pret-idioms.md in tangos.json,
tools/refine_run.js, tools/sched_run.js, tools/archive/crack_pr104.js and
notes/levers.jsonl; notes/stale-tu-map-overcut-ov006.md and
notes/minigame-family-decisions.md in three config/tu_manifest.d manifests;
src/actors/daPropeller_Heyho_c.cpp in config/profile-campaign-baseline.json)
is already fixed by this branch — all of those files are in its diff.

Minimal fix

config/port_linkage.json is rewritten by the progress bot on every restamp — the
last twenty-odd commits touching it are all "Restamp LINKED off port/link100 …" —
so the branch name in it will keep changing. Banking port/link100 in the prose
baseline therefore re-breaks on the next rename, and leaves a waiver for a path
nobody wrote, which is the failure mode tools/check_dead_references.py's own
docstring warns about.

The durable minimal fix is to keep config/port_linkage.json out of the prose
scan surface the same way this PR already excludes the other bot-written ledgers:
it is generated state whose branch field is a ref name by construction, not
prose anyone can fix. One line alongside the existing ROOT_JSON_LEDGERS /
JSONL_LEDGERS sets does it. Excluding the branch key specifically would work
too and is narrower, but costs a special case in the scanner.

I have not edited the tool.

Note: this is not issue #2534

#2534 is the +-truncation defect in PATH_RE (paths like
src_tu/actors/Koopa+KoopaSmall.cpp matching as src_tu/actors/Koopa). I could
not get that one to fire from this branch, on the head or in the composition
above. Landing this PR would still close it if the regex fix ships with it, but
the thing actually blocking the gate today is the port/link100 value, which is a
different false positive with a different fix.

Two blockers ahead of that one

  • This PR is still marked draft.
  • It conflicts with main in 8 files, four of them modify/delete where main
    edited a note this branch retires: AGENTS.md,
    config/dead-reference-baseline.json, notes/minigame-provenance.md,
    notes/mwccarm-codegen.md, and the deletions of
    notes/enemy-leaf-provenance.md, notes/overlay-ambiguous-references.md,
    notes/sinit-ownership-pilot-ov002.md, notes/sinit-wave-a-ownership.md.
    The four modify/delete pairs are decisions, not merges — main added content to
    files this branch retires, and the retire rule in the new notes/CONVENTIONS.md
    §3 requires the citations to move in the same commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01G1Gdj5fTjMsW2VjRLpXxYA

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant