v3.5.7 - measured, and no lever to change it #227

Merged
john merged 86 commits from cadence/v3.5.7 into main 2026-08-22 11:45:37 +00:00
Owner

Cadence has been measuring its own cost for several releases and handing you
nothing to spend the measurement with. That is the theme. Four phases, 83
commits off v3.5.6, four requirement ids seeded at the open and all four traced
to a verified phase.

/cad-audit PASS on both arms: 4 of 4 requirements traced requirement to phase
to plan to verified, 14 of 14 acceptance criteria covered, zero breaks.

What shipped:

  • stakes is the minimum a project accepts, not the level every phase pays.
    route.mjs resolve reads the phase's own declared files: at plan time and
    raises from that floor. route.mjs replay over this repo's 30 phases: 27
    raise back to shipped on real evidence, 2 take the discount, 1 is withheld
    because a declared path was not readable, which is the fail-closed direction.
    (CER-01, closes #189)
  • A fifth gate mode deferred, one position between advisory and blocking.
    It runs its reviewer, queues what it found as a committed DEFERRED-*.json,
    and lets the phase finish. The guarantee moves to the land, where /cad-land
    refuses on both publish arms while any member is unadjudicated. (HLT-01,
    closes #193)
  • /cad-config --surfaces gives the risk-surface interview a way back in
    against a fresh scan, and the menu that rendered all eight categories twice
    now renders two distinct sets. (IVW-01, closes #206)
  • trace suggest opens .planning/reads.jsonl. A role over its floor produces
    an entry naming the worst single file inside one dispatch: on this repo,
    cad-executor at 3.64 opens per distinct file, worst case planning.mjs
    read 29 times in one bracket. The entry names no config key, because none
    exists, and says so. (RDX-01, closes #167)

Two spikes ran before any phase was planned, which is why this cycle is one
phase shorter than it was scoped to be.

BCH-01 (#174) was killed by its own spike. Batching N security reviews into
one process saves 1.91% of reviewer spend, a 1,676-token fixed prefix against
six dispatches totalling 438,080 tokens, and it does not flip at the 61
invocations the issue cites because both sides of the ratio scale with N. The
per-commit scoping #174 correctly names as a real cost would have been traded
for a rounding error. Moved to ## Deferred carrying the verdict rather than
dropped. Closes #174.

The other spike narrowed #167 before it was planned: the 7.0x figure the issue
carries was measured over declared read-sets, and the observed in-dispatch
figure is 3.64.

Full notes in CHANGELOG.md under [3.5.7].

Cadence has been measuring its own cost for several releases and handing you nothing to spend the measurement with. That is the theme. Four phases, 83 commits off v3.5.6, four requirement ids seeded at the open and all four traced to a verified phase. `/cad-audit` PASS on both arms: 4 of 4 requirements traced requirement to phase to plan to verified, 14 of 14 acceptance criteria covered, zero breaks. What shipped: - `stakes` is the minimum a project accepts, not the level every phase pays. `route.mjs resolve` reads the phase's own declared `files:` at plan time and raises from that floor. `route.mjs replay` over this repo's 30 phases: 27 raise back to `shipped` on real evidence, 2 take the discount, 1 is withheld because a declared path was not readable, which is the fail-closed direction. (CER-01, closes #189) - A fifth gate mode `deferred`, one position between `advisory` and `blocking`. It runs its reviewer, queues what it found as a committed `DEFERRED-*.json`, and lets the phase finish. The guarantee moves to the land, where `/cad-land` refuses on both publish arms while any member is unadjudicated. (HLT-01, closes #193) - `/cad-config --surfaces` gives the risk-surface interview a way back in against a fresh scan, and the menu that rendered all eight categories twice now renders two distinct sets. (IVW-01, closes #206) - `trace suggest` opens `.planning/reads.jsonl`. A role over its floor produces an entry naming the worst single file inside one dispatch: on this repo, `cad-executor` at 3.64 opens per distinct file, worst case `planning.mjs` read 29 times in one bracket. The entry names no config key, because none exists, and says so. (RDX-01, closes #167) Two spikes ran before any phase was planned, which is why this cycle is one phase shorter than it was scoped to be. `BCH-01` (#174) was killed by its own spike. Batching N security reviews into one process saves 1.91% of reviewer spend, a 1,676-token fixed prefix against six dispatches totalling 438,080 tokens, and it does not flip at the 61 invocations the issue cites because both sides of the ratio scale with N. The per-commit scoping #174 correctly names as a real cost would have been traded for a rounding error. Moved to `## Deferred` carrying the verdict rather than dropped. Closes #174. The other spike narrowed #167 before it was planned: the 7.0x figure the issue carries was measured over declared read-sets, and the observed in-dispatch figure is 3.64. Full notes in CHANGELOG.md under [3.5.7].
john added 86 commits 2026-08-22 11:44:25 +00:00
The one-time risk-surface question composed its options from three prose
bullets per run, and the last bullet restated the first: all eight categories
appeared in slot 1 and again in the last slot (#206). A composed list is
unprovable, so distinctness becomes a property of a value.

interviewOptions() turns a scanTree result plus the set a config layer already
answered into the ordered choices the ask offers, each carrying the set it
would write and the reason to state beside it. The recommendation is all eight
on both scan arms - inconclusive changes only the REASON - and an already
answered project gets the union with what the scan now evidences as its second
choice, which is the added-Stripe-six-months-in case the arm exists for. Empty
and repeated sets drop, every set is built in CATEGORIES order so two spellings
compare equal, and the list never exceeds the ask-user seam's option cap.
The seam now emits `options` beside the fields it already reports, under the
same always-report convention: an empty tree still gets its one all-eight
choice, so "the structure evidences nothing" stays distinguishable from
"nobody built a list".

`--answered <a,b,c>` carries the set a config layer already holds, so a
re-entrant ask reaches the same option rule the first fire does rather than
merging the current answer itself. It reads the way `risk-check run` reads
`--surfaces` - split, trim, drop empties, refuse a token outside the eight by
name - and carries a declared row in arg-contract's CONTRACTS, without which
self-verify files an unknown-flag against every prose site that uses it.

The union choice is built only when the scan evidences something the answer
does not already cover: with an empty gap that set IS the answered set, and
keeping it would offer two choices reading as one, which is the #206 shape.

The #206 demo tree is a fixture now - Express, Stripe, Prisma, Passport, the
four category directories, a .sql file and an openapi.yaml - and it evidences
exactly six, offers two options whose sets differ, and puts the six-plus-secrets
union second when secrets is already answered.
Three bullets told a model to recommend the scan's `recommended` array and
then "fill the remaining slots with ... the evidenced categories alone, and all
eight" - so the first option and the last one were the same eight categories
(#206). The two recommendation arms were already one arm in the code:
`recommended` is all eight either way, and `inconclusive` changes only the
reason stated.

The section now renders the `options` array `detect-surfaces` returns, in the
order it arrives, and composes nothing. The seam's three binding rules are
stated where the ask happens - the four-option cap, recommended first and
labelled, and that the label is a display convention and never a pre-selection
- and D-14's refusal to narrow the recommendation on evidence that does not
exist is unchanged.

review-triggers.md's weight budget re-pinned to the grown size.
The only configuration question Cadence asks on its own was asked once, at the
first risk_surface fire, and there was no way back to it - a project that added
Stripe six months after answering had no door to the answer and no view of the
evidence against it (#206).

The new arm reads the effective answer through config.mjs get (never a raw read
of .planning/config.json for a workflow value), runs detect-surfaces --root .
with --answered carrying it, shows the answered set beside what the structure
evidences NOW with each signal named, and calls out every evidenced category
the answer does not cover - that gap is what the arm exists for. It then renders
the envelope's options in order under the ask-user seam's three rules: four per
question, the first labelled (recommended), and the label a display convention
and never a pre-selection.

It writes ONLY on an explicit pick, through the Validation seam at the repo
layer. A decline calls no set and edits no file, so re-entering the arm can
never cost a user the answer they already gave.

The catalog row stops offering a free-typed list and routes here instead: a
second door writing this key with no evidence beside it is the shape this
closes. Interactive menu headings and numbering are untouched (deferred-reads
anchors cad-config at Interactive menu (no args)/The walk/2). Budgets re-pinned
for every grown row.
Task 2's declared `detect-surfaces --answered` row moves the CONTRACTS
table from 156 flag entries to 157, and the census assertion at
cadence-core/bin/arg-contract.test.mjs:297 pins the old count. The pin is
designed to redden on exactly this and be bumped deliberately, so the file
joins the plan's declared files rather than the count being redefined
quietly or the bump riding an undeclared path.
With `held` empty the union candidate carries the same set as the
evidenced-only candidate, and the dedup keeps the first, so a project
that had answered nothing was offered the evidenced categories under
"the answered set plus what the scan now evidences beyond it". That is
the first-fire path, so it is the sentence most users read.

Widen the guard to `held.length && gap.length`, which is what the
ORDER comment already meant by "with nothing answered, 2 collapses onto
4". Pinned by a test that fails against the old guard naming the
phantom answered set.
The clause naming every evidenced category the answered set does not
cover was prose only - no check held it and no scenario would notice
its absence. It cannot join the shared RULES list, which runs against
all three ask-user sites and neither of the other two states it, so it
gets an assertion scoped to workflows/config.md's `## Risk surfaces`
section. Fails naming the clause when the sentence is deleted.
The fifth gate mode needs a name before it can have behaviour. `deferred`
joins the `values` array of all four `review.triggers.<t>.gate` schema keys,
`route-table.json`'s `gates` and route.mjs's `DEFAULT_GATES` fallback, the
same five names in the same order in all three - `vocabularyIssues` compares
the table against the schema enum element-for-element, so anything less is
`gate-vocabulary-drift`.

The position is a decision, not a formality: `gates` is the ordered ladder
`oneStepDown` walks, so a `blocking` gate whose fires keep coming back empty
is now proposed down to a mode that still stops the LAND rather than to
`advisory`, which stops nothing. The seam arm in trace-suggest.test.mjs reads
the shipped table, so moving `deferred` reddens it.

The `review` GRID does not move (D-03) - no stakes level fires `deferred`
by default, and phase 3 owns what a level decides about gates.
A gate resolved `deferred` settles by QUEUING what it found rather than by
adjudicating it, and none of the four older outcome names describes that.
It cannot borrow one either: `gate_pass` reads as a clean gate in every
downstream recount, and `override` is the coordinator's own say-so - the
manufactured clear the receipt machinery exists to refuse. Without a receipt
of its own, `cmdRiskCheckStatus` reports the matched range `unfired`
forever and the run halts at exactly the step deferring it was meant to let
through, so the block comment that ended "A FIFTH name would be a state
nothing produces" now names the state and what produces it.

`deferral` gets no special case beside `override`'s empty-reason refusal -
it is a settled review outcome and is exactly as joinable as `gate_pass`,
needing the same `--trigger`, `--plan`, `--base` and `--sha` to settle a
range through `settledBy`.

Two arms in planning.test.mjs run the real seams end to end over a risky
range: a `deferral` receipt moves the plan's row from `unfired` to
`recorded`, and an `outcome` event whose only defect is a name outside the
accepted set settles nothing. Removing `deferral` from FIRE_RECEIPTS reddens
both.
The artifact that makes "stops the LAND, not the RUN" true: a committed
`DEFERRED-<trigger>-<discriminator>[-r<round>].json` beside the sibling
REVIEW file, holding the reviewer's findings VERBATIM. Verbatim rather than
counted because /cad-milestone deletes that REVIEW file - a member whose
bodies lived only there names a number nobody can triage. Committed because
`.planning/trace.jsonl` is gitignored and renderTrace drops a phase's events
at sign-off, so a trace-resident queue evaporates on a fresh clone (D-01).

Split the way lib/adjudication-record.mjs and cmdAdjudication already are:
lib/deferred-queue.mjs owns the filename grammar and the classification, the
command owns every decision that touches the world - the RECORD_TOKEN rail on
both tokens that reach a filename, the resolved range ids, the refusal to
overwrite, the refusal to mint a phase directory. It writes no
ADJUDICATION-*.json and adds no fourth ruling: RULINGS stays frozen at three,
so a record at fire time is impossible by construction (D-09).

Two rules deduplicated rather than copied, because a second copy is a second
place for the queue and the record to disagree about the same fire:
`recordName` moves into lib/adjudication-record.mjs with both call sites
importing it, and the per-finding bounds inside buildEntries become the
exported `findingIssue` both artifacts validate through - so the shape a queue
member stores is the shape an adjudication record already refuses to store
wrong, in the same words. The shared `--phase/--trigger/--discriminator/
--round/--base/--head` preamble and the phase-directory check are one function
each for the same reason.

`deferred` joins TWO_WORD: three operations are coming (record now, list and
carry next), which is the risk-check run|status precedent rather than the
single-operation adjudication one.
`references/triage-gate.md` is the file every gate site re-reads at its gate
step without loading `review-triggers.md`, so the arm lives there and nowhere
else: persist the findings by the persistence rule step 5 already states, pass
that same file as the queue member's --payload, leave the `deferral` receipt,
and CONTINUE - no halt, no ask, no re-arm, nothing fixed at the fire. It also
states the two things a reader would otherwise get wrong: the queue member is
COMMITTED unlike the REVIEW file beside it, because a gitignored trace and an
untracked review both evaporate on a fresh clone, and the arm writes no
adjudication record, so a member reads as unruled until one supersedes it.

`workflows/plan.md`, `workflows/execute.md` and `references/execute-parallel.md`
deliberately grow no arm of their own: they state what each LEVEL resolves, no
level resolves `deferred` (D-03), and each already re-reads triage-gate.md,
which is why that file exists.

The GAT-04 receipt census moves to five names in the same change - the new
fenced line would otherwise redden it, and separating the two makes a correct
addition look like a deleted assertion. The three edited references get their
weight-budgets.json ceilings re-pinned in the same change (D-12), since those
budgets sit at each file's exact byte count.
A queue MEMBER is a DEFERRED-<trigger>-<discriminator>[-r<round>].json whose
ADJUDICATION sibling - the name recordName resolves for the same trigger,
discriminator and round - is not beside it (D-01). Two homes are read,
phases/*/ and the carried deferred/*/, and no third: an _archive-<label>/ tree
sits at the planning root and holds a closed milestone's copy of work that was
already carried.

lib/deferred-queue.mjs gains the pure half - isQueueName selects candidates by
prefix and suffix because DEFERRED-diff-plan-1.json cannot be split back into
its two tokens, and queueIdentity takes the identity off the member's own
fields and then REBUILDS the filename from them, so a member cannot be cleared
by an adjudication of a fire it does not belong to.

An unprovable queue is not an empty one: an unreadable directory, a member
whose bytes do not parse, a symlink wearing a member's name and a member whose
phase disagrees with the directory holding it each land on the reported
unreadable list and the envelope answers ok:false, the disposition
decideGateHalt already states for a findings payload it could not read. A
symlink is not a superseding record either.

No finding bodies cross the seam, only counts and identities, so the land
refusal and the progress line print the answer without a scratch file.
cmdStatus reads the same readQueue derivation deferred list answers from, so
/cad-progress and /cad-land cannot disagree about what is queued.

ALWAYS present, unlike the cycle and drift keys beside it that appear only in
their own states: this key is read by a REFUSAL surface, and a key absent in
the empty state collapses "nothing is deferred" into "this seam predates the
queue" - the fail-open answer on the one gate whose job is to refuse.

No cursor status value and no touch to AGREE (D-05): a Status: outside that map
is reported as cursor drift and rewritten by the very next /cad-progress. An
unreadable queue rides the block rather than degrading the status, because the
status answers about the roadmap and the refusal reads unreadable for itself.
The refusal is a block at the TOP of step 3, inside neither arm of the
git.auto_close branch, and no step is renumbered - prose-agreement pins
"cad-land step 3" and "cad-land 3(b)" by name and milestone.md and
triage-gate.md cite the same numbers.

It is a NEW arm and not land-cleanup.mjs gate (D-06): that gate halts only when
git.auto_close is true and reads only risk_surface survivors, so a
default-configured project would publish straight over a deferred
plan/diff/phase_diff finding. A non-empty unreadable arrives as ok:false and
refuses exactly as a member does, for the reason the survivor gate already
gives about a payload it could not parse.

The new prose-agreement arm pins REGION and ORDER rather than presence: a call
inside 3(a) never runs on the unattended arm, and a call after either arm has
already published. Deleting the invocation reddens it (watched).

skills/cad-land/SKILL.md re-pinned in weight-budgets.json, 13273 -> 14853.
The derive step names the new envelope key and what it carries; the report
step prints the count and the queued-fire count; the route table gains a row
below every recovery and work row and above the three that end a cycle,
because each of those leads to a land - /cad-milestone chains /cad-land after
its prune, and /cad-land refuses on this same queue.

Both sites state the figure comes from the status envelope and never from the
cursor's Next: text (D-05): the cursor is a hint the derivation overrides, and
parsing a count back out of free text is the substitution this repository
already condemned for trigger names. No new Status: value and no touch to the
reconcile mapping - one outside AGREE is reported as cursor drift and rewritten
by the very next /cad-progress.

The new prose-agreement arm pins the key at derive, report and route, the
never-off-the-cursor rule at both reading sites, the route row's ORDER against
the five rows around it, and the absence of a deferred status mapping in
reconcile.

progress.md re-pinned in weight-budgets.json, 11449 -> 13110.
The state step's cursor set branches: when the run deferred a fire the resume
pointer is composed from what the run did rather than a literal, so it rides
--next-file, the transport conventions.md binds caller-derived text to and the
reason that flag exists. --status executed is pinned as it was - a new status
value lands outside planning.mjs's AGREE map and is rewritten as cursor drift
by the very next /cad-progress (D-05). The pointer is a HINT; the count comes
from the status envelope.

The commit list stages .planning/phases/<N>/DEFERRED-*.json, and stages it
whatever planning.commit_docs says: a queue member is the only durable evidence
a fire was deferred - trace.jsonl is gitignored and the sibling REVIEW file is
committed by nothing - so untracked it is gone on a fresh clone and it leaves
the tree dirty at /cad-land step 2. It is not a planning doc that key answers
for; it is what stops the land.

execute.md re-pinned in weight-budgets.json, 29218 -> 30671.
A SEAM and not a prose instruction (D-10), unlike the transient risk_surface
union beside it: this moves committed artifacts during a close that runs
completely unattended, and a half-run prose step there leaves the only thing
stopping the chained land inside a directory milestone-prune is about to
delete.

A MOVE and not a copy, so --mode archive cannot leave a second copy under
_archive-<label>/ for the same fire. The phase stays a directory level rather
than folding into the filename, because two phases routinely defer the same
trigger on the same plan-<k> discriminator and a flat carry would collide
silently. A settled member is left behind to be pruned with its phase.

Three rails before anything moves: the destination is lstat'd for a symlink or
file squatting it, ahead of the queue read, because renameSync follows one out
of the tree; an unreadable member refuses the whole carry, with a hint naming
milestone-prune, since carrying what was provable would destroy what was not;
and every destination is checked before the first rename, so a collision leaves
the queue in one home rather than half in each. A re-run after a partial carry
finishes the job.

milestone.md step 3 calls it per pruned phase, beside the risk_surface carry,
with the difference stated in one clause: that union is transient and step 7
deletes it, the carried queue is committed and stays until it is adjudicated.

Censuses re-pinned: arg-contract flag entries 165 -> 166, milestone.md budget
13307 -> 14222.
firePhaseDir becomes fireHome and resolves two homes in order: phases/<N>/
while the phase is live, else .planning/deferred/<N>/ once deferred carry has
moved that phase's queue out ahead of milestone-prune. Both write faces -
adjudication and deferred record - go through it, off one resolver.

Without the second home a carried member is permanently unclearable: the
adjudication that would supersede it refuses on the deleted phase directory, so
the finding stopping the land can never be ruled on, and an unclearable gate is
one that gets bypassed. deferred record needed the same widening in the same
change, because a triage that rules a blocker/high survived has to re-arm, and
a round-2 member with nowhere to be written leaves the cap reading unspent off
a queue that could never gain one.

The lstatSync check moved WITH the resolution, so the carried home gets the
rail the phase directory has: a symlink there is followed straight out of the
planning root. Each envelope's record path is now derived from the home that
was chosen rather than the literal phases/<N>/, so it names a path an auditor
can open.

recordForFire deliberately does not widen: it resolves the receipt RECOUNT,
whose contract already omits the check on an unresolvable record rather than
failing the append, so a carried fire degrades to no cross-check instead of a
wrong one. The asymmetry is stated at the widened site.

Falsifier watched: narrowing the resolver back to phases/ alone reddens the
carried-fire arm.
lstatSync does not follow the final path component and follows every one
before it, so the destination guard aimed at deferred/<N> answered "absent,
go ahead" while deferred/ was already a symlink out of the planning tree.
The mkdirSync(recursive) then built the phase directory wherever that link
pointed and renameSync filled it with committed queue members - the gate's
only durable evidence, deposited outside the repository, reported as a
successful carry, immediately before milestone-prune deletes the original.

milestone-prune's archive root escapes the same shape because it sits one
level under the planning root, where its single lstat IS the intermediate
check. This destination sits two levels down, so it takes two.
Three plans, 15 task commits plus one gate fix. The deferred gate mode, its
committed queue, the land refusal that reads it, and the two rails pinned
against movement.

The risk_surface gate fired on plans 1 and 2 and found one real blocker in
plan 2's range: deferred carry checked only the leaf of the destination it
creates, so a symlinked parent sent committed queue members out of the tree
on an ordinary checkout. Fixed and pinned before the phase closed. The
adjudication records for all three fires ship beside this summary.
Four items skipped, all four the same two live runs: a /cad-execute chain
whose gate defers a blocker, and a /cad-land that refuses on the queue.
Neither is invocable by an agent and the land's other arm publishes, so
both are deferred to phase 3's own run.

The deep pass refuted this phase's own goal check. The SUMMARY claimed
nothing exercises the deferred path end to end; cad-verifier exercised the
machine half live - record, list, carry, supersession, round-2 into the
carried home - and proved the receipt seam in both polarities. What is
actually unproven is two agent-prose hops, which no code branch can reach.
phase-plans.mjs gains its disk half back: `declaredPhaseFiles` unions the
frontmatter `files:` list across every conforming PLAN in `phases/<N>/`, and
`declaredPlanFiles` answers the same for ONE plan named by its key, so an
executor dispatch can floor on the plan it was handed rather than on its
phase's union.

`found` and `clean` ride beside the union so a caller applies the aggregation
rule - the discount is earned only by a scope every member of which was read
clean - without re-reading a byte. Both failure arms contribute no paths and
one warning: a read that throws, and frontmatter with a non-empty `issues`
array, which is dropped whole rather than salvaged.

The frontmatter list only, never the union with `- **Files:**` task lines:
that over-approximation is safe for an overlap check and unsafe as a raise.
A second face beside `scanDiff`, over the SAME table and the same signal
ordering: the caller hands declared paths with whatever body each currently
has, and it answers in the `{category, signal}` shape a fire site states a
reason from. The walk both faces run was extracted rather than copied, so the
floor cannot raise on evidence the commit-time gate would not fire on.

A declared path with no readable body contributes its path signals and no
content signals, and that is not inconclusive: at plan time a declared file
frequently does not exist because the plan creates it, and calling an absent
body unjudgeable would raise every create-a-file plan.

The two signal-table files are exempt from the whole-body content pass, scoped
to this face alone - `scanDiff` reads a hunk, where a self-match is a real
edit, and its fix-at-the-mention rule stands unedited. surface-scan.mjs is
what proves the exemption bites: its own table evidences three categories by
construction.
resolve reads the phase's declared `files:` at plan time, scans them with
`scanDeclared` scoped to the answered surfaces, and takes the higher of the
configured level and the computed raise through `stakes_order`. An explicit
`stakes` is never resolved below at any level; an unset one floors at `solo`
and the raise does the work.

The raise target is `shipped`, deliberately not `critical`: the criterion is
that a matched phase resolve no lower than today's default, and a `critical`
target would put most of a repo's plans on the top row and rebuild the
raise-tax the deleted name-keyed floor died of.

`cad-planner` and `cad-assumptions-analyzer` are exempt - the cursor lags at
both their call sites, so a floor computed for them would be computed off
another phase's file list. Their reason entry says the floor was not computed
rather than carrying the `risk floor:` prefix, so "nothing raised it" and
"nothing looked" stay apart.

Bodies are read against the planning root's PARENT and never outside it: an
absolute or `..`-climbing declared path, and a body over the size cap, get
their path signals and no read. The `files:` list is data that can arrive with
a clone, and a resolve is not a place to open an arbitrary path.

The header block that stated THERE IS NO RISK FLOOR now states the floor, and
the schema's `stakes` purpose says it is a minimum rather than the level every
phase pays.
The fail-closed arm splits three ways. A scope read whole that matched nothing
has earned the discount and says so; a scope with nothing readable in it has
proved nothing, names what it cost, and holds the configured level. The
aggregation rule is one predicate - at least one conforming plan, and every one
of them read clean - so a mixed phase whose unreadable plan is the risky one
can never resolve below today.

Six rows pin it, all with `stakes` unset and all asserting ok:true at the
schema default: an absent phase directory, a directory with no PLAN, a PLAN out
of grammar, a PLAN that cannot be read, and a two-plan phase where one member
is unreadable - plus the paired positive, both plans clean and surfaceless
resolving solo, without which the other five are satisfied by a floor that
never discounts anything.

The unreadable-plan fixture is a directory at the plan's own name rather than a
chmod: EISDIR fails for any uid, where a chmod is a silent no-op under a root
test runner.
The floor is per PLAN for an executor dispatch and per PHASE for a phase-scoped
role. `--plan` is typed `plan-key`, so lib/plan-key.mjs's existing predicate
judges it - the same grammar `risk-check` reads - and it refuses both a bad
value and a bare flag, since a valueless plan flag would silently take the
phase union for a caller that asked about one plan.

It is a flag of its own and never an overload of `--bracket-plan`: that value
is the trace worker key and is the role name for every non-executor dispatch,
so reading it as a floor key would make a phase-scoped role indistinguishable
from a plan key naming no file - and the two take opposite arms, the union
versus fail-closed.

A key resolving to no plan file takes the fail-closed arm and says so, rather
than widening to the union a caller did not ask about.
`route.mjs resolve --phase` declared `warn` on both axes, on the reasoning that
a usage refusal would route the phase lower than its own risk baseline. That
held while the flag named only the phase a trace event is keyed to. It decides
a FLOOR now, so warn-and-continue answers a typo by computing that floor from
the cursor's phase - a different phase's declared files, at a level the
resolved bundle gives the caller no way to notice is wrong.

So it flips to `refuse` on both axes, reads through `RESOLVE_FLAGS` like every
other value-carrying flag, and the `phaseWarning` relay is deleted with it. An
absent flag still falls to the STATE cursor: the declared row is a value door.

`warn` keeps its place in the vocabulary and in the evaluator with no shipped
row declaring it, pinned by synthetic cases, because the disposition is a
reasoned position - deleting the arm would make the next flag that needs it a
refusal by default rather than by decision. Every prose site naming this flag
as the `warn` exemplar now states the new reason instead of the reversed one.
execute.md's executor resolve carries `--plan <k>` beside the `--bracket-plan
<k>` it already had - the same number as a different quantity, one keying the
trace and one scoping the floor - so an executor is routed for the plan it is
being handed. plan.md's check_gate resolve carries an explicit `--phase {N}`,
because it runs while the cursor still names N-1 and would otherwise be floored
off the wrong phase's plans.

seams.md's "the stakes level a config layer set is the level, full stop" becomes
the floor rule: the configured level is the minimum, the phase's declared
`files:` raise it, an unreadable plan holds it rather than dropping under it,
and a malformed `--phase` is refused. The Concurrent-dispatch paragraph gains
the executor exception to resolve-ONCE-per-(role, attempt) - the plan scope is a
routing input now, so per-plan executors of a parallel phase can resolve
different levels.

The mechanism is stated once, in the seam; the workflow files cite it. Every
budgeted file edited here has its `weight-budgets.json` row re-pinned from
weight.mjs's own measurement in this same commit.
The discount predicate measured PLAN readability and nothing below it, so a
plan that parsed clean while declaring a source file nobody could open handed
the whole scope a level below the configured stakes on evidence never gathered.
declaredBodies now separates a body that does not exist yet - the routine
create-a-file case - from one that exists and was skipped, and riskFloor
withholds the discount on the second.

Two arms produce a skip. An oversized body loses its content signals silently,
which is how a 600 KiB file full of JSON.parse calls read as clean. And
statSync FOLLOWS a symlink: a link to a character device reports size 0, clears
the byte bound, and is then read to an EOF that never comes - the lexical
absolute/.. refusal checks the spelling, never what the path resolves to.
lstatSync plus isFile() is that second check.

Both raised by the phase's own risk_surface gate and confirmed against the
code.
The risk_surface gate fired on plan 1's range and blocked. Both high findings
were confirmed against the code and fixed at da5bae4; the one narrowed re-arm
round raised a third, adjudicated down to medium and filed as an open item.
Round 1 and round 2 adjudication records ride along - the rulings, not a count
of them.
scanDeclared's whole-body content pass read documentation as call sites: METHOD.md alone raised five of this repository's phases on a recursive-delete line it documents, and references/review-triggers.md raised five more. A declared path whose final extension names a document now contributes its PATH signals and no content signals, the same way a signal-table file already skips its body.

Scoped to the plan-time face. scanDiff reads a hunk, so a line added to a document is a change someone actually made in the range, and its header's rule - fix at the mention, never a path exemption - stays in force unedited.
Every plan-time raise on this repository read 'touches secrets (changed line: a credential-named assignment)' at a moment when no diff exists and no line changed - the whole current body was scanned. The declared face now says 'body line: <label>'.

signalIn takes the prefix rather than forking the table: the ORDER is the thing that must not drift, and the label bytes after the prefix stay identical on both faces so a plan-time reason and a commit-time risk_surface finding name the same construct the same way. scanDiff's own strings are unmoved.
The shipped templates/PLAN.md ships `files:` with no items, so a plan copied from it parsed perfectly, scanned zero files and took the solo discount - absence of evidence reported as absence of surface, with verify off and the plan gate down to advisory.

The reader now names the plans that read clean and declared no path, beside found and clean; the floor treats those as not discountable, warns per plan file, and gives the withheld reason its own arm. "No surface" and "nothing was declared" are the two sentences this seam exists to keep apart, and the second may never be spelled as the first.

planning-files.mjs is untouched: items: [] is a correct answer there for a missing block, a missing key and an empty list alike, and what zero declared paths MEAN is the floor's judgement.
declaredBodies guarded only the FINAL declared path component with lstatSync, so a symlinked PARENT directory put the read in another tree while the declared spelling stayed repo-relative and clean - and the docstring's boundary claim, that a --file pointed elsewhere cannot read this tree's files, was untrue for any repository whose layout carries such a link.

Containment is now judged on what the path RESOLVES to, with the repository root resolved the same way and once so a root reached through a link does not refuse its own tree. The arms stay distinct and in order: an absent path is still the one arm that is not unread, a link straight to a device keeps the not-a-regular-file finding, and a path resolving outside is unread with the boundary as its cause. The bytes are never read and never echoed.
The risk_surface review on 23fb76d..1c2c45c found the floor's FIRST input
unguarded while the declared bodies one level down were not: PLAN_FILE admits
a name, and a name says nothing about what the entry is. A plan path that is a
symlink to a character device or a FIFO reports size 0 through a following
stat and is then read to an EOF that never arrives, so a resolve whose whole
contract is to fail closed at the configured stakes hangs instead.

lstatSync before the read, on route.mjs's declaredBodies reasoning and at its
byte bound. A non-regular or oversized plan takes the same unread arm an
unreadable one already took, so the discount is withheld rather than earned.
The two readers here joined `phases/<phase>` under the planning root, so a
phase a milestone close ARCHIVED into `_archive-<label>/<N>/` was unreachable -
27 of this repository's own 30 phase directories.

Split the locating from the reading: `declaredFilesIn(dir, planKey?)` is the
one reader and the one set of failure rules, addressed by PATH, and the two
phase-keyed faces are that function with `phases/<N>` joined for them.
`phaseDirsIn` lists every directory under the planning root that HOLDS a
conforming plan file, live and archived, each with the path a reader takes and
a stable label, sorted by label.

The locator's test for a phase is that the directory holds a `PLAN_FILE` match,
never a phase-name grammar: `PHASE_DIR_NAME` lives in bin/planning.mjs, which
this lib may not import, and a directory holding no plan declares no files
whatever it is named. Everything still fails OPEN - an unreadable planning
root, archive directory or phase directory contributes no entries and no
throw.
`route.mjs replay` answers AC3 in one JSON line: one row per phase directory
the planning root holds - live and archived - carrying today's level, the
computed one, and the surface, signal and declared file behind any raise, plus
the plan counts the discount predicate read. `regressions` is always present,
empty on a healthy tree, on the record shape risk-check established.

The RAISE is what carries evidence, never the diff between the two columns:
RAISE_TARGET and the schema default are both `shipped`, so most raises land ON
today's level and a diff-triggered evidence column would blank exactly the rows
whose surface a reader needs.

The computed column is not a second arithmetic. The scope-to-level half of
`riskFloor` - the discount predicate, the stakes_order comparison, the raise
and the reason vocabulary - is now `levelFor`, which `resolve` and `replay`
both call; `riskFloor` keeps the addressing half alone. `resolve`'s behaviour,
envelope and reason strings do not move.

Live on this repository: 30 rows, 27 raised, regressions [], and both rows
computing below today name no surface.
There was no way to route below the computed floor at all, so "a lowering
without the override is refused" was vacuous. One NEW key inside
`review.triggers.risk_surface` - `waive_routing_floor`, an array_enum over the
same eight categories its `surfaces` sibling enumerates, defaulting to null so
"waived nothing" and "never answered" stay one state.

It waives a LEVEL and never a REVIEW: the blocking commit-time risk_surface
gate still fires on the actual diff, and the key name and the schema purpose
both say so. D-03 holds - lib/retired-keys.mjs is untouched and the eight
`risk.override.*` keys stay retired.

The arms: a waived category raises nothing and every waiver applied is named in
`reason` with the surface and the file it would have raised on; the next
unwaived match still raises, so waiving `secrets` on a phase that also touches
`destructive` routes at the raise; every match waived holds at the CONFIGURED
stakes and takes no unset-`solo` discount, because a scope that matched a
surface it waived is not a scope that matched nothing; and the paired refusal
says which key and which surface would have to be named. Never an ok:false.

A waiver value outside the table's vocabulary is named in `warnings` and waives
nothing, validated once per command rather than once per replay row. The
replay honours the waiver by construction - one levelFor, both faces.
AC4's "that rail is byte-identical" was a claim nothing checked: every
assertion in this file reads message CONTENT, so the eight `risk.override.*`
rows could be edited, reworded or un-retired with the suite still green.

The pin is a sha256 of lib/retired-keys.mjs as CER-01 found it, with the
comment saying what it protects - D-03 keeps the family retired because a key
cannot live in the schema and the retired registry at once, and the floor is
given back through `review.triggers.risk_surface.waive_routing_floor` instead -
and what to do when it goes red: re-read D-03 first, because a deliberate edit
to that file is a decision and not a refresh.

Recorded beside it: the eight `detail` strings still say "there is no floor for
a waiver to lower", which stopped being true when the floor landed and which
D-03 locks in place regardless.

lib/retired-keys.mjs is not edited by this or any task in this phase. Falsified
before commit: one appended byte reddens this test alone, with the file and
D-03 named in the message.
D-08: `model.effort.<role>` still wins over the cell, but a DETECTED risk
surface floors it. Until now the configured rung won unconditionally, so a
project that pinned a cheap rung for a role kept it on the one phase the floor
had just raised - and references/config-reach.md has claimed the clamp exists
since the v2.7.0 deletion, which is half of UAT item 12.

The floor picks the ROW before the cell lookup runs, so the clamp is against
the FLOORED cell's rung: a configured rung below it in `rung_order` does not
apply, the cell's rung stands, and `reason` says so naming the surface and the
file - in the voice the other three arms of that block already use.

Gated on a RAISE having fired, never on a floor merely having been computed: a
scope that read clean and matched nothing took a DISCOUNT, and clamping against
a discounted cell would take the dial away in exactly the cheap case CER-01
buys. The two pre-plan roles are exempt for free (no floor is computed for
them) and a waived surface raised nothing, so it clamps nothing.

`riskFloor` now returns the whole record rather than a bare level, which is how
the resolve learns a raise fired; `reason` and `warnings` are untouched. When
`rung_order` cannot place both rungs the configured rung stands with a warning,
on the precedent of the retry block below.
seams.md gains the three statements task 3, task 5 and task 2 made true: the
waiver key that is the one way to route below the computed floor and lowers to
the configured stakes and no further, the rung clamp a raise applies to a
configured model.effort for post-plan roles, and route.mjs replay as the answer
to what the floor does to this project's own phases. weight-budgets.json is
re-pinned in the same change (phase 2 D-12).

self-verify.test.mjs's placeholder census enumerates every schema key, so the
new waive_routing_floor key had to join its surfaces sibling under the same <t>
expansion or the census reported it inert. Made here rather than by the
executor, which correctly refused to write outside PLAN-3's declared lease.
The blocking risk_surface review on this plan's own range found phaseDirsIn
walking through symlinks: readdirSync follows a linked directory, so an
archive group or a phase entry that is a link puts route.mjs replay in another
tree, reading PLAN files that are not this project's and scanning the paths
they declare.

Contained on declaredBodies' reasoning and by its test - judged on what the
path RESOLVES to rather than on how it is spelled, so an archive kept behind an
in-root link still reads as a phase while an escape is skipped. Fails open like
every other arm here: a path that cannot be resolved contributes nothing and
throws nothing. Both directions pinned; replay still returns 30 rows on this
repository with regressions empty.
PLAN-2, PLAN-3 and PLAN-4 close every failure phase 3's first UAT found: the
detector stops raising on a mention, a scope that declared nothing stops taking
the discount, the replay and the waiver key exist, and the narrative documents
describe the floor that ships. Carries the review records for the blocking plan
and risk_surface gates, including the two survivors fixed before execution and
the two findings ruled down with their reasoning.
docs: open v3.5.8, record the v3.5.7 close
Some checks failed
test / node-test (git, 22) (pull_request) Successful in 54s
test / node-test (git, 24) (pull_request) Successful in 30s
test / node-test (other, 22) (pull_request) Successful in 1m3s
test / node-test (other, 24) (pull_request) Successful in 1m3s
test / node-test (planning, 22) (pull_request) Successful in 1m29s
test / node-test (planning, 24) (pull_request) Successful in 1m30s
test / node-test (prose, 22) (pull_request) Successful in 30s
test / node-test (prose, 24) (pull_request) Successful in 28s
test / node-test (review, 22) (pull_request) Successful in 17s
test / node-test (review, 24) (pull_request) Successful in 16s
test / node-test (routing, 22) (pull_request) Failing after 32s
test / node-test (routing, 24) (pull_request) Failing after 33s
test / self-verify (pull_request) Successful in 12s
test / typecheck (pull_request) Successful in 18s
23b71c866b
john merged commit 6ec15adff0 into main 2026-08-22 11:45:37 +00:00
Sign in to join this conversation.
No description provided.