Skip to content

fix(gate): rule 3c governs a repository by identity, not by path prefix (BACKLOG #1067) - #653

Queued
wshallwshall wants to merge 1 commit into
mainfrom
b1-1067-repo-governance
Queued

fix(gate): rule 3c governs a repository by identity, not by path prefix (BACKLOG #1067)#653
wshallwshall wants to merge 1 commit into
mainfrom
b1-1067-repo-governance

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Authored by Builder 1; pushed and opened by the LANDER. Four files, +259/-1.

The defect

Rule 3c decided "is this repository governed" by an equality-or-slash-prefix test comparing the target's git common dir against each allowlisted root's working tree path. So path containment stood in for repository membership.

An independent clone vendored under a governed root inherited that root's governance — and the refusal then asserted a shared .git that the clone does not have. A gate refusing for a reason that is not true of the thing it refused.

Now it compares against the root's own common dir, equality-or-under.

Verification, reported by the author with controls

Blob-SHA control across the rebase — every file in the commit captured before and after:

scripts/hooks/worktree_gate.ps1            284d26ab9 -> 0db96b217   MOVED, expected, #1085 edits it
tests/test_worktree_gate_control_plane.py  9539799fb -> 9539799fb   byte-identical
docs/adr/0177-....md                       d275a0120 -> d275a0120   byte-identical
docs/adr/README.md                         3f876ea9c -> 3f876ea9c   byte-identical

Red-first: the two vendored rows fail on the unchanged gate; the two deny controls pass on both sides. Gate suite -k worktree_gate: 679 passed / 2 skipped at base → 683 passed / 2 skipped after, with node ids compared by set difference rather than by count — zero lost, exactly the four added.

Already-landed re-test, both axes with controls: commits on main citing BACKLOG #1067 = 0 (negative control #999999 = 0, positive control #1085 = 1); Get-RootCommonDirCmp present on main = 0 (control Test-Governed = 16). Subject and content agree, and neither control is silent.

ruff check clean, ruff format --check 1186 formatted, mypy 267 files clean, PowerShell parser 0 errors, non-cp1252 characters 0. Mutation: 4 mutants, all killed, with the harness refusing to score a mutant that did not change the file's SHA-256.

Rebase and ordering

Rebased onto a490993b4 after #1085 landed, since that PR edits the same rule 3c — not a formality. b1-1072 is stacked on this and lands after it; the combined-tree gate suite is 698 passed = 694 (#1072 alone) + 4 (#1067's rows), exact.

LANDER note on the banner

No docs/BACKLOG.md edit, and none is required. backlog-hygiene.yml:124-133 demands a banner only when the diff touches messagefoundry/, ide/ or messagefoundry_webconsole/. This branch touches scripts/hooks/, docs/adr/ and tests/ — no engine or IDE path.

I had previously told the author a banner was owed here. That was me quoting BUILDER.md's summary, which drops the condition the gate actually applies.

…ix (BACKLOG #1067)

"Is this repository governed?" was an equality-or-slash-prefix test comparing the target's git COMMON
DIR against each allowlisted root's WORKING TREE path, so path containment stood in for repository
membership. Any repository living anywhere under a governed root inherited its governance, including an
independent clone that shares nothing with it but its path. Measured on the shipped gate at 58e710a:
a disarm write in <primary>/vendor/thirdparty DENIED both from its own cwd and by an absolute path
token from the primary, and both refusals named the PRIMARY.

The refusal's own text is what makes this more than a nuisance. It asserts that every worktree of the
repository shares one .git directory, which is untrue of a vendored clone, and rule 3's comment in this
same file already records what a misdescribing refusal produces: people route around the gate.

The comparison is now against the ROOT'S OWN common dir. Every worktree of the primary, sibling or
nested under .claude/worktrees, answers the SAME common dir and keeps denying, so path shape stops
being what decides it.

EQUALITY-OR-UNDER, NOT EQUALITY ALONE, IS THE LOAD-BEARING HALF. A submodule's git dir is
<root>/.git/modules/<name>, so the identity-only predicate the item warned about would have flipped
submodules from DENY to ALLOW as a silent side effect of fixing the vendored case -- a control weakened
by accident, under cover of a fix. Whether a submodule should be governed is its own decision; a test
pins today's answer so that changing it has to be one.

Where a root is not a repository's top level there is no identity to compare, so the old path test
stays unchanged rather than failing open on a shape it used to catch. An allowlist entry may
legitimately name a directory that merely contains checkouts, and without the top-level check a root
that is a SUBDIRECTORY of some repo would report that repo's git dir and quietly govern all of it.

Cost is two rev-parse calls per root, cached per invocation, on a path reached only once a disarm key
is already present.

NOT REBUILT FROM w3-l1-gate-3c. That dormant branch carries 1d4056c for this item, from the round
whose siblings #1069's row records as having acquired at least five new fail-opens. It is also not
cherry-pickable: Resolve-ConfigTarget and ConvertFrom-LocalAdminShare, the helpers it is built on,
return zero hits in the current file. This design was derived before that commit was read.

ONE RIDER, NAMED SO IT IS NOT SILENT: the surviving path-prefix comparison gains
[System.StringComparison]::Ordinal, matching Get-SessionRoot two functions away. The line was being
rewritten anyway and the culture-sensitive default was never intended.

NO TEST WAS REMOVED OR WEAKENED. Four added, none deleted, none relaxed.

Measured, interpreter .venv\Scripts\python.exe in this worktree, -p no:randomly:
   red-first: the 2 vendored rows FAILED on the unchanged gate, the 2 deny controls passed on both
     sides, so neither control is co-moving with the fix
   gate suite -k worktree_gate: 679 passed / 2 skipped at base, 683 passed / 2 skipped after
   collected node ids: zero lost, exactly the 4 added -- set difference, not a count
   ruff check . clean; ruff format --check . 1186 files formatted; mypy messagefoundry 267 files clean
   PowerShell parser on the changed gate: 0 errors; non-cp1252 characters: 0

Mutation, with a harness that hashes the file before and after planting and REFUSES TO SCORE a mutant
that did not change it -- a mutant that failed to apply prints the same passing count as a test that
cannot fail. All four killed, and the two directions red DISJOINT sets:
   identity-only (drop equality-or-under) -> reds the SUBMODULE row alone
   revert to the path prefix              -> reds the two VENDORED rows alone, overlap 0
   always governed -> 4 reds, incl. two pre-existing ungoverned controls
   never governed  -> 17 reds
File restored byte-identical by SHA-256 afterwards.

Severity is conditional per CLAUDE.md section 0: a FALSE DENY in developer tooling with no product
surface, not reachable until someone vendors a clone under a governed root.

Ledger untouched: docs/BACKLOG.md is not in this diff and the banner is not mine to flip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall added this pull request to the merge queue Aug 28, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 28, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Aug 28, 2026
Any commits made after this event will not be merged.
@wshallwshall
wshallwshall removed this pull request from the merge queue due to a manual request Aug 28, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Aug 28, 2026
Any commits made after this event will not be merged.
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 28, 2026
@wshallwshall
wshallwshall added this pull request to the merge queue Aug 28, 2026
Any commits made after this event will not be merged.
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