v3.5.4 - the gate that clears itself wrong #217

Merged
john merged 53 commits from cadence/v3.5.4 into main 2026-08-18 19:15:05 +00:00
Owner

Closes out the v3.5.4 cycle: eight requirements, three phases, 53 commits.

  • EXP-02, PHS-01: a credential the 4096-byte sanitize window cut before its
    @ is now redacted instead of shipped to a review provider, and
    cad-phase remove refuses a git state it could not read rather than
    classifying it clean and deleting phases/<N>/ recursively.
  • DRF-01, DRF-02, TAG-01: /cad-audit stopped FAILing a healthy repository
    on three counts - a wrapped continuation line out-declaring the milestone,
    version_drift firing on the rolled-forward phase the workflow declares
    exempt, and tag discovery walking upward out of the caller's own project
    root.
  • RVW-03, REL-01, ISS-01: the stakes level moves both halves of a cross-model
    review panel, git.create_tag governs the land-time tag cut alone, and
    issue-check's per-issue resolve loop runs under one wall-clock budget.

Every fix carries a check watched failing against the unpatched tree first.
Audit green (8/8 traced, 21/21 acceptance criteria covered), full suite 2218
passing, self-verify clean. Manifest at 3.5.4; the v3.5.4 tag is cut on main
after this merges.

Closes out the v3.5.4 cycle: eight requirements, three phases, 53 commits. - EXP-02, PHS-01: a credential the 4096-byte sanitize window cut before its `@` is now redacted instead of shipped to a review provider, and `cad-phase remove` refuses a git state it could not read rather than classifying it clean and deleting `phases/<N>/` recursively. - DRF-01, DRF-02, TAG-01: `/cad-audit` stopped FAILing a healthy repository on three counts - a wrapped continuation line out-declaring the milestone, `version_drift` firing on the rolled-forward phase the workflow declares exempt, and tag discovery walking upward out of the caller's own project root. - RVW-03, REL-01, ISS-01: the stakes level moves both halves of a cross-model review panel, `git.create_tag` governs the land-time tag cut alone, and issue-check's per-issue resolve loop runs under one wall-clock budget. Every fix carries a check watched failing against the unpatched tree first. Audit green (8/8 traced, 21/21 acceptance criteria covered), full suite 2218 passing, self-verify clean. Manifest at 3.5.4; the v3.5.4 tag is cut on main after this merges.
john added 53 commits 2026-08-18 19:13:43 +00:00
The prune left the Overview describing v3.5.3; it now describes this cycle.

