v3.5.2 - one reader, one transport #196

Merged
john merged 34 commits from cadence/v3.5.2 into main 2026-08-16 16:39:37 +00:00
Owner

Closes #133, closes #132.

Two surfaces where the tree already conceded the correct rule in one place and
prescribed the wrong one somewhere else. Neither is a bug anyone hit yet. Both
came out of an external deep dive against v3.3.0 and were adjudicated
AGREE-high.

TRN-01 (phase 1) - caller-derived free text now rides a file path at every
seam flag that carries it. Five new flags behind one reader
(lib/text-flag-file.mjs, four refusals), the rule stated once in
references/conventions.md, and a committed 36-row register
(lib/text-transport.mjs) that self-verify check 19 reads, so a seventeenth
inline site is refused rather than noticed later. 13 prose surfaces converted.

LSE-01 (phase 2) - plan-overlap and lease-check reach containment
through one predicate in lib/lease-grammar.mjs, so the pre-flight overlap
gate can no longer admit a plan pair the commit-time enforcement would refuse.
./a.txt and src//a.txt are refused with a named diagnostic at both
declaration doors, and a census test reddens if the comparison is pasted back
anywhere under cadence-core/bin/.

Manifest bumped 3.5.1 -> 3.5.2 with the CHANGELOG entry. Phases 1-2 pruned,
v3.5.3 opened. The v3.5.2 tag is cut after this merges, on the pulled base.

34 commits. Full suite 2012 pass / 0 fail, self-verify clean across 23 checks.

Closes #133, closes #132. Two surfaces where the tree already conceded the correct rule in one place and prescribed the wrong one somewhere else. Neither is a bug anyone hit yet. Both came out of an external deep dive against v3.3.0 and were adjudicated AGREE-high. **TRN-01 (phase 1)** - caller-derived free text now rides a file path at every seam flag that carries it. Five new flags behind one reader (`lib/text-flag-file.mjs`, four refusals), the rule stated once in `references/conventions.md`, and a committed 36-row register (`lib/text-transport.mjs`) that `self-verify` check 19 reads, so a seventeenth inline site is refused rather than noticed later. 13 prose surfaces converted. **LSE-01 (phase 2)** - `plan-overlap` and `lease-check` reach containment through one predicate in `lib/lease-grammar.mjs`, so the pre-flight overlap gate can no longer admit a plan pair the commit-time enforcement would refuse. `./a.txt` and `src//a.txt` are refused with a named diagnostic at both declaration doors, and a census test reddens if the comparison is pasted back anywhere under `cadence-core/bin/`. Manifest bumped 3.5.1 -> 3.5.2 with the CHANGELOG entry. Phases 1-2 pruned, v3.5.3 opened. The v3.5.2 tag is cut after this merges, on the pulled base. 34 commits. Full suite 2012 pass / 0 fail, `self-verify` clean across 23 checks.
john added 34 commits 2026-08-16 16:39:06 +00:00
The arm reads the live .planning/ROADMAP.md and REQUIREMENTS.md and asserted
that at least one requirement moved. milestone.md step 3 prunes the roadmap
empty at every close, so from the moment a milestone closes until the next
phase is verified there is no completed phase to move and the suite is red.
That window is exactly when someone installs the release and runs the tests.

An empty roadmap is a state of the repository, not a result, so the arm now
skips with the reason said out loud. The assertion that survives is the one
that is actually about the transform: a roadmap naming a completed phase must
move its requirements. Checked against 64891c4's pre-prune bytes, the arm
still moves AUT-01, AUT-02, PRN-01 and TRK-01 with zero orphans and five
unescaped pipes per row.
Phase 1 (TRN-01) states the shell transport rule once and applies it at
every site carrying caller-derived text, with a self-verify check for the
seventeenth. Phase 2 (LSE-01) puts plan-overlap and lease-check behind one
lease-normalization module so the pre-flight gate cannot admit a pair the
enforcement would refuse. Independent phases, disjoint files.

