Uh oh!
There was an error while loading. Please reload this page.
feat(cli): carry the computed conversions on every os validate --json and os build --json failure exit - #12285
Conversation
…son` and `os build --json` failure exit (#12125) `conversionNotices` is filled by the `onConversionNotice` sink handed to `normalizeStackInput` at step 2 — above the schema parse and above every later gate in both commands — and was then published on the terminal success payload alone. All five `os validate` failure exits and all nine `os build` failure exits dropped a list already in hand. An ADR-0087 D2 notice is the one advisory class carrying an expiry, so a CI job gating on either `--json` face could not see that its tree depends on a conversion about to retire until every unrelated failure was fixed first. The fix is a pure SCOPE change: the sink array is declared above the `try` so the catch-all exit can read it. `normalizeStackInput` still runs at exactly step 2, so a run that throws in `loadConfig` reports `[]` honestly — carrying, never computing earlier. `warnings` and `conversions` are deliberately NOT folded: whether they should become one field is a live question the ruling did not address. Part of #12125 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ
📓 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 — 23 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 cd29847ac195282bea435e117578e2247420328c && git checkout cd29847ac195282bea435e117578e2247420328c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8cdd696592b0c3c7bb7c3caa776f91aa6eb8cb68 2c7f00270803deb48abba3d56725447cd9568d07 && git checkout -B drift-repro 8cdd696592b0c3c7bb7c3caa776f91aa6eb8cb68 && git merge --no-ff 2c7f00270803deb48abba3d56725447cd9568d07
node scripts/docs-audit/affected-docs.mjs --json 8cdd696592b0c3c7bb7c3caa776f91aa6eb8cb68
|
The card measured its fixture with `source: 'hi'`, which no longer reaches the
success exit at current `main`: the `jsx-no-root` authoring rule rejects it
("Expected a single root element"), so the control fixture stopped at the rule
gate instead of proving the conversion fires on a passing run.
`<div>hi</div>` is the same source the ADR-0087 conversion registry uses in its
own fixture for `page-kind-jsx-to-html`.
Part of #12125
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ…s name claims The guard also asserted that the failure payload carries the one notice, which is red before the fix — so a pin named REGRESSION GUARD was in fact red-before evidence. Measured in the ablated tree: it failed for that assertion alone. That claim is the `doc errors` / `3f (package docs)` test's job, on this very fixture. The guard now asserts the FOLD property and nothing else. Part of #12125 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbG3rGVLjZStHQxHDtzJdJ
os-trump
commented
Aug 25, 2026
Contract-review verdict: PASS (#12125)Reviewed by the contract-review chain at Reviewed: the changeset and both source patches in full, the test-file design as described with its ablation record, and the ruling lineage on #12125. Conformance to the family ruling (maintainer 2026-08-25 on #11772/#12047, option 1, extended to
Routed to the maintainer, not blocking: the fold question (A/B/C in the dev report) goes to the decision box with the two implementation inputs (the recorded deliberate-separation comment; #12297's sequencing interest). Landing completed by the chain in the same stroke per the maintainer's standing instruction (2026-08-25, verbatim: 「审核通过你应该直接负责合并吧,还要等原始的项目经理吗」): clearing Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#12125
os validate --jsonandos build --jsondropped theconversionsfield onevery failure exit — the same "computed, then dropped" shape as the
warningsfamily (#11643 / #11391 / #11772 / #12047), one field over.
conversionNoticesis filled by theonConversionNoticesink handed tonormalizeStackInputat step 2, above the schema parse and above everylater gate in both commands. The notice was therefore already in hand at each
failure exit and was then discarded;
conversions:reached the terminalsuccess payload alone.
An ADR-0087 D2 notice is the one advisory class carrying an expiry —
retiresInnames the protocol major where the old shape stops loading. So a CIjob gating on either
--jsonface could not see that its tree depends on aconversion about to retire for as long as the tree also tripped any unrelated
gate.
What changed
conversionsis now present on everyemitJsonexit of both commands: 6 invalidate.ts(5 failure + success) and 10 incompile.ts(9 failure +success). Existing keys are unchanged; the success payloads are unchanged in
content.
The fix is a pure scope change — the sink array is declared above the
tryso the catch-all exit can read it — and
normalizeStackInputstill runs atexactly step 2. ⛔ Carrying, not computing: a run that throws in
loadConfig, above step 2, reports[]honestly, and a pin holds that line.#12047 is not addressed hereand#11772 is not addressed here— both are thewarningsside and already landed; this card is theconversionsfield only.⛔ The open sub-question, NOT settled here
Whether
warningsandconversionsshould be folded into one field is alive question raised by the filer that the 2026-08-25 ruling did not address,
and this change was explicitly not given authority to settle it. The two fields
are deliberately mirrored, not merged, and neither was restructured to make a
future fold easier.
Implementing did not surface any forced fork. The two fields are written
side by side at each exit with no shared site that has to be written twice; the
warningsspread stayswarningsSoFar()andconversionsreads its singlesink directly. A
conversionsSoFar()wrapper was deliberately NOT added:warningsSoFar()exists to state the order of five producers once, and thisfield has exactly one producer.
The
REGRESSION GUARD — warnings and conversions stay separate fieldspin ineach test file records the shape as-shipped so a fold happens deliberately
rather than as a side effect. It asserts the fold property and nothing else,
so it is green in both states — ⛔ not red-before evidence, nor an argument
that folding is wrong.
payload carries the one notice, which made a pin named REGRESSION GUARD into
red-before evidence — and the ablation caught it (it was one of the 25 reds).
That duplicate claim is the
doc errors/3f (package docs)test's job on thesame fixture, so it was removed rather than the name being loosened. Reported
rather than reconciled.
Zone 3 — PM mechanism assumptions, measured
Both assumptions held, and the surface sweep found no third command:
onConversionNoticeis passed at exactly two call sites in the repo outsidepackages/specitself:validate.ts:148andcompile.ts:162.os buildisa thin alias subclass (
build.ts→export default class Build extends Compile), socompile.tsis that whole surface.normalizeStackInputwithout the callback(
info,doctor,lint,i18n/check,i18n/extract,lint/score,scaffold-validate), so they never compute notices at all and publish noconversionsfield. That is a "never computed" shape, not this card's"computed then dropped" one — recorded, not fixed here.
validatefailure exits and all 9compilefailure exits, so every one of them carries the notice. The only honestly
empty exit is a throw at load, above step 2 — pinned as
[]in bothfiles.
⭐ Measured difference from the sibling card: for
warnings,os build'stwo earliest exits (
--strict-bodyand the protocol parse) are empty byconstruction. Step 2 is above both, so
conversionsis populated there.The two fields genuinely disagree on those exits, which is why this field
needed its own measurement rather than inheriting the sibling's table.
Anti-vacuity
⭐ A pin asserting
'conversions' in payloadis green against aconversions: []hard-coded at every exit. So every fixture drives a live conversion —page-kind-jsx-to-html(ADR-0087 D2, protocol 11) onpages[0].kind: 'jsx'—and each exit asserts the array whole against the notice's identity
(
conversionId,surface,from,to,path), which is the "and NO MORE"half.
Both directions of the instrument are proven: a
converts nothingfixture runsthe same exit with the canonical
kind: 'html'and requires[]. A payloadwith the notice hard-coded in passes every other test here and fails that one.
Verification
All runs below are on the final commit
2c7f002708, and every heavy run wentthrough
scripts/pm/os-verify-lock.sh(its ownVERDICTline is the authorityquoted, never a bare
$?).Pins, fixed tree —
Test Files 2 passed (2) · Tests 29 passed (29),VERDICT command-exit 0.Ablation — revert only the two source files to the merge base, keep the
pins, restore under
trap … EXIT INT TERM. Mutation confirmed on disk in bothdirections before any result was read (
conversions: conversionNotices6→1 invalidate.tsand 10→1 incompile.ts; the hoisted 4-space sink 1→0 and thein-
try6-space sink 0→1 in each). Result:Tests 23 failed | 6 passed (29).The 6 green in both states are named as regression guards, ⛔ never as
red-before evidence: the two
controltests (the success payload always carriedthe field), the two extractor self-tests (synthetic instrument controls), and
the two fold guards.
Restore verified after the run —
conversions: conversionNoticesback to 6/10,[#12125]markers present, andgit diff HEADempty.Full-repo lint —
pnpm lint(eslint . --no-inline-config), notnarrowed:
VERDICT command-exit 0.Typecheck —
pnpm --filter @objectstack/cli typecheck: exit 0.Ratchet —
check:type-check-debtfirst refused (--re-measure cannot run: … @objectstack/service-knowledgehas no built type entry point). ⛔refuse ≠ pass, so it was NOT counted; the closure was built with lint.yml'sown command and it was re-run to a real measurement:
check-type-check-coverage --re-measure: OK — 32 ledger entr(ies) re-measured in 191.5s, 1843 raw tsc error(s) total, none above its recorded number. surplus: none.Gate family — re-derived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstackagainst the actual change set (5 paths); all 19runnable families green, re-run on this head after the final commit.
Generated by Claude Code