Skip to content

guard: register tools/INDEX.md as a generated path (prevents the predicted batch wedge) - #757

Merged
matt82198 merged 1 commit into
mainfrom
guard/register-index-generated
Aug 3, 2026
Merged

matt82198 merged 1 commit into
mainfrom
guard/register-index-generated

Conversation

@matt82198

Copy link
Copy Markdown
Owner

The predicted jam

PR #751 moved the per-tool index out of tools/CLAUDE.md into the generated tools/INDEX.md. That file was not added to tools/generated_paths.py::GENERATED_PATHS, and no tools/merge_queue.py::REGENERATORS entry was created for it — leaving the exact hole the registry exists to close, one file over.

The failure it sets up, verbatim:

  1. Two queued PRs each add a tool carrying its own INDEX: header line.
  2. Each branch's tools/INDEX.md is byte-identical to its own sources, so both are individually green.
  3. Their union is missing the other member's entry, so gen_tool_index.py --check fail-closes on an integration branch no member ever tested.
  4. Unregistered, the queue cannot repair it: regenerate_on_batch classifies the dirty tools/INDEX.md as unregistered, rows regenerator_overreach and commits nothing; worktree_is_safe refuses the whole pass over a path the repo itself generated.
  5. The queue wedges holding a batch it can never publish.

That is the suite-count jam of 2026-08-03 — the one documented in generated_paths.py's own docstring — on a different file.

Repro before the fix (TDD)

tests/test_generated_paths.py builds a hermetic temp git repo containing the real gen_tool_index.py and drives the actual gate. Against unfixed main, 3 of 15 tests failed while the jam-repro test itself passed — i.e. the drift is real today and nothing repairs it:

  • test_union_of_two_green_members_drifts_the_index_gate — PASSED pre-fix. Two members each add a tool; the union returns exit 1, out of date.
  • test_index_md_is_a_registered_generated_path — FAILED: 'tools/INDEX.md' not found in ('tests/CLAUDE.md', 'tools/CLAUDE.md').
  • test_unregistered_drift_would_be_rejected_as_overreach — FAILED against regenerate_on_batch's own predicate: ['tools/INDEX.md'] != [].
  • test_a_registered_regenerator_repairs_the_drifted_union — FAILED: no regenerator existed.

All 15 pass after the change.

This PR also demonstrates the union-drift mechanism live: adding one test file moved the tracked Python suite count 237 -> 238, which is precisely the class of drift a batch produces and no member sees.

Does registration weaken the byte-identity gate?

No — and the tests pin it. The membership criterion is that the file is derived. tools/INDEX.md is a pure function of the INDEX: lines in tools/ sources, so the only edit automation is permitted to discard is an edit to the derived bytes — information-free by construction, since regenerating reproduces them exactly. Every edit that carries meaning lives in a source file, which is not registered and still faces the unchanged gate:

  • A tools/ file with no INDEX: line still fails gen_tool_index.py closed, and --regenerate refuses to write — registration cannot launder an undocumented tool into a green index (test_a_tool_with_no_index_line_still_fails_closed, which also asserts the tree is left untouched).
  • A tampered INDEX: line is propagated into the regenerated index, where the diff shows it under review — not swallowed (test_editing_a_source_index_line_propagates_it_visibly).
  • The gate is unchanged on the pushed branch and in CI; drift still fails closed (test_the_gate_itself_is_unchanged_by_registration).

Registration adds a repair path on the integration branch. It removes no check anywhere. regenerate_on_batch still commits only registry paths, and overreach onto anything else still commits nothing.

Sequencing vs #753

#753 (guard/daemon-safety-fixes) is open and edits tools/merge_queue.py. Its hunks are at lines 1-6 and 244+; REGENERATORS lives at line 143. Disjoint, so both halves shipped here. The merge_queue.py module docstring/INDEX: line — which #753 rewrites wholesale — was deliberately left untouched to avoid a conflict on that 4000-char line; the accurate contract text lives in generated_paths.py and in a comment directly above REGENERATORS, where a reader actually looks. No merge_queue.py logic was modified.

Gates

claudemd_lint OK - gen_tool_index --check OK (146 tools, in sync) - claudemd_sync_gate --check OK - verify_test_suite_count --check OK - verify_gates_wired OK - encoding_lint --check OK - subprocess_guard clean for the new file - secret_scan --staged exit 0.

