Skip to content

Release 0.9.3 — the gate stops failing on build output, and says which files when it does - #252

Merged
qwerfunch merged 9 commits into
mainfrom
develop
Aug 4, 2026
Merged

Release 0.9.3 — the gate stops failing on build output, and says which files when it does#252
qwerfunch merged 9 commits into
mainfrom
develop

Conversation

@qwerfunch

Copy link
Copy Markdown
Owner

Summary

Ships 0.9.3. Eight commits, all on develop and green there.

The architecture stage no longer scans build output for import cycles, and when it does find one it now names the files. clad report gained an acceptance-criteria diff, so a requirement rewritten to match the code it was supposed to constrain is visible in review.

Gate: relaxed — see the [0.9.3] changelog section for the directory list and the way to keep the old scan.

Linked issue

None.

PR contract (GOVERNANCE.md §4.3)

Verified on develop at 98fafc2, before the merge commit:

  • npm run typecheck clean
  • npm run lint clean
  • npm test — 2,815 pass
  • npm run stage:drift — zero error-severity findings
  • npm run conformance — 33/33
  • node bin/clad check — 15-stage gate green
  • Spec updated — two new entries
  • CHANGELOG.md[0.9.3], dated

Scope of change

  • Patch — defect repair; the two additions extend an existing verb.

Out-of-scope checklist (must all be no)

  • Does not regress Iron Law conformance below the declared level
  • Does not bypass the anti-self-cert guard
  • Does not fork the Ironclad spec

Notes for the reviewer

Merge this with "Create a merge commit" — not squash, not rebase. A squash puts the release commit outside develop's ancestry and the next release PR reports every file touched since as conflicting. That happened with #180 and cost #181 thirty-one phantom conflicts.

Back-merge main into develop afterwards.

qwerfunch and others added 9 commits July 31, 2026 23:55
…fbac9c)

Every drift detector compares code against the spec as it stands now, so one
class of change is structurally invisible: rewrite a criterion to match what
you built, and code and spec agree. The reversal is real and in git, but no
reviewer finds it by hand.

`clad report` now carries two more sections:

- How the acceptance criteria moved — each criterion classified new /
  rewritten / removed / unchanged by joining base and head on its id, with the
  entry's status transition and any EARS pattern shift alongside.
- Declared tests — whether each test a criterion names also moved in the
  range, distinguishing a real path that did not change from a placeholder the
  harness suggested. It grades nothing; the judgement stays with the reviewer.

Two corrections that make those sections trustworthy:

- The range now anchors on the merge base, not the ref's tip. Diffing tip to
  tip charges the base branch's own commits to the range under review; on a
  live repository that misattributed four untouched features. A shallow clone
  has no merge base, so the helper falls back to the ref and still renders.
- The packet's population comes from a new status-blind enumerator rather than
  the changelog collector, which is a shipped-changes filter and drops
  planned/in_progress entries — 74% of a live range's touched entries.

Verified against a real 69-feature project: 12 rewritten criteria over the
range, 4 EARS polarity shifts, status transitions intact. Exit codes unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…F-2c02991f)

A bundler's output legitimately contains mutual imports — that is what bundling
produces. The architecture gate was scanning it anyway, so it reported cycles
that exist in no hand-written file and blocked the gate on them.

One adopter hit this five times. Every one of the five was inside a
version-ignored Vite bundle: 100% false with respect to source. They could not
find the cause, recorded the wrong one, and the issue is still open sixteen days
later; they eventually worked around it alone with a hand-written .madgerc
buried in an unrelated commit. A second adopter reproduces the same shape today,
four cycles for four, all in dist/assets.

The TS/JS arch gate now excludes generated build output — dist, build, out,
coverage, target, .next, .nuxt, .output, .svelte-kit, .vite — anchored at the
repository root, so a source directory that merely happens to be named build/
deeper in the tree is still scanned.

It does not do this when the project configures the scanner itself. madge
REPLACES its configured excludeRegExp with the command-line flag rather than
merging, so passing ours unconditionally would silently delete an adopter's own
rules — including the workaround the adopter above is currently relying on.

The scan root deliberately stays the repository root. Narrowing it to a named
source directory was measured to be worse than the defect it fixes: a missing
directory makes madge exit ENOENT, which classifies as a scanner setup gap and
skips the whole stage — a green gate that checked nothing. One adopter's
front-end has exactly that layout.

