Skip to content

fix(validator): bind the SSoT ignore guard to git instead of .gitignore text - #423

Merged
KbWen merged 2 commits into
mainfrom
fix/ignore-assertion-binding
Aug 24, 2026
Merged

fix(validator): bind the SSoT ignore guard to git instead of .gitignore text#423
KbWen merged 2 commits into
mainfrom
fix/ignore-assertion-binding

Conversation

@KbWen

@KbWen KbWen commented Aug 24, 2026

Copy link
Copy Markdown
Owner

A sibling fork of the same ancestor reported six governance findings. Diagnosis against this tree confirmed one root cause worth fixing here, at two altitudes — and the same blind spot had grown independently in both forks, which is what makes it a class rather than a slip.

The defect

.gitignore preserves persistent SSoT artifacts compared whole .gitignore lines against a fixed list of directory paths. .agentcortex/context/archive/*.md hides the archived Work Logs without ever spelling that directory, so the guard reported PASS while the governance record stopped being committed.

Reproduced before fixing — one appended line, fail=0, and git check-ignore confirming the logs were hidden:

$ printf '\n.agentcortex/context/archive/*.md\n' >> .gitignore
$ git check-ignore -q .agentcortex/context/archive/some-worklog.md ; echo $?
0
$ bash .agentcortex/bin/validate.sh | grep -E 'persistent SSoT|Summary:'
[PASS] .gitignore preserves persistent SSoT artifacts
Summary: pass=86 warn=1 fail=0 skip=8

The fix, and the three regressions it took to get right

The check now asks git. Four flags carry the correctness, and each of the last three was a regression I introduced and an independent reviewer caught:

probe a file inside each protected artifact a directory probe reproduces the original blindness — docs/specs/*.md ignores contents without matching the directory
verdict from -q, never -v -v exits 0 whenever a pattern matched, negations included. On docs/adr/* + !docs/adr/*.md, -v exits 0 while -q exits 1 and git tracks the file — reading -v would fail a correct adopter and name their protective ! line as the pattern to remove
--no-index without it check-ignore skips tracked paths, so current_state.md — the one real probe — is inert in every healthy deploy. A detection narrowing inside a change meant to broaden
outer-repo cause re-labels, never re-decides all probes ignored and non-empty rev-parse --show-prefix. Deliberately not check-ignore -- .: a blank CRLF line is the pattern \r, git strips it to the empty string, and the empty pattern matches . — that discriminator failed every core.autocrlf=true checkout, including this repository

Also: no longer gated on .gitignore existing (that branch emitted a PASS asserted without looking at anything, while .git/info/exclude and a global excludes file hide files just as well), and tri-state so a check that could not run reports SKIP rather than assurance.

The playbook is now bound to the mechanism

audit-guardrails.md Test 1 asserted git status shows none of .agent/, .agents/, .antigravity/, .agentcortex/context/. A real cold deploy shows 58 / 29 / 1 / 2 paths — and its command did not run at all, since --force is not a deploy.sh flag and the catch-all *) takes it as TARGET.

Rewritten to the verified ignore set, re-pointed at the canonical deploy.sh (the wrapper clones from the remote and writes an .agentcortex-src/ cache into the reader's own tree — this guide ships downstream), and bound by a test that executes the page's own assertion list against a real deployed ignore block. Both language twins; heading kept verbatim because it is an encoding canary in both validators.

Adopter delta

Measured on one identical tree with each version's own deployed validator, .gitignore hiding both the archive and the adopter's own spec:

  • v1.8.23[PASS] .gitignore preserves persistent SSoT artifacts, fail=0
  • this build[FAIL] 2/6 probes ignored naming each source:line

On a healthy tree the tallies are identical before and after upgrade, so this adds detection without adding churn. All four changed deployed files are core tier — adopters get it on their next deploy with no action.

Evidence

  • 17 scenarios across install / update / dev, each run rather than reasoned about: fresh deploy · --no-python host · non-git tree · nested under an outer vendor/ ignore · upgrade from v1.8.23 with downstream state seeded · re-deploy ×3 · rollback · no-trailing-newline .gitignore · git worktree · the pre-commit hook's real interpreter (powershell 5.1, not pwsh) · CRLF .gitignore
  • 16 test arms, every new guard mutation-proved against the code it guards
  • Full CI-equivalent suite, no -m filter, run as CI runs it (--splits 3): 913 passed, 1 skipped, 0 failed
  • Both validators on this repo: pass=118 warn=3 fail=0 skip=2, identical, exit 0
  • ADR-006 native-check ratchet unmoved at 204/204 — the stricter guard cost no ratchet headroom

What this does not do

The parity and structural guards live in non-required CI contexts, so they fail visibly on a PR and nothing more; making one blocking is a branch-protection setting, not something this branch can reach. Pytest is never exercised under the 3.9 floor — CI's 3.9 job runs validate.sh only — so 3.9 compliance here rests on the static ratchet plus a scan, not on ever running these files under 3.9. And the probes are representative, not exhaustive: a pattern narrower than the probe name (archive/*-worklog.md) still slips through. It catches the whole-directory and whole-extension shapes, which is the class that bit downstream.

Four findings surfaced by doing the work were filed rather than folded in: #183 workflow job-graph needs: integrity, #184 PowerShell version floor + a 5.1 CI arm, #185 backtick-path reference integrity, #186 token-governance.md §8's compaction target colliding with the archived-Work-Log contract.

🤖 Generated with Claude Code

KbWen and others added 2 commits August 24, 2026 22:56
…re text

The `.gitignore preserves persistent SSoT artifacts` check compared whole
`.gitignore` lines against a fixed list of directory paths. A downstream fork's
`.agentcortex/context/archive/*.md` hides the archived Work Logs without ever
spelling that directory, so the guard reported PASS while the governance record
stopped being committed. Reproduced before fixing: one appended line left
`validate.sh` at `fail=0` while `git check-ignore` confirmed the logs were
hidden. The same blind spot had grown independently in both forks of the same
ancestor, which is what makes it a class rather than a slip.

The check now asks git, and four of its flags carry the correctness:

- probes a representative FILE inside each protected artifact -- a directory
  probe reproduces the original blindness, since `docs/specs/*.md` ignores the
  contents without ever matching the directory
- takes the verdict from `check-ignore -q`, never `-v`. `-v` exits 0 whenever a
  pattern MATCHED, negations included: on the ordinary `docs/adr/*` +
  `!docs/adr/*.md` idiom `-v` exits 0 while `-q` exits 1 and git tracks the file,
  so reading `-v` would fail a correct adopter and name their protective `!`
  line as the pattern to remove
- passes `--no-index`, without which `check-ignore` skips tracked paths and
  `current_state.md`, the one real probe, is inert in every healthy deploy
- is tri-state: git absent or not a work tree reports SKIP, never assurance
- is no longer gated on `.gitignore` existing; that branch emitted a PASS
  asserted without looking at anything, while `.git/info/exclude` and a global
  excludes file hide files just as effectively

An outer repository hiding the whole tree gets its own cause and remedy, but by
re-labelling after the probe loop, never deciding before it: all probes ignored
AND a non-empty `rev-parse --show-prefix`. Deliberately not `check-ignore -- .`
-- a blank CRLF line is the pattern `\r`, which git strips to the empty string,
and the empty pattern matches the pathspec `.`, so that discriminator failed
every `core.autocrlf=true` checkout including this repository.

`audit-guardrails.md` Test 1 asserted `git status` shows none of `.agent/`,
`.agents/`, `.antigravity/`, `.agentcortex/context/`; a real cold deploy shows
58 / 29 / 1 / 2 paths, and its command did not run at all (`--force` is not a
`deploy.sh` flag). Rewritten to the verified ignore set, re-pointed at the
canonical `deploy.sh` so a downstream reader is not sent to the network and
handed an `.agentcortex-src/` cache in their own tree, and now bound by a test
that executes the page's own assertion list against a real deployed ignore
block. Both language twins; heading kept verbatim because it is an encoding
canary in both validators.

Adopter delta, measured on one identical tree with each version's own deployed
validator: with `.gitignore` hiding both the archive and the adopter's own spec,
v1.8.23 prints `[PASS]` and `fail=0`; this build prints `[FAIL] 2/6 probes
ignored` naming each `source:line`. On a healthy tree the tallies are unchanged,
so this adds detection without adding churn.

16 test arms, every new guard mutation-proved against the code it guards. Full
CI-equivalent suite (no `-m` filter, run as CI runs it): 913 passed, 1 skipped,
0 failed. Both validators on this repo `pass=118 warn=3 fail=0 skip=2`,
identical, exit 0. ADR-006 native-check ratchet unmoved at 204/204 -- the
stricter guard cost no headroom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Row 185 described the CI markdown-link checker's scope by quoting the syntax
it resolves. The checker's own regex extracts a bracketed-then-parenthesised
target wherever it appears -- code fencing does not exempt it -- so the example
became a link pointing at a file named `path`, and Check Markdown Links went
red in 5s.

Reworded to prose. No other broken links: re-checked every tracked .md with a
working extractor.

Worth recording why this was not caught before pushing: reproducing the CI step
locally passed vacuously. The step is `grep -oP ... 2>/dev/null`, and `grep -oP`
fails on this machine ("-P supports only unibyte and UTF-8 locales", exit 2),
so the suppressed error made every file yield zero links. A local reproduction
of a check has to be shown capable of failing before its green means anything --
which is the same defect the parent commit exists to fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KbWen
KbWen merged commit 4253549 into main Aug 24, 2026
19 checks passed
@KbWen
KbWen deleted the fix/ignore-assertion-binding branch August 24, 2026 15:23
KbWen added a commit that referenced this pull request Aug 24, 2026
…times learning how (#424)

Release cut for **v1.8.24**, packaging
[#423](#423).

## What is in it

A sibling fork of the same ancestor reported that `.gitignore preserves
persistent SSoT artifacts` returned PASS while the governance record had
stopped being committed — their `.agentcortex/context/archive/*.md`
hides the archived Work Logs without ever spelling that directory, and
the guard compared whole `.gitignore` lines against a fixed list of
directory paths. **The same blind spot had grown independently in both
forks**, which is what makes it a class.

The guard now asks git. The fix then broke three times, each break the
same shape as the defect it was fixing, and none of them caught by
self-review:

| break | why it mattered |
|---|---|
| verdict read from `check-ignore -v` | `-v` exits 0 whenever a pattern
**matched**, negations included — so `dir/*` + `!dir/*.md` failed a
correct adopter and named their protective `!` line as the pattern to
remove |
| `--no-index` omitted | `check-ignore` skips **tracked** paths, so the
one real probe was inert in every healthy deploy — a detection
*narrowing* inside a broadening change |
| discriminating on `check-ignore -- .` | a blank CRLF line is the
pattern `\r`, git strips it to the empty string, and the empty pattern
matches `.` — FAILing every `core.autocrlf=true` checkout, **including
this repository** |

The third survived a self-review, seventeen scenarios and a green
912-test suite, because every scenario deployed a fresh tree and
`deploy.sh` writes LF. **A fresh-deploy fixture is not a checkout.**

## Adopter delta

On a healthy tree, nothing changes — measured: upgrading a v1.8.23
install carrying its own work log, spec and ignore rule left the tallies
**identical** at `96/6/2/6`.

Newly caught, each a FAIL naming the exact `source:line`: a `.gitignore`
hiding the archive or `docs/specs/` by a content glob; a
`.git/info/exclude` or global excludes file doing the same (previously
not consulted at all); a project deployed under an outer repository's
ignored path (its own cause, its own remedy, and an explicit warning not
to delete that rule).

Newly **not** flagged, equally deliberate: the `dir/*` + `!dir/*.md`
idiom, a CRLF `.gitignore`, and a non-git tree — which reports SKIP
rather than a false PASS.

All four changed deployed files are `core` tier, so this arrives on the
next deploy with no adopter action. No new flags, no new deployed files,
no engine or gate change.

## Release surfaces

Seven, bumped by hand: `deploy.sh` `ACX_VERSION`, `CITATION.cff`, both
`TESTING_PROTOCOL` twins, both `AGENT_MODEL_GUIDE` twins, and the
CHANGELOG heading. There is still no test pinning their consistency —
recorded, not fixed here.

## Evidence

Full CI-equivalent suite (no `-m` filter, run as CI runs it): **913
passed, 1 skipped, 0 failed**. Both validators on this repo `pass=118
warn=3 fail=0 skip=2`, identical, exit 0. ADR-006 native-check ratchet
**unmoved at 204/204** — the stricter guard cost no headroom.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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