The ROADMAP overview still described v3.5.1; the milestone evolve (87906d8)
rewrote PROJECT.md and REQUIREMENTS.md and never touched it. Repaired here
rather than left to contradict the cycle it now holds.
One frozen module under cadence-core/bin/lib/ holding both the enumeration -
one row per examined site: surface, flag, the value it passes verbatim, whether
that value is caller-derived, and a reason on every row that is not - and the
pure rule self-verify runs over it as check 19.

The key is {surface, flag, value}, not a line number: line numbers rot, and the
classification is a property of the value, which is also what keeps progress.md's
composed --next apart from the literal one five lines below it. A row outlives
its occurrence, so a converted site's row is what makes the inline form coming
back a reported problem rather than an unclassified one.

The discriminator is the immediately following quoted value, so prose that
NAMES a flag in order to forbid it needs no row. What the scan cannot delimit -
an unquoted placeholder, a value that closes on the next line, a flag ending its
line - is settled by a register row for that surface and flag, and REPORTED as
its own kind when none exists. Not built on check 2's invocation parser: thirty
of the qualifying mentions sit in prose fragments it skips.

git tag -m is watched too, on a git tag line only: it is the one site that
provably puts a PROJECT.md milestone name into a double-quoted shell word.

self-verify exits 1 at this commit, naming 21 live sites. That run is the
phase's watched-FAIL evidence and the prose fix lands next.
The derivation test in one place, in that file's own one-line-rules voice: is
the value derived from agent output or repository content, rather than authored
by the workflow itself? If yes it reaches the seam as a PATH, because a
double-quoted shell word carrying $(...) or a backtick executes before Node
starts and a path cannot. If no it stays inline, as it does for a human typing
at a shell whose text is their own.

Cited by path, never @-included: conventions.md already describes itself as
rules cited where they apply, so this adds no eager bytes, no include-consumer
sentence and no deferred-read row. The budget row is re-pinned in this commit -
the file sat at exactly 0 B headroom.

The third bullet is what keeps the rule from becoming per-edit judgement: the
site list lives in lib/text-transport.mjs and an unclassified site is reported,
so a new one is registered rather than argued.
The composed-value sites the phase exists for now write their value to a
scratch file and pass the PATH: the empty-or-unmarked close details in
plan.md and plan-revision.md, the `<what failed>` details in
decision-review.md, minimalism-review.md and review-triggers.md, the
adjudication detail whose voice list the model composes, and the two
dispatch read-sets resolved from the user's own argument. Each cites
references/conventions.md for why, in one clause - the reasoning stays
stated once.

The trace census parser learns the file spelling. Its rule is that a
dispatch NAMES what it caused, and `--read-file <path>` satisfies that
exactly as `--read "..."` did; the inline reader requires whitespace
after `read`, so it can never match `--read-file` and the fallback is
unambiguous.
Three close details are the worker's own words about how it failed, so
each writes them to a scratch file and passes the PATH, citing
references/conventions.md for why.

The arm rule stays stated once. `seams.md` is where dispatch sites are
pointed for "the seam picks the arm off the detail", so that paragraph
now names both flags that can carry one: the arm turns on whether a
detail was given at all, never on which spelling gave it. The OMIT
sentences at the two sites keep their meaning against both transports.
The three sites that record a failing item now write the free-text
fields as a JSON object to one scratch file and pass the PATH: the
model-composed evidence in pass 1, the user's verbatim reply and reason
in pass 2, and the root cause at the diagnosis. `--phase`, `--item`,
`--result`, `--severity` and `--source` stay inline on the same call -
the seam validates each against a closed set, so no transport protects
them from anything.