Measured, before and after: adopter front-end 4 cycles -> 0 (209 -> 197 files
scanned); cladding itself 0 -> 0 (992 -> 596), i.e. no false negatives; the
guarded adopter is untouched. The change can only remove findings, never add one.

docs/gate-scope.md now names .madgerc and package.json#madge as the override —
the word "madgerc" appeared nowhere in this repository before, which is why the
adopter had to discover it alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…it audited against

The two features that shipped on this branch left four false statements behind —
three of them printed to users, one of them a shipped acceptance criterion. The
irony is the point: the feature built to expose contract drift caused contract
drift, and its own commit never touched the entry it falsified.

- AC-cbf1c202 of the review-packet feature says the report prints FOUR sections.
  It has printed six since 308b3e7. The criterion, `clad report --help`, and the
  doc comment on the function that assembles them all said four. No test caught
  it: they assert only that the original four are present and in order.
- The packet anchors on the merge base of your ref and HEAD, then stamped the
  header and the JSON with the ref you typed. On a branch that forked earlier
  those are different commits — three apart on the corpus this was measured on —
  so an auditor reproducing by hand used a different base than the tool did, and
  the artifact never named the revision it audited against. Both are now stamped,
  and the markdown says so only when they differ.
- The criterion-movement section asserted a rewrite is invisible "because the
  code was changed to match it". The population is deliberately status-blind, so
  that line also sat above entries with no code yet. It is now conditional.
- The `planner` brief asked for an `archive_reason` when removing an acceptance
  criterion. That field exists only at feature level and the criterion schema
  refuses unknown keys, so following the instruction failed the sync barrier.

Also: the six READMEs disagreed with the spec and, in two places, with themselves
inside the same file — 308b3e7 corrected some numbers and not others, leaving a
hero that said 266 of 270 above a record row that said 273 (269 done). Feature,
done and test-file totals now match spec/index.yaml and the tests on disk.

And the 0.9.3 changelog section, which GOVERNANCE.md §4.3 requires of every PR
and the release ritual consumes by name. It carries the `.madgerc` override for
the architecture-gate change: `package.json#files` ships no `docs/`, so for an
npm adopter the changelog is the only file that can tell them.

Verified: every claim above was reproduced before the fix and re-probed after —
including the base stamp against a real 3-commit fork gap. 2783 tests green,
`clad check --tier=pre-push --strict` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…three commits

The author of those commits also wrote the probes that "proved" them. An
independent audit re-derived every claim and found eight defects; all eight
survived a refutation attempt. Three were blocking.

THE ARCHITECTURE GUARD ASKED THE WRONG QUESTION, IN BOTH DIRECTIONS.
It decided "this project configures the scanner" by looking for five filenames
in the gate's own directory. madge's loader does neither of those things: it
reads the exact name `.madgerc` walked UP to the filesystem root, plus four
`$HOME` locations and two under `/etc`, and it never reads `.madgerc.json`,
`.js` or `.yaml` at all.

  - A user whose `.madgerc` sits one directory up was GREEN before, and this
    branch turned them RED: cladding sent `--exclude`, madge replaced their
    rules with ours, and the cycles they had suppressed came back and blocked
    the gate. That is the exact harm the criterion was written to prevent, and
    the shipped test could never catch it — it wrote the config into the very
    directory it queried.
  - An empty `.madgerc`, or one carrying only `detectiveOptions`, sets no
    exclusion, yet tripped the guard — so the fix switched itself off and the
    gate kept blocking on generated output.

The guard now asks what madge asks: does an exclusion actually apply, anywhere
on madge's real search path. Two tests that fail against the old code cover the
parent directory and `$HOME`; a third covers the configs that set nothing.

A REWRITTEN OBLIGATION COULD PASS AS UNCHANGED. The criterion comparison read
`text`, `ears` and `condition` — not `action` and `response`, which state the
obligation itself: the impl-blind oracle renders `action` as "system shall:",
and a criterion carrying only these fields is fully specified. Four out of five
criteria here carry them, and a sibling project has criteria with no `text` at
all. Measured against this repository's own history, the blind spot hid 31
rewrites in 400 commits.

