v3.2.0 - the controls that reported success #124
No reviewers
Labels
No labels
already-shipped
bug
documentation
duplicate
enhancement
external-review
good first issue
help wanted
in progress
invalid
needs-decision
proposal
question
security
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: crenshawdev/cadence-archived#124
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "cadence/v3.2.0"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Twelve requirements across four phases, 105 commits. The theme is one line: a control that reports success without having checked is worse than no control, because the product advertises the check.
A repo config layer could reparent the merged config through
__proto__and reach every enforcement surface, whileconfig.mjs validatereturned{"ok":true,"checked":0,"errors":[]}over the same file.atomicWritewrote through a symlinked temp path. The unattended-close gate could not tell "no findings" from "could not read the findings" and returnedproceedfor both. Git failure details carried credentials out of a remote URL. Each now fails safe and names the failure it saw.The review arm proves itself in the same cycle: adjudication records kills as well as survivors, the reviewer set is resolved in the seam rather than by prose that could be skipped without a trace, and an external reviewer gets the same stance and severity bar as the local one. Three triggers that blocked nothing are off or deleted,
risk_surfacelearned the eight categories a project actually has, and every dispatch is bounded at 200 turns instead of a nominal 400.Audit green: 12/12 traced, 0 broken, 36/36 acceptance criteria covered. Manifest at 3.2.0. LND-01 was cut before execution and stays open as #121.
deepMerge accumulated with merged[k] = ..., which runs Object.prototype's __proto__ setter for a key JSON.parse makes an ordinary own property. A .planning/config.json arriving with a clone could therefore hand the merged config a prototype of its choosing, and every config.git?.on_protected read in the spine inherited it. The accumulation now defines an own property, so the key lands inert and the result keeps its ordinary Object.prototype. Both live shapes close: top-level {"__proto__":{"git":...}} when no global layer defines git, and {"git":{"__proto__":...}} one level down when one does. constructor and prototype ride along as regression pins - they pass against the unfixed merge and distinguish nothing on their own.flatten skipped every _-prefixed key, a rule written for the _meta annotation block that also swallowed __proto__ - so validate answered {"ok":true,"checked":0,"errors":[]} on the same hostile file git-guard was reading as settings. The skip is narrowed to exactly _meta rather than deleted; deleting it turns validate red on every hand-annotated config. Two smaller holes on the same path close with it. flatten's accumulator defines own properties, so a top-level SCALAR leaf named __proto__ is reported instead of silently reparenting the accumulator, and validate's schema lookup is guarded by hasOwn, so that leaf is answered "unknown key" rather than Object.prototype standing in as a spec with no type.srcmean config-layer scope, on every surface that renders it 7c14f468fbThe strip read the key and nothing above it, so a repo layer holding `{"workflow":"x"}` reached the same end by a path that sets nothing: deepMerge replaces scalars and arrays wholesale, so that section took out the user-global `workflow` object outright and both commands fell back to their schema defaults. `review: 5` did the same to `review.key_file`. readPath reported ABSENT for it, so the strip skipped it and nothing warned - the suppression D-13 reasons about, arriving without touching a protected key. Walk the intermediate segments and remove the offending section instead, warning on a non-null value the way the leaf arm does. Nothing legitimate is lost: a scalar cannot carry any key, so the repo-settable siblings under that section were already unreachable through it. The shipped arm asserting the old behaviour pinned the defect, so it is rewritten rather than added to. Raised by the blocking risk_surface review of a27441f..a9b429f.appendFileSync follows a symlink, so a planted .planning/trace.jsonl link redirected every event a run wrote out of the tree. appendEvent now lstats the path ahead of the size stat and returns {written:false, reason:'symlinked-trace'}, appending nothing. A reason rather than a throw (D-04): the header's NEVER-throws contract is what keeps a record of a decision from changing the decision, and the reason reads exactly like size-cap and append-failed beside it. An absent file is still the ordinary first write; the header's APPEND contract paragraph now states what the append costs and what is paid.`readFindings` collapsed unreadable stdin, EMPTY stdin, malformed JSON and a valid wrong-shaped envelope all to `[]`, so the unattended-close gate answered `proceed` with "no surviving blocker/high finding" about a payload it never parsed. It now reports which of the four it saw and passes that to decideGateHalt, keeping the advisory envelope exactly as it was (D-08). EMPTY stdin is one of the four halting states (D-09): the gate requires an explicit `{"findings":[]}` to proceed, because a forgotten pipe is the likeliest operator error and is the one case the old gate waved through under git.auto_close. A bare JSON array still reads as the findings list. The header's `gate` contract and skills/cad-land/SKILL.md's step-3 pipe instruction state the same rule, and the SKILL.md weight budget is re-pinned to its new byte count.read() collapses ENOENT with EACCES, EISDIR and EIO, so a CONTEXT.md at chmod 000 collected D-10's absent-file exemption and criteria-coverage answered {"ok":true,"phases":[]} over criteria it never looked at. A reader scoped to this one call site keeps the exemption for a genuinely absent file and pushes an unreadable-context break - the envelope's only verdict-moving key - for every other errno. read() and its 38 call sites are unchanged.`milestone-prune --label '../../../outside-tree'` built the archive root by string concatenation and handed it to mkdirSync and renameSync, so it moved phases/1 clean out of the planning root and still answered {"ok":true,"action":"pruned"}. Two independent terms now run at the point the label is read, before any read, mkdir or rename, and in both modes: containment via resolve(), and a refusal for "|" or a newline, which archiveRequirements would write straight into a table cell. A spaced milestone name is still accepted - REMOTE_NAME's shape would refuse it.Deleting pre_ship left decideGateHalt with no producer. Leaving it wired and always fed {"findings":[]} would re-create in /cad-land exactly the control-that-reports-success defect this milestone is named after, with phase 2s hardening in place and unreachable, so the gate gets a real feed instead: a risk_surface fire now persists its settled survivors to .planning/phases/<N>/REVIEW-risk_surface*.md, and the unattended close unions those files rather than firing a review of its own. Every behaviour phase 2 gave the gate is untouched - the four named unreadable failures each halt under auto_close, proceed whenever auto_close is off. What changed is the trigger the halt reason names, now pinned by a test so a gate fed by a deleted producer cannot go quiet again.One commit, because self-verify derives its trigger vocabulary from the schema and cross-checks it against both the routing table and the reach table: any split leaves unknown-trigger or unstated-reach standing. The three keys go to lib/retired-keys.mjs with since v3.2.0, following the risk.override.* precedent for a whole retired feature. A user who set the gate gets a diagnostic naming the removal and the gate that still runs on this branch, not an unrecognized-key error - this repo carried that exact key until this commit, so the upgrade path is not hypothetical. BREAKING CHANGE: review.triggers.pre_ship.{gate,tier,effort} are removed. They are refused at the write face and warned at both read faces.