ONE file, not one per field: a failed item costs one extra Write, which
is what keeps the per-item round-trip discipline this workflow states.
It is still one `uat record` per item, and a fields file is not a
`uat merge` payload.
One value, two sites. The prune takes `--label-file <path>`, and the
release tag is cut with `git tag -a <version> -F <path>` - the one place
in the tree that provably put repository content, a PROJECT.md milestone
name, inside a double-quoted shell word.

What the label IS is unchanged (the version on a release, else the
milestone name), and so is everything else about the tag step: the
`git.create_tag` condition, the tag-membership test, the separate ask
before any push, and the never-auto-push-a-tag rail.
The pause note and the routed action are the user's own sentence carried
into a shell word, so both sites take `--next-file <path>`. The seven
literal `--next "/cad-<command> N"` sites are untouched: their value is a
slash command the workflow authors, and a transport protects it from
nothing.

With that, check 19 is green on the whole tree, so the assertion the
plan has been building toward can land: self-verify run against the REPO
with an empty list for each of the three transport codes. A site that
goes back to the inline form now reddens `node --test` as well as the
linter.
AC1 says a converted site names the action and cites the path; the tag
step had kept a clause of the reasoning itself. What the value IS - a
PROJECT.md milestone name - stays, because that is the classification
the citation resolves.
`plan-overlap` intersects declared `files:` by exact string equality while
`lease-check` reads a trailing slash as a directory prefix, so a phase
declaring `src/` in one plan and `src/auth.js` in another produces an empty
overlap, passes the parallel-safety gate, and is then refused at the commit
step.

Two cases against the existing `overlapTree` fixture pin both defective
pairs - `src/` against `src/auth.js`, and `src/` against `src/auth/`. Both
spellings ride `overlaps[].files` as separate strings; the emission order is
deliberately not pinned here, because it is the fix's to settle and pinning
it now would fail these cases for a second, unrelated reason.

This commit carries the tests and NOT the fix on purpose: it is the phase's
watched-FAIL evidence, and `node --test cadence-core/bin/planning.test.mjs`
is red at this SHA by design.
`lib/lease-grammar.mjs` is the single statement of the lease grammar:
`covers(declaration, path)` - a declaration ending in `/` is a directory
lease covering every path beneath it, every other declaration covers only
the byte-identical path - and the derived
`intersects(a, b) = covers(a, b) || covers(b, a)` the pre-flight gate needs,
because either side of a declaration pair may be the containing lease.

The non-substring arm is deliberate: `src/auth` never licenses another
plan's `src/authority.js`. Non-path strings answer instead of throwing, the
task-line arm unions the raw annotated form as a matter of course. Symlinks,
case folding and `..` traversal are stated in the header as deliberately not
normalized, each with its reason, and the module touches no filesystem.

`cmdPlanOverlap`'s exact-equality intersection becomes a pairwise walk
through `intersects`. A collision emits BOTH spellings as separate strings,
an exact match still contributes one, and the order is plan i's declarations
then plan j's, so the output is the same on every run.

Closes the two cases 328ce76 left red.
`cmdLeaseCheck`'s local `exact`/`prefixes` split is replaced by one `covers`
call per staged path, so the enforcement step and the pre-flight gate can no
longer hold different ideas of what a declaration licenses.

The grammar prose moves with it: the block comment that restated the
directory-lease rule here is gone, because the second copy is what the two
seams drifted through, and the header's claim that "the reader is
`parsePlanFiles`, the SAME one cmdPlanOverlap uses, so a path the pre-flight
overlap gate admitted cannot be refused here" is corrected to say what was
actually true - that held of the parse and not of the comparison until now.

Nothing else in the seam moves. The staged side is still NOT re-normalized,
the report-file exemption, the rename's both-sides read and the
`unrepresentable-paths` refusal are untouched, and `staged` and `declared`
still count what was parsed. Two cases beside the shipped ones pin that: a
two-file clean lease still reporting 2 and 2, and an empty `files:` list
still refused as `undeclared-files` with exit 1.
`./a.txt`, `src/./a.txt` and `src//a.txt` each name the same file as their
plain spelling, and the lease grammar reads them as a different file - so
one file enters the declared set under two strings and the gate that is
supposed to prove two plans independent stops being able to.