Suites: Python npm run test:py exit 0 (238 suite files) - Node 321/321 - Shell 14/14 files, pre-push-policy 18/18 subtests.

Follow-ups found (not in scope here)

  • tools/auto_merge.py:97 still hard-codes ['tests/CLAUDE.md', 'tools/CLAUDE.md'] instead of importing generated_paths — the duplicate the registry docstring says should not exist. It will now drift from the registry.
  • tools/commit_lint.py --range crashes: run_subprocess() got an unexpected keyword argument 'capture_output'. It is not wired into CI or the pre-push hook, so nothing caught it.
  • commit_lint rejects the guard type this repo uses in practice (guard(queue): restore the kill switch + atomic lock + worktree/timeout failure containment (deep-scan A1-A3,A7) #753, this PR); allowed set is build, chore, ci, docs, feat, fix, perf, refactor, style, test. Either the type list or the convention should move.

🤖 Generated with Claude Code

PR #751 moved the per-tool index out of tools/CLAUDE.md into the generated
tools/INDEX.md, but did not add that file to the GENERATED_PATHS registry or
give it a merge_queue REGENERATORS entry. That left the exact hole the registry
was created to close, one file over.

The wedge it sets up: two queued PRs each add a tool with its own `INDEX:`
line. Each branch's tools/INDEX.md is byte-identical to its own sources, so
both are green. Their union is missing the other's entry, so the byte-identity
gate fail-closes on an integration branch no member ever tested. Unregistered,
the queue cannot repair it -- regenerate_on_batch classifies the dirty
tools/INDEX.md as unregistered, rows regenerator_overreach and commits nothing,
and worktree_is_safe refuses the whole pass over a path the repo itself
generated. The queue wedges holding a batch it can never publish. That is the
suite-count jam of 2026-08-03, verbatim, on a different file.

  * tools/generated_paths.py: register tools/INDEX.md, and write down that
    registration is only half the contract -- every entry needs a matching
    regenerator or it still wedges the queue.
  * tools/merge_queue.py: REGENERATORS entry only (gen_tool_index.py
    --regenerate). No logic touched; #753 owns the rest of this file.
  * tests/test_generated_paths.py: 15 tests. The union-drift jam is reproduced
    against the real generator in a hermetic temp git repo before the fix.

This does not weaken the byte-identity gate. A registered artifact is a pure
function of unregistered sources, so the only edit automation may discard is an
edit to the derived bytes, which regenerating reproduces exactly. Every edit
that carries meaning lives in a source file that is not registered and still
faces the same gate: a tools/ file with no `INDEX:` line still fails
gen_tool_index closed and no index is written, and a tampered `INDEX:` line is
propagated INTO the regenerated index where the diff shows it. Registration
adds a repair path on the integration branch; it removes no check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@matt82198 matt82198 added merge-queue Queued for the merge-queue advancer daemon merge-priority Jump the merge queue labels Aug 3, 2026
@matt82198
matt82198 merged commit 60efd2d into main Aug 3, 2026
12 checks passed
@matt82198
matt82198 deleted the guard/register-index-generated branch August 3, 2026 20:08
@matt82198 matt82198 mentioned this pull request Sep 11, 2026
matt82198 added a commit that referenced this pull request Oct 6, 2026
…-coded copy (#758)

* guard: auto_merge reads the generated-path registry instead of a hard-coded copy

tools/auto_merge.py hard-coded ["tests/CLAUDE.md", "tools/CLAUDE.md"] when
resolving merge conflicts -- a re-typed copy of tools/generated_paths.py,
the exact duplication that registry exists to prevent. PR #757 adds
tools/INDEX.md to the registry; auto_merge would never have known.

fix_branch() now imports generated_paths as a MODULE and calls
generated_paths() at call time, so the tool tracks whatever the registry
currently holds rather than freezing a copy at import.

tests/test_auto_merge_registry.py adds two guards:
  - behavioural: injects a sentinel entry into the registry at runtime and
    asserts fix_branch resolves it (a copy cannot see the sentinel);
    shrinking the registry to empty resolves nothing
  - structural (durable): AST source scan over tools/ failing any module
    that re-types the registry as a literal collection, with its own
    falsifiability tests (planted copy is caught; a broader overlapping
    list such as merge_telemetry contended-files is not a false positive)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: regenerate suite counts after merge (post-merge count was stale)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-priority Jump the merge queue merge-queue Queued for the merge-queue advancer daemon

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant