fix(cli): os validate / build / lint --json report the conversions a refusing defineStack applied, beside the refusal - #20926
Conversation
… stack producer applied A defineStack / composeStacks that converts an ADR-0087 D2 spelling and then refuses stamps the notices it applied on its ADR-0112 refusal (stackConversionsOf(error)). os validate, os build and os lint now fold that record into the --json `conversions` of their catch-all exit, beside the refusal. Folded, never recomputed; `[]` for any other throw. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
A strict defineStack that converts page:header `description` and then refuses `requires: ['no-such-capability']` answers exit 1, STACK_CAPABILITY_UNKNOWN and exactly the one page-header-subtitle-alias notice on each door; the canonical-then-refuse control answers `[]`. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…s on --json Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…fusal-conversions
…e ledger The catch-all fold is a call site both doors now make; it reads the record a producer stamped on its refusal and judges nothing. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 25 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8647e2d4b860e859f958f604110b88ed4e3047cc && git checkout 8647e2d4b860e859f958f604110b88ed4e3047cc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d78a0bda07c4bed2e3b71e051ac5db2632b82741 dd6fa5537450a9b13d982abead4b9e543a39725b && git checkout -B drift-repro d78a0bda07c4bed2e3b71e051ac5db2632b82741 && git merge --no-ff dd6fa5537450a9b13d982abead4b9e543a39725b
node scripts/docs-audit/affected-docs.mjs --json d78a0bda07c4bed2e3b71e051ac5db2632b82741
|
Contract reviewServed-tier: Inputs read: card #20583 (body and all ten comments, governed by ① Derived judgmentsAccept set: unchanged. Public surface: no export added or removed; no (a) The fold reads the producer's own record, unwrapped, exactly once — RIGHT.
(b) Text face unchanged, fold inside the (c) Payload shape — RIGHT. The emitted object literals are untouched by the diff: validate (d) The (e) Changeset ② Semver level
③ Boundary flagsEach dev
Anything in the diff that should have been an Check-runs on this head, read from the API and collapsed latest-per-name, converged at 19:56:36Z: 34 names, 31 Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #20583
Clause-②: no
What this lands: location 2's CLI half
A
defineStack(orcomposeStacks) call that converts an ADR-0087 D2 spelling and then refuses now reports both onos validate --json,os build --jsonandos lint --json: the refusal'serrorandcode, and the conversions the producer applied before it refused, inconversions.The spec half landed in PR #20651 (#20618): a refusing producer stamps the notices it applied on the ADR-0112 refusal it throws, and
stackConversionsOf(error)reads them. This PR is the fold the seat answer5886671384kept on this card: one line in each of the three catch-alls.validate.ts,compile.ts(os buildinherits it) andlint.ts: inside the catch-all's--jsonbranch,conversionNotices.push(...stackConversionsOf(error))runs beforeemitJson. The payload still reads the one shared sink (conversions: conversionNotices), which the nightly source-scan pins require of every exit.5886671384).git grepoverpackages/cli/src: everydefineStack(/composeStacks(hit is prose or scaffold text; the door dependenciespackages/lint/srcandpackages/objectql/srchit only comments). So only the config module's load can throw a stamped refusal, and a throwing load comes before both other fillers of the list (step 1b'sloaded.stackConversionsfold and step 2's own pass).stackConversionsOfanswers[]for every other throw: a plainError, this CLI's own refusals (refuseUnbuiltStack,ConfigRefusalError) and non-refusal throws.loadConfigpasses the refusal through untouched, sopackages/cli/src/utils/config.tsis not edited. Measured at165c1d49e3by callingloadConfigfrom source on the fixture below. The caught error is aStackCapabilityUnknownError(codeSTACK_CAPABILITY_UNKNOWN,instanceof Error) that carries the own symbolobjectstack.stack.conversions, andstackConversionsOf(error)answers the onepage-header-subtitle-aliasnotice. The canonical control answers[].--jsonbranch, and the producer's own stderr line is unchanged.[]" now name the one exception.validate.ts's hoist note counts three fillers.test/validate-build-gate-parity.test.ts:stackConversionsOfis now a bare call site in bothcompile.tsandvalidate.ts, so the closed call-site ledger needed a row for it. It gets its ownNOT_A_GATEreason ("carries the conversion record a stack producer stamped on the refusal it threw ... refuses nothing"), not the nearest bucket.os lintstill does not take the one-authoring-shape rule. An unbuilt default export is neither built nor refused by a producer, so nothing moves for it (#20367's OQ3 stays unopened).Measurements: the three doors through
bin/run-dev.js(CLI from source)The fixture is a strict
defineStackwhosepage:headerauthorsdescription(the live conversionpage-header-subtitle-alias) and which then declaresrequires: ['no-such-capability']. The control is the same config withsubtitle.165c1d49e3)os validate --json, convert then refuseSTACK_CAPABILITY_UNKNOWN,conversions: [], 1 producer stderr lineSTACK_CAPABILITY_UNKNOWN,conversions= the one notice, 1 stderr lineos build --json, the same configSTACK_CAPABILITY_UNKNOWN,conversions: [], 1 stderr lineSTACK_CAPABILITY_UNKNOWN, the one notice, 1 stderr lineos lint --json, the same configSTACK_CAPABILITY_UNKNOWN,conversions: [], 1 stderr lineSTACK_CAPABILITY_UNKNOWN, the one notice, 1 stderr lineSTACK_CAPABILITY_UNKNOWN,conversions: [], 0 stderr linesThe notice is
page-header-subtitle-aliasatpages[0].regions[0].components[0].properties.subtitle,descriptiontosubtitle,retiresIn18. The "after" column was also read at the final head: atdd6fa55374all six rows answer exactly as in the table.Tests
packages/cli/test/stack-conversion-record-door.test.ts(the per-PRintegrationtier;*.e2e.*files run nightly only, which is why the fix(cli): os lint --json reports the ADR-0087 conversions defineStack applied #20617 lint rows live here too). A new block drives triage's convert-then-refuse fixture through each door. Each row asserts exit 1,codeSTACK_CAPABILITY_UNKNOWN, the catch-all'serrorstring plus the door's own verdict key (valid: false/success: false), andconversionsequal to exactly the one notice. The control (canonical then refuse) asserts the same envelope withconversions: []. That is 6 new rows, and the file now holds 21.dd6fa55374, throughscripts/pm/os-verify-lock.sh:pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2:Test Files 238 passed (238)/Tests 3382 passed (3382).vitest run --project integration --maxWorkers=2 test/stack-conversion-record-door.test.ts:Test Files 1 passed (1)/Tests 21 passed (21). That is the 15 existing rows plus the 6 new ones.pnpm --filter @objectstack/cli typecheck: exit 0,check:test-typecheck: OK. The door file is in the test-layer program (tsc -p tsconfig.test.json --listFilesOnly, 1 hit).dd6fa55374,OS_TEST_TIERS=nightly vitest runovervalidate-json-failure-conversions.e2e.test.ts,build-json-failure-conversions.e2e.test.tsandlint-conversion-notices.e2e.test.tsgaveTest Files 3 passed (3)/Tests 35 passed (35). That covers every exit still reading the one sink, the load-throw rows (a plainError) still answering[], and the normalize call still belowloadConfig.649236e024, failed one test:validate-build-gate-parity.test.tsfound the new call site unclassified.dd6fa55374adds the ledger row, and the run above is green.Ablation (reverse verification)
Run from the committed state
a34e868fa4. The fold line was deleted in all three doors at once: three nested WRAP legs ofnode scripts/ablation-replace.mjs, plus a shelltraprestoring the three absolute paths withgit checkout HEAD --. The CLI loads these files fromsrc/through tsx, so nodist/sits on the measured path and no rebuild was needed.conversionNotices.push(...stackConversionsOf(error));went from 1 to 0 in each file. The blobs moved:validate.ts11b7ea3e79e2tof744edd59909,compile.ts1e716a6e53fato3f653b1a82ba,lint.ts4837e2246a13to5df926e26a3e.Tests 3 failed | 18 passed (21). The three red rows are the convert-then-refuse rows onvalidate,buildandlint, each failing onconversions:expected [] to deeply equal [ { …(5) } ]. Their exit andcodeassertions held. The three controls and all 15 earlier rows stayed green.11b7ea3e79e2,1e716a6e53fa,4837e2246a13), the anchor count is 1 in each, andgit diff HEADover the three paths is 0 bytes.Gates
At the final head
dd6fa55374, in this worktree:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 63 commands. I ran each one and captured its exit code before any pipe.check:dual-build-cjs-loadsandcheck:i18n-coveragefirst answered PREREQUISITE NOT MET (exit 3: nodist/for packages outside the CLI closure). Afterpnpm turbo run build --filter=!@objectstack/docs, both exited 0.check:i18n-coverageprintedOK (13 config(s), 621 baselined untranslated string(s), none new).--ranreconciliation:Run reconciliation — 63 derived, 63 run, 0 NOT-MEASURED, 0 UNRUN, a derived zero (all 63 carry an exit code).pnpm lint(eslint . --no-inline-config, the whole repo): exit 0 atdd6fa55374.Acceptance notes
content/docsfinds no page describing theconversionsfield of a failure payload, so no doc sentence was made false.TMPDIRnote on PR fix(cli): os lint --json reports the ADR-0087 conversions defineStack applied #20617 does not apply here: this run is on Linux.Generated by Claude Code