`isRefusedSpelling` states the three forms and nothing else (`..` is an
out-of-repo-lease defect, a different question, and widening the rule there
would refuse declarations naming real files). `parsePlanFiles` calls it on
BOTH arms of the declared-files union - the frontmatter `files:` items and
each comma element of a `- **Files:**` task line, raw twin included - so the
spelling cannot enter through the second door. The declaration is DROPPED
and files one `redundant-path-segment` issue on the array both envelopes
already carry.

Refusal, not normalization: normalizing can only WIDEN a lease, and 0 of 613
declared entries across 47 shipped plan files use either form.

The line number is the declaration's own. The frontmatter arm scans only the
`files:` list's item lines, with a cursor advancing past each match so a
spelling written twice reports two lines rather than one twice, and the
task-line arm measures to the `**Files:**` marker - the pattern's leading
`\s*` matches newlines, so its match offset names an earlier line than the
declaration is written on.

The rule is deliberately NOT pushed down into `parseFrontmatter`: that pass
serves `requirements:` too, where these characters are not path syntax.
`plan-frontmatter.md` said what a plan DECLARED and never what a declaration
LICENSES, so the rule the two seams disagreed about was not in any
user-facing surface to disagree with. It has its own section now: a trailing
slash is a directory lease covering every path beneath it, honoured
identically by `plan-overlap` and `lease-check` through one module; every
other declaration matches only the byte-identical path, so `src/auth` does
not license `src/authority.js`; and symlinks, case and `..` traversal are
each stated as deliberately not normalized with the reason.

The three refused spellings and `redundant-path-segment`'s table row land
with it, and `## What is out of scope` is amended: the task-line arm is
outside this PARSING grammar, which is what that paragraph was always about,
and the lease-spelling refusal is the one rule that deliberately spans both
arms.

The `weight-budgets.json` re-pin rides this same commit by necessity - the
file sat at 13,954 B against a 13,954 B budget, so any addition lands
self-verify red without it. Set to the new exact count, the convention every
row here follows.
The correction this phase makes is only worth what stops it coming back. A
fifth `HELPERS` row censuses the lease grammar's containment body, tree-wide
over every `.mjs` under `cadence-core/bin/` - `lib/` and test files included,
which is where this tree's recorded failure mode actually lives.

It matches the BODY IDIOM of the directory-lease arm, not a name and not a
call site, so a paste-back under a new name is still a copy and still
reddens. Proven live rather than assumed: copying the module to
`lib/census-probe.mjs` fails the row naming both files.

`lib/lease-grammar.mjs` joins the named-file list in the walk case too, so a
moved or renamed module fails loudly instead of leaving the new arm vacuous.
docs: open v3.5.3, the bounds the review path never stated
All checks were successful
test / node-test (git, 22) (pull_request) Successful in 22s
test / node-test (git, 24) (pull_request) Successful in 23s
test / node-test (other, 22) (pull_request) Successful in 23s
test / node-test (other, 24) (pull_request) Successful in 22s
test / node-test (planning, 22) (pull_request) Successful in 1m11s
test / node-test (planning, 24) (pull_request) Successful in 1m11s
test / node-test (prose, 22) (pull_request) Successful in 26s
test / node-test (prose, 24) (pull_request) Successful in 26s
test / node-test (review, 22) (pull_request) Successful in 16s
test / node-test (review, 24) (pull_request) Successful in 15s
test / node-test (routing, 22) (pull_request) Successful in 26s
test / node-test (routing, 24) (pull_request) Successful in 29s
test / self-verify (pull_request) Successful in 14s
test / typecheck (pull_request) Successful in 17s
9bf40b4625
john merged commit babde6638a into main 2026-08-16 16:39:37 +00:00
Sign in to join this conversation.
No description provided.