v3.7.0 - the refusal that names the next step #251

Merged
john merged 37 commits from cadence/v3.7.0 into main 2026-08-24 02:16:33 +00:00
Owner

Two phases, 37 commits off v3.6.1. Three requirement ids seeded at the open and all three traced to a verified phase: HNT-01, HNT-02 (#238) and SCP-01 (#249). /cad-audit PASS on both arms, 13 of 13 acceptance criteria covered.

Phase 1 wrote hints at every in-scope refusal under cadence-core/bin/ - 243 sites, 0 hintless, down from 215 hintless when the check went in - and added self-verify check 22 (refusal-hints), which is what makes it a property of the tree rather than a number that drifts back. No reason token string changed.

Phase 2 made config.mjs set refuse a repo-scoped key at the user-global layer at write time, reading a new repo_only schema marker rather than a list of key names. It resolves the layer from the target file rather than the flag, and runs inside checkPairs ahead of every read and write, so a multi-pair set carrying one marked key leaves the target byte-identical. config.mjs check --global reports the same error the write face refuses on.

One accepted limitation ships with it, stated in the changelog under Known: the scope check resolves layer identity from a pathname and re-resolves that pathname for the read and the write, so a symlink swap in that window defeats the refusal. Not a regression - before this release set applied the pair with no layer check at all. Raised by the blocking risk-surface gate, confirmed by the phase verifier, and overridden deliberately rather than missed.

Two phases, 37 commits off v3.6.1. Three requirement ids seeded at the open and all three traced to a verified phase: HNT-01, HNT-02 (#238) and SCP-01 (#249). /cad-audit PASS on both arms, 13 of 13 acceptance criteria covered. Phase 1 wrote hints at every in-scope refusal under cadence-core/bin/ - 243 sites, 0 hintless, down from 215 hintless when the check went in - and added self-verify check 22 (refusal-hints), which is what makes it a property of the tree rather than a number that drifts back. No reason token string changed. Phase 2 made config.mjs set refuse a repo-scoped key at the user-global layer at write time, reading a new repo_only schema marker rather than a list of key names. It resolves the layer from the target file rather than the flag, and runs inside checkPairs ahead of every read and write, so a multi-pair set carrying one marked key leaves the target byte-identical. config.mjs check --global reports the same error the write face refuses on. One accepted limitation ships with it, stated in the changelog under Known: the scope check resolves layer identity from a pathname and re-resolves that pathname for the read and the write, so a symlink swap in that window defeats the refusal. Not a regression - before this release set applied the pair with no layer check at all. Raised by the blocking risk-surface gate, confirmed by the phase verifier, and overridden deliberately rather than missed.
john added 37 commits 2026-08-24 02:16:28 +00:00
The theme is #238, reassigned here from v3.6.1 on 2026-08-23 because it is a
named theme rather than a defect: a seam that refuses hands the user a
kebab-case token and no next step. Measured across cadence-core/bin/ with tests
excluded, 186 sites set a literal reason and 13 set a literal hint, all 13 in
planning.mjs or skim.mjs. The ratio has drifted since filing (130/10 then), so
phase 1 carries both the sweep and the self-verify check that keeps it.

Phase 2 is #249, the same silence one layer over: config.mjs set applies a
repo-scoped key at the user-global layer without complaint, and the user learns
the repository never opted in when the close refuses at land time. The fix reads
the schema's src: repo marker across all 33 keys that carry it. The two phases
share no files and carry no ordering.

Ids seeded: HNT-01, HNT-02, SCP-01.
config.mjs, route.mjs and review-provider.mjs each took (reason, detail)
and had no way to say what to do next. All three now take a third `hint`
argument and spread it conditionally, copying planning.mjs's already-widened
wrapper, so an absent hint adds no key and no shipped assertion moves.

review-provider.mjs's fail() records the degradation through traceProvider
before it emits; that call keeps its three arguments untouched, because the
trace is a fact about what happened and a hint is advice to the person at
the terminal.

One site behind each wrapper is hinted to prove the path end to end: the
unknown-key refusal from `config set`, route's unknown-role, and
review-provider's bad-provider.
Nine entry blocks carry the same two-armed catch, and the e.seam arm is the
argument contract's refusal reaching the user: {"reason":"missing-flag-value",
"detail":"--root"} and nothing saying what to type. All nine now carry one
hint, written once and used verbatim, telling the caller to give the flag the
detail names a value of the kind that flag takes and re-run. It names no
specific flag because the arm relays whichever one refused.

The internal arm beside it is untouched - D-03 excludes it, since there is no
user action there beyond filing a bug.

self-verify.mjs is in this task rather than in plan 1 on purpose: it is the
same idiom and must read identically, and the linter should not be an example
of the defect it reports.
Eight sites: the read refusals over a layer file that will not parse (in
validate and in set), the two non-object-root refusals, the path collision
set refuses to write through, get's unknown-key, the bad-schema refusal when
the plugin's own schema will not parse, and check's invalid envelope.

Each hint names the user's next action - which file to repair, which command
lists the keys, what a valid layer looks like - and none restates its own
detail. No reason literal moved; check and set still speak the one failure
contract workflows/config.md documents, hint included.
Seventeen sites across over-cap, bad-payload, bad-provider, bad-args, no-key,
http, no-output, bad-json, bad-shape, bad-command and the computed
over-response/transport pair. This is the refusal surface a user hits with the
least context - a cross-model review fails somewhere between a config key and
a remote HTTP response - so each hint names the action per token: where the
key goes, what to shrink or which cap to raise, what is worth a retry, and
which side produced a shape the seam could not read.

Two shapes worth naming. failRequest's hint splits where its two words split,
because a socket that died is retryable and a provider flooding us is not.
And the http hint is one constant both request paths use, reading the status
back as an action rather than restating the number the envelope carries.

traceProvider's arguments and the detail: detail || null behaviour are
untouched.

Accepted, not resolved: four of these hints repeat guidance
workflows/config-review.md already carries in prose. AC5 forbids editing
workflows/ to reconcile them, so the duplication is the stated cost.
Fourteen sites, and these are the refusals that stop a land or a close, so
each hint is what the user does to make it proceed.

git-publish.mjs relays a decision from lib/publish-decision.mjs at both
gates, so the hint is a table keyed on the ARM and then the reason: three
tokens mean different things on the two arms - no-branch is a detached HEAD
when publishing and an unnamed --branch when reaping - and one sentence for
all of them would name none of them. auto-close-off sends the reader at the
REPOSITORY layer, because a user-global true does not speak for a repo that
never opted in.

release-bump.mjs relays primary.code the same way and gets the same
treatment. partial-bump is the exception that proves the rule: its hint
deliberately does not say re-run, because a second bump over a half-written
tree moves the files that already landed again - it sends the operator to the
bumped/changed flags in the envelope first.
Seven sites. route.mjs's unresolved and bad-table both send the reader at
route-table.json, since a torn or partial table is what either one means.
why.mjs's bad-query shows the query shape rather than describing it, and its
two git-failed refusals share one constant: classifyResult collapses every
non-clean git exit into that outcome, so neither site knows which fact
failed and the honest hint names the repository state the query needs.

skim.mjs's computed pair splits with its reason - an absent path is the
caller's to correct, anything else leaves Read as the way to see the file -
and line-count-drift takes the same wording the two hints already in that
file use, because the answer is the same one: read it with Read.

Every hintless-refusal entry self-verify now reports names planning.mjs,
which is plan 3.
skim.mjs strips comments from JS/TS, so 'skim to find, read to change' had
no arm for the markdown references, schemas and JSON tables both roles
spend most of their probes on. Locate with a no-context grep -n, read the
window its line numbers name, and answer a missed pattern with a looser one
rather than a wider range - the blind slab pays for the same miss twice.

Budgets re-pinned: planner 11235 -> 11747, analyzer 5467 -> 5980.
`repo_only` is a NEW spec field, separate from `src`: `src: repo` means
settable in either layer and 33 keys carry it, so nothing may key a
layer refusal on it. `repo_only` marks the narrower question - would a
user-global value AUTHORIZE a change to a repository that never opted in
- and `git.auto_close` is the only key that answers yes today.

_meta.note states the field, what bin/config.mjs does with it, and the
test a maintainer applies before marking a second key.
checkPairs takes the resolved target layer and, after checkValue, refuses
any pair whose schema spec carries `repo_only: true`. The entry rides the
existing reason:"invalid" detail array - no new reason token - and its
error names the key, says a user-global value cannot authorize a change
to the repository that has to honour it, and names the next step.

set() resolves that layer off the target FILE, never the flag: `--file
<the global config's own path>` returned global:false from optFile and
wrote straight through. `layerIdentity` is exported from the merge lib
rather than re-spelled here, since it is the same "one file, both layers"
question the read face answers.

Ahead of every read and every write, so a two-pair global set carrying
one marked key leaves the target byte-identical.
`check --global git.auto_close=true` answered `--global is not a
key=value pair`, so the inspect face could not be asked about the layer
the write face now refuses at - two faces that could not agree or
disagree about the same bytes.

The row is declared on CONTRACTS['config.mjs'].check with the identical
grammar its three siblings carry, and the dispatch arm reads it through
evaluateFlag off that row - never through optFile, which falls through to
a `--file` row `check` deliberately does not declare and would surface as
reason:"internal".

ARG-06's subcommand list widens to include check. The envelope is
otherwise untouched: same reason, same detail array, same hint.
A fixture schema through runWithSchema - which sets the CADENCE_TEST_SEAM
sentinel the override is gated behind, so the fixture is actually read -
marks a key the shipped schema has never marked and leaves its sibling
unmarked. The marked one refuses at the user-global layer through both
faces, the sibling is accepted at that same layer, and `auto_close`
appears nowhere in the fixture: the assertion is about the marker.

Watched failing with the `repo_only: true` line deleted from the marked
key - exactly that test, 92 pass 1 fail - then restored.

The negative control derives the shipped marked set in-process: exactly
`['git.auto_close']`, and no other `src: "repo"` key carries it.
One clause on the legend paragraph that already defines `[src]`, saying
`[repo-layer-only]` is a DIFFERENT marker from `[repo]` rather than a
stronger reading of it, and what it means at the write face. The marker
is spelled out in full on the `git.auto_close` row's Key cell so it
cannot be skimmed as `[repo]` in the same column.

The surface sat at 10452/10452 with zero headroom and self-verify treats
a budget as a ceiling, so its weight-budgets.json entry is re-pinned to
10725 in this same commit.
The third role with the same gap. cad-executor probes markdown, schemas and
JSON exactly as the planner and analyzer do, and 'skim to find, read to
change' had no arm for any of them.

Budget re-pinned: 12458 -> 12970.
Line 64 and success criterion 2 both described the schema's "src": "repo"
marker as the layer-scope rule. CONTEXT D-01 disproved that premise - the
schema's legend defines "src": "repo" as settable in either layer - so the
phase shipped a new repo_only field carried by one key. SC2 now states the
derivation it was really asking for, with the superseded count recorded
inline rather than dropped.
docs: close v3.7.0, no cycle open
Some checks failed
test / node-test (git, 22) (pull_request) Successful in 1m0s
test / node-test (git, 24) (pull_request) Successful in 37s
test / node-test (other, 22) (pull_request) Failing after 1m18s
test / node-test (other, 24) (pull_request) Failing after 1m20s
test / node-test (planning, 22) (pull_request) Successful in 1m49s
test / node-test (planning, 24) (pull_request) Successful in 1m35s
test / node-test (prose, 22) (pull_request) Successful in 31s
test / node-test (prose, 24) (pull_request) Successful in 36s
test / node-test (review, 22) (pull_request) Successful in 18s
test / node-test (review, 24) (pull_request) Successful in 16s
test / node-test (routing, 22) (pull_request) Successful in 36s
test / node-test (routing, 24) (pull_request) Successful in 35s
test / self-verify (pull_request) Successful in 14s
test / typecheck (pull_request) Successful in 17s
cfb00c2dc9
john merged commit 9c24160fee into main 2026-08-24 02:16:33 +00:00
Sign in to join this conversation.
No description provided.