THE EXCLUSION MISSED THE SHAPE THE MOTIVATING ADOPTER HAS. Anchored at depth 0,
it left `frontend/dist` and `packages/*/dist` blocking. `dist`, `coverage` and
the framework caches now match at any depth; `build`, `out` and `target` stay
at the root, because those names do occur as real source directories and a
false negative is worse than the false RED being fixed. The criterion's
measurement note now says plainly that its four-cycles-to-zero figure came from
a directory that is not a gate root, and that on the gate roots on record this
removes no findings today.

Also fixed, none of them blocking:
  - The base disclosure compared a ref NAME to a 40-hex sha, so it announced a
    difference on every linear range — the previous commit's claim that it
    speaks "only when they differ" was false on every invocation. The CLI now
    resolves both to commits, which also peels an annotated tag.
  - A declared test with no file behind it was labelled `unchanged`, the same
    as a real untouched test, while the legend reserved a state for exactly
    that case. It now reads `absent`; six such rows exist in this repo today.
  - The verb glossary and two module headers still described the four-section
    packet, and two determinism notes were falsified by the base field the
    previous commit added.

Every new test was mutation-checked: reverting each fix makes them fail (2, 2
and 3 respectively). 2794 tests green, `clad check --tier=pre-push --strict`
exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous round was checked by reading the source. This one drove the shipped
bundle from outside the repo, against real adopter clones and against madge
itself as a two-sided oracle. Three of five changes held up unchanged; two did
not, and one of those was wrong in a way no source review had caught.

THE CRITERION DELTA READ THE WORKING TREE FOR ITS HEAD SIDE.
The packet names two commits, but one side of the comparison was whatever
happened to be on disk. So an uncommitted edit could hide a rewrite that IS in
the range, or charge the range with one it never made — and the same leak
reached the status, which manufactures exactly the rewrite-plus-done pairing
this section exists to surface. That it was a slip rather than a decision is
visible in the asymmetry: the candidate set and the declared-test column both
already read the committed diff. One line, plus the three regression tests that
were missing — the suite passed unpatched, so nothing pinned the contract.

THE MADGE GUARD ASKED "IS THE KEY THERE", NOT "IS AN EXCLUSION IN FORCE".
It understood only strict JSON and only key presence, with any parse failure
counting as configured. So an empty, whitespace-only, comment-bearing, INI-form
or vacuous (`[]`, `null`, `""`, `false`) config made cladding stand down while
madge applied nothing — withholding the build-output exclusion and leaving the
gate red over cycles that exist only in generated code. It now reads the file
the way madge's loader does, JSON then INI, and judges the effective value.
Driven against madge as the oracle, cladding's decision now matches madge's
actual behaviour on all thirteen config states.

Two more, found by running the battery the audit had designed but never
executed. Both are the damaging direction — madge honours a rule cladding
cannot see, so cladding sends its own exclusion, which REPLACES rather than
merges, and the user's rule disappears:
  - `madge_excludeRegExp` in the environment is a live rule with no file behind
    it; the config loader folds any `madge_*` variable in.
  - A byte-order mark in package.json hid its `madge` block, because madge
    reads that file with require() and JSON.parse does not tolerate a BOM.
And one in the safe direction: a `.madgerc` that is a DIRECTORY carries no
config, but was being treated as one, silently withholding the fix.

Also: a duplicated criterion id joined first-wins, so a rewrite of the later
occurrence reported unchanged while the same packet's spec-changes section
printed the new text. Occurrences now pair positionally, surplus on either side
counting as added or removed. The strict gate rejects duplicates, but this
renderer gates nothing and runs precisely in the window before green.

Every fix was mutation-checked: reverting each makes its tests fail (3, 1, 1
and 1). Verified end to end through a tarball built in a clean clone and
installed into an empty project, driven against adopter clones: with a
criterion edited but uncommitted, the packet is byte-identical.

Known and unchanged: a spec entry's TITLE still moves with the working tree in
the spec-changes and code-changes sections, which read the worktree by an older
design. Those are different consumers and the criterion data is unaffected.

2803 tests green, `clad check --tier=pre-push --strict` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…erion

Both of these came out of a controlled A/B run, not from reading the code.