Phase 1 carries the two whose wrong answer destroys something (#215, #136),
phase 2 the ship gate that FAILs correct docs (#210, #211, #214), phase 3 the
three flags that misdescribe their own reach (#207, #176, #181). Four new
requirement ids seeded for the issues that had none: PHS-01, RVW-03, REL-01
and ISS-01.

#187 is not a phase - this cycle satisfies it by citing issue numbers in its
own commits, which is what the v3.5.3 land could not do.
The PHS-01 guard refused an unreadable git state at the pre-flight and at the
rmSync fallback, but the classifier underneath both answered ABSENT in three
states where a repository was present:

- GIT_DIR/GIT_WORK_TREE select a repository with no lexical `.git` above the
  work tree, so the walk found nothing while an object store held the only copy.
- A probe that errored (EACCES on an unsearchable ancestor, EIO, ESTALE) took
  the same permissive arm as a probe that found nothing, so the guard itself
  failed open.
- The walk starts at the planning root and looks UP, so a repository rooted
  inside phases/<N> was invisible to it and rmSync took the nested object store
  along with the directory.

All three now answer PRESENT: the environment is read before the walk, a failed
probe is a repository we could not rule out rather than one we ruled out, and
gitDirUnder carries the nested half at the fallback. Two falsifiers cover the
first and third, both watched failing at ffeaa3f; the source row is widened to
pin the nested probe ahead of the delete as well.
The re-armed review found the two halves of the delete guard disagreeing about
what `.git` matches. `gitDirAbove` has always probed with lstat, which inherits
the filesystem's own case semantics; `gitDirUnder` compared readdir entry names,
which is case-sensitive whatever the filesystem underneath does. An admin
directory stored as `.GIT` on APFS or NTFS still resolves for git and was
scanned straight past, so the nested object store went with the phase directory.

Probing per directory closes it and drops the special-case branch. Recursion
stays on `isDirectory()`, which is lstat-shaped, so a symlink is never followed
and the scan cannot leave the phase directory. A source row pins the mechanism,
since the failing state needs a case-insensitive filesystem the suite cannot
conjure on Linux.
Both requirements delivered with a falsifier watched failing at ae73dd6, plus
the blocking risk_surface review's own findings closed at fec446e and c0b0d04.
The settled survivor file rides along: it is the only producer /cad-land's
unattended close has, and an uncommitted one would let a later close merge over
findings nobody halted on.
The two PHS-01 falsifiers written during the blocking `risk_surface` round
had no `WATCHED FAILING AT` header - the convention rides plan tasks, and the
gate round runs outside that loop, so nothing carried it to them. The watch
itself was real; it was the record that lived only in SUMMARY.md and
fec446e's message, neither of which travels with the test file.

Re-ran both at ae73dd6 in a scratch worktree and recorded what they showed:
each returns `ok:true` with `{"rm":"phases/3"}` among its ops there. Header
names ae73dd6, the sha observed, rather than the ffeaa3f SUMMARY asserted;
corrected SUMMARY to match.

Test comments and one sha. No behavior change.
The plan gate found D-02 and AC5 unsatisfiable as written; both are amended in
CONTEXT.md and the plan Notes record why.
activeVersion ran both scans over a truncated body: it returned the first
line-anchored version token anywhere in `### Active` and computed its loose
fallback only over the lines above that match. A token markdown wrapping left
at a line start therefore out-declared a milestone named correctly in the
section's opening sentence, and version_drift compared the wrong version
against the tag list - the 81bdb5d shape, which hard-FAILs the ship gate on
docs that are right.

Both scans now run over the whole body, and an anchored candidate is admitted
as the declaration only when it agrees with the body's first token or its line
opens a sentence rather than continuing a wrapped one. A rejected candidate
contributes nothing, so the earlier correct mention answers (D-02). The reader
is shared, so git-branch decide's naming and its already-published refusal move
with it (D-01).
The pin asserted the two-scan agreement property and then told the reader to
fix PROJECT.md, stating that activeVersion() and DECLARED_VERSION_RE are not
changed and that the file is what moves when it goes red. DRF-01 moved the
property into the reader, so both sentences described code that no longer
exists.

The comment now states the admission rule the reader carries and what the one
remaining red shape is - a sentence-opening admission disagreeing with the
body's first token - and the failure message routes the remedy to the reader's
rule in lib/branch-decision.mjs and this pin derived from it. Phase 2 D-03,
reversing the pin's own D-07 policy. Mechanics unchanged.
`git -C <dir>` discovers a repository UPWARD, and the audit asks the tag
question from `.planning` - a directory that never holds `.git` - so a project
that is not itself a repository read the tag list of whatever repository
contained it. An unrelated umbrella repo's release could FAIL the ship gate of
a project that published nothing, and could refuse it an integration branch.

readTags now takes the project root the answer must belong to, probes
`rev-parse --show-toplevel` and answers [] unless that toplevel is the root or
sits inside it, compared segment-wise on realpaths so a symlinked checkout
(/code -> /data/code here) is not read as a different project. Each caller
derives its own root (D-07): git-branch decide already holds it, cmdAudit's dir
IS the planning root so its project is the parent. Every refusal stays
permissive at [] with no new reason (D-08). A linked worktree still reads the
shared tags - its toplevel is the worktree root - and a fixture pins it.
The interrupted-close exemption read phase artifacts only, and a close is
sanctioned to carry work forward - milestone.md names the state. Such a phase
is byte-identical on disk to one still being worked, so version_drift fired in
the state audit.md declares exempt, with the remedy it offers ('complete the
close so no phase is left open') unreachable by construction.

The requirement rows are the only surface carrying the answer (D-04): rolling a
phase forward marks its rows Deferred, which the close already writes and this
audit already excludes from breaks. A phase whose rows are ALL Deferred stops
holding the cycle open; rows still Pending keep the gate armed, and a phase with
no rows at all is not exempt - vacuous truth there is the ordinary unplanned
mid-cycle state #87 fires on. Read off the rows parseRequirements already
produced, no second file read.
The version_drift bullet offered two exits, and the second - complete the close
so no phase is left open - is unreachable for a phase deliberately rolled
forward, which is the state milestone.md treats as sanctioned. The bullet now
names the third exit the seam implements: roll the work forward, honoured once
every Traceability row naming that phase is Deferred.

audit.md's weight-budgets pin moves with it in the same commit (D-11) - the
budget check is blocking in both directions.
PHS-01, RVW-03, REL-01 and ISS-01 were authored as single 247-290 char
lines while every other bullet wraps at the file's ~79 col width. The
milestone-prune corpus test asserts every moved bullet spans more than
one line, so PHS-01 - phase 1 being complete - failed the suite. Prose
is byte-identical; only the line breaks changed.
A repo config layer arrives with a clone, and its per-trigger tier and
effort were returned verbatim on the resolve line - the two fields
review-triggers.md step 4 interpolates into the provider command, which
review-provider.mjs sends unvalidated. Same treatment as a bad gate:
warn by name, let the level's value stand.
Step 2 gave two incompatible rules: the skip fired only on empty tags
AND no version, while the ok:false halt said STOP - so a tagged project
whose user declined the proposal died at the bump instead of skipping
it. Now no confirmed version alone skips the bump, the bump never runs
without --version, and the tags probe moves ahead of the proposal so a
never-published project is not pressed toward a release.
AC2's read half asked `config.mjs get review.triggers.plan.tier` to report
the value the resolver uses at the effective stakes level. D-04 put that key
on the `null` unset sentinel and bin/config.mjs:288-291 states the matching
refusal outright: that seam does not know the stakes level and never reads
route-table.json, so answering as if it did is the same defect pointed the
other way.

What shipped is the `.gate` shape - `null` plus a warning naming
`route.mjs resolve` as the answering seam - and that is the fix for the fixed
`flagship` the criterion was written against. AC2 now states it, with the
rewording dated in place; the grids half is unchanged and held on first
check. SUMMARY records the deviation.
chore: pin cad-planner start rung at xhigh
All checks were successful
test / node-test (git, 22) (pull_request) Successful in 29s
test / node-test (git, 24) (pull_request) Successful in 28s
test / node-test (other, 22) (pull_request) Successful in 28s
test / node-test (other, 24) (pull_request) Successful in 30s
test / node-test (planning, 22) (pull_request) Successful in 1m24s
test / node-test (planning, 24) (pull_request) Successful in 1m26s
test / node-test (prose, 22) (pull_request) Successful in 35s
test / node-test (prose, 24) (pull_request) Successful in 33s
test / node-test (review, 22) (pull_request) Successful in 17s
test / node-test (review, 24) (pull_request) Successful in 24s
test / node-test (routing, 22) (pull_request) Successful in 34s
test / node-test (routing, 24) (pull_request) Successful in 31s
test / self-verify (pull_request) Successful in 13s
test / typecheck (pull_request) Successful in 19s
b20e5b3889
john merged commit 74c15fa332 into main 2026-08-18 19:15:05 +00:00
Sign in to join this conversation.
No description provided.