A FAILING SCANNER NAMED THE KIND OF PROBLEM AND NEVER THE INSTANCE.
The architecture stage told a developer "the code has an import loop" and
stopped. Three things conspired: the message builder preferred the tool's
progress stream over its report stream — madge writes "- Finding files" to one
and the numbered cycle list to the other — then cut what survived to 200
characters, and finally the terminal replaced the message wholesale with a
canned plain sentence. So the one fact that makes the finding actionable, which
files form the loop, could not reach the person who had to fix it.

Now both streams survive with the report first, the budget fits a real list,
and the terminal prints the specifics indented under the plain sentence rather
than instead of it. Verified end to end: a project with three planted cycles
now names all three.

This is the defect that cost a real adopter sixteen days and left a wrong root
cause on their issue log. In the A/B run, three of three agents given the
released build "fixed" the gate by deleting or rewriting their own build output
— a wrong repair they believed was right, because nothing told them what the
gate was actually objecting to.

NOTHING SHIPPED TO AN ADOPTER SAID HOW TO WRITE AN ACCEPTANCE CRITERION.
`docs/` is not in the published package, so a project that installs cladding
receives no statement of the EARS rules its own gate enforces. Two of three
agents in the A/B run reverse-engineered them from the minified bundle. The
managed AGENTS.md block now carries the table — the trigger word each pattern
requires, one working example per row, and where the obligation versus the
reasoning belongs.

A table that drifts from the validator is worse than no table, so the test
extracts every example from the rendered block and asserts it against
checkEarsShape. Changing one without the other fails.

Mutation-checked: reverting the stream/budget change fails 3 tests, skewing one
table row fails 1. One older test pinned the defect ("stderr wins over stdout")
and was rewritten to the corrected contract. 2809 tests green,
`clad check --tier=pre-push --strict` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in cladding answered this, so four adopter projects answered it four
different ways: two reached ~100% Korean titles, two stayed fully English. The
existing guidance is about something else — "speak the user's language" governs
how findings are REPORTED, and the English-only catalog governs cladding's own
strings. What a user's spec entry should say was never addressed.

The rule now shipped, in the managed AGENTS.md block:

  English by default. If the spec already has entries, match the language THOSE
  use. Where they disagree, English.

The corpus is the source of truth rather than a setting, for two reasons. A
setting has to be maintained and can contradict the file it describes; entries
already written cannot. And reading the language off whoever happens to be
talking makes a repository change voice with its contributors — a project that
speaks Korean should keep speaking Korean when an English-speaking colleague
adds a feature.

Asking for another language covers `title`, `notes` and `text`, and needs no
storage: the next entry follows the corpus rule on its own. Switching applies to
new entries only — existing ones are the project's own words and the tool does
not rewrite them.

Four words stay English: `when`, `while`, `if`, `where`. Not a field-level rule
— only the FIRST word of a `condition` is constrained, so `when 앱이 종료될 때`
is valid, and `title`/`notes`/`text` have no validation at all. They are fixed
because the gate matches them literally and EARS is a published notation; the
same reason a language does not translate `if`.

Landed in the AGENTS.md block rather than the CLAUDE.md one (21 characters of
headroom against a ceiling asserted in three places) or a persona prompt (3
characters, and it reaches only hosts that install the plugin). AGENTS.md is
generated into every adopter repository and read by the agent that writes the
spec.

Guard tests assert each rule against the rendered block, including the
mixed-language example — a reader who cannot see that only the trigger is fixed
will over- or under-translate. Mutation-checked: replacing the corpus rule with
"whatever language the user speaks", or dropping the example, each fails.

Verified outside the repo: tarball built in a clean clone, installed into an
empty project, and `clad update` run against real adopter clones. Both receive
the rules, and applying them by hand yields Korean for the Korean corpus and
English for the English one, with zero non-English triggers in either.

2815 tests green, `clad check --tier=pre-push --strict` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Eleven version sites via `npm run version-bump`, the lockfile regenerated so
`npm ci` resolves, and the built mirrors refreshed. The changelog section is
dated, and the record row in all six README variants now says v0.9.3 — it had
been carrying this release's numbers under the previous release's label.

2815 tests green, `npm ci` clean from a fresh clone,
`clad check --tier=pre-push --strict` exits 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stop scanning build output for import cycles; diff the acceptance criteria
@qwerfunch
qwerfunch merged commit 23ea6be into main Aug 4, 2026
2 checks passed
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