Uh oh!
There was an error while loading. Please reload this page.
feat(cli): carry the computed advisory lists on every os build --json failure exit - #12079
Conversation
…n` failure exit (#11772) `warnings` lived on the terminal success payload only (plus `ruleAdvisories` alone on the author-time-rules failure), while the text face prints its advisory blocks — the #11529 author-time advisories at 3b and the #3786 undeclared authoring-key findings at 3d — several gates earlier, each ending in `— re-run with --json for the full list`. A build that then failed at a later gate emitted that gate's failure payload, and none of those carried the list: the remedy the notice named returned a payload without the withheld entries in it (the #11643 / #11391 "the remedy named is unreachable" shape). Maintainer ruling 2026-08-25, option 1 of three: every `emitJson` failure exit carries the lists the run has ALREADY COMPUTED, so `warnings` means the same thing on every exit. Option 2 (shape depends on how far the run got) and option 3 (weaken the pointer) were both rejected. Nine failure exits, three more than the filing card's table listed — it missed the protocol-parse exit, the `--no-runtime-bundle` refusal and the bottom catch-all. Every one now reads a single `warningsSoFar()` site, which the success payload reads too, so the member order (`os validate --json`'s, minus its trailing `structuralWarnings`) cannot drift between exits. Carrying, not computing: each list stays computed at the step that owns it, so an exit upstream of a step reports that list empty rather than paying for a computation it had not already done. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 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 395ef9ecfd5adbbdbcbdb60557ff6a4cb622b272 && git checkout 395ef9ecfd5adbbdbcbdb60557ff6a4cb622b272
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e0bf8049dfcef2ac29c43cfa15f9963c3b921a0 6da672d43dd14e8623eba1dbefc1c1cec9a3b44b && git checkout -B drift-repro 0e0bf8049dfcef2ac29c43cfa15f9963c3b921a0 && git merge --no-ff 6da672d43dd14e8623eba1dbefc1c1cec9a3b44b
node scripts/docs-audit/affected-docs.mjs --json 0e0bf8049dfcef2ac29c43cfa15f9963c3b921a0
|
…type (#11772) `Test Core (2/6)` failed on `packages/lint/src/authoring-rule-wiring.test.ts:291` -- "os build imports no unratcheted symbol from @objectstack/lint": AssertionError: compile.ts imports AuthoringFinding from @objectstack/lint directly. Register the rule in AUTHORING_RULES, or add the symbol to LINT_IMPORT_RATCHET with a reason. expected [ 'AuthoringFinding' ] to deeply equal [] WHY THIS CARD TRIPPED IT. Hoisting the four advisory lists out of the `try` so every `emitJson` failure exit can read `warningsSoFar()` turned one inferred binding into a declared one. Before the hoist the list arrived destructured -- `const { errors: ruleErrors, advisories: ruleAdvisories } = splitBySeverity(...)` -- and its type was inferred, so no name was imported. A `let` declared ahead of the assignment needs a written type, and the first spelling reached for the underlying finding type, adding `AuthoringFinding` to compile.ts's import list. The #4409 import scan reads that list and had never been told about the symbol. `import type` does not escape the scan, and that is deliberate: `lintImportsIn()` matches `/import\s+(?:type\s+)?\{([^}]*)\}\s*from\s*['"]@objectstack\/lint['"]/g` and then strips a per-name `type ` prefix, so both the statement-level and the inline modifier are seen. The guard is asking which SYMBOLS this command file names, not which of them survive to runtime. THE FIX. Bind the annotation to the function that produces the value: let ruleAdvisories: ReturnType<typeof splitBySeverity>['advisories'] = []; `splitBySeverity` is already an import this file makes and already carries a LINT_IMPORT_RATCHET entry ("Pure partition of a finding list into gating vs advisory. Carries no rule identity at all."), so no symbol is added and no exemption is widened. It resolves to exactly the same type the old annotation spelled -- `AuthoringFinding[]`, the function is sync so no `Awaited` is involved -- and `tsc --noEmit` is clean. WHY THIS IS NOT A GATE BYPASS. Nothing was added to LINT_IMPORT_RATCHET or to AUTHORING_RULES, no assertion in authoring-rule-wiring.test.ts was touched, and no test was skipped or relaxed. The ratchet's exemption set is byte-identical before and after; the import list it scans is what shrank. The annotation is strictly tighter than the one it replaces: it is now pinned to `splitBySeverity`'s declared shape, so if that function's `advisories` member ever changes type this binding follows it instead of silently disagreeing -- the same "one list cannot drift from itself" idiom this card applied to the member ORDER of `warningsSoFar()`, now applied to the list's TYPE. Behaviour is unchanged: type-only edit, no emitted JS differs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
os-zhuang
commented
Aug 25, 2026
CI repair: |
| check | before | after |
|---|---|---|
pnpm --filter @objectstack/lint test | Test Files 1 failed (1) · Tests 1 failed | 25 passed (26) | Test Files 81 passed (81) · Tests 2294 passed (2294) |
pnpm --filter @objectstack/cli test | — | Test Files 178 passed (178) · Tests 2025 passed (2025) |
pnpm --filter @objectstack/cli typecheck (tsc --noEmit) | — | exit 0, no diagnostics |
eslint --no-inline-config on compile.ts | — | "errorCount":0,"warningCount":0 |
The two structural pins in build-json-failure-warnings.e2e.test.ts (const warningsSoFar = () => [ and the spread-order regex) are untouched by this edit and passed: vitest list --filesOnly collects exactly 178 files including test/build-json-failure-warnings.e2e.test.ts, and all 178 passed.
Measured, untouched: packages/cli/src/utils/scaffold-validate.ts also imports type AuthoringFinding from @objectstack/lint and is green — the guard scans only packages/cli/src/commands/{validate,compile,lint}.ts, so that file is outside its reach. Not this card's business; flagging it only so the asymmetry isn't read as an oversight here.
One environment difference worth stating rather than smoothing over: CI's shard reported 2288 passed | 5 skipped, the local run 2294 passed with no skips — the 5 skipped upstream ran and passed locally.
Generated by Claude Code
Generated by Claude Code
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11772
Maintainer ruling 2026-08-25 (
5404971104, verbatim 「其他接受」): option 1 — everyemitJson(...)failure exit incompile.tscarries the advisory lists the run has already computed, sowarningsmeans the same thing on every exit and the text face's— re-run with --json for the full listpointer is true regardless of which later gate fails. Option 2 (payload shape depends on how far the run got) rejected; option 3 (weaken the pointer) dead.Re-derived at
origin/mainc804f0ca5— the card's table was short by threeThe dispatch flagged that the ruling's list of exits was not guaranteed exhaustive. Enumerated from source rather than from the card:
compile.tshas 10emitJsoncall sites — 9 failure exits and 1 success payload. The filing card's table listed six failure exits. The three it missed:errors, noterror);--no-runtime-bundlerefusal (step 4b);catch (error)), whose payload is reached by any throw — including the artifact write.The catch-all is the one worth calling out: the four lists were block-scoped
consts inside thetry, so they were not merely unpublished there, they were out of scope. That exit is reached with everything computed (e.g. an unwritable artifact path), which is exactly the scenario the truncation pointer promises to serve.What each exit carries now
warningsis present on all 10 payloads. Contents are what the run had computed at that point — never more:warningsbeforestrict-body: missing body(2b)issues[]errors[]author-time rules failed(3b)issuesruleAdvisoriescapability provider preflight failed(3c)issuesaccess matrix drift(3e)changesdocs validation failed(3f)issues--no-runtime-bundlerefusal (4b)errorruntime bundle failed(4b)errorerrorCarrying, not computing. Each list stays computed at the step that owns it; the change only hoists the four bindings out of the
tryand adds onewarningsSoFar()ordering site. No computation moved earlier, so no failure path pays for work it had not already done — and the two exits that run before any advisory step report[]because that is the truth about that run, not because the tree is clean. Per the dispatch's instruction, exits where a list is genuinely not computed yet are reported, not forced: that is the-cells above, and it is why 3c carries two lists rather than four.Order now lives at one site that the success payload reads too (
os validate --json's order minus its trailingstructuralWarnings), so member order cannot drift between exits — the same "one list cannot drift from itself" idiom #11643 and #11727 used one list over.Pin evidence — per exit, and failing without the behaviour
packages/cli/test/build-json-failure-warnings.e2e.test.ts(11 assertions). Each failure exit is driven by a fixture carrying a known advisory of each class, and the payload is asserted to carry exactly the classes the run had reached — no fewer and no more. The "no more" half is what distinguishes option 1 from a change that hoisted the computations earlier:The 3b row is a REGRESSION GUARD, not red-before evidence, and is named as such in the file: that exit already published
warnings: ruleAdvisories, and at 3bwarningsSoFar()is exactly[...ruleAdvisories]. Itscap: 0corrected a wrong first draft of my own — the fixture declares the unknown capability token, but the #3366 preflight is one step later, so the hint does not exist yet.Ablation — revert only
packages/cli/src/commands/compile.tstoc804f0ca5, keep the pins, run. Mutation and restore each confirmed on disk with anchored greps in both directions before the run (warnings: warningsSoFar()10 → 0 → 10; the one-line four-list spread 0 → 1 → 0), and the script carries atrap … EXIT INT TERMrestore:The 9 red are the 7 behavioural failure-exit pins plus both structural pins. The 2 green in both states are the ones named in advance: the 3b regression guard, and the instrument's own positive control.
No
dist/on the measured path, and it is proven, not asserted. These pins drivepackages/cli/bin/run-dev.js, whose header states it is "the SOURCE entry point — same CLI, run fromsrc/through tsx, used by this repo's gates and e2e suites so they do not depend onpackages/cli/disthaving been built".packages/cli/distwas absent for the whole ablation, so the child could not have resolved through it, and the mutation went red with no rebuild. (The contrast case is live in this same package:serve-node-env-production-default.e2e.test.tsdeliberately drives the builtbin/run.js, and it fails withcommand serve not foundon a tree wherepackages/clihas not been built — see below.)The instrument produces a positive before its negative is trusted. The exhaustiveness pin extracts every
emitJsonpayload literal by brace matching; it is first run over synthetic input containing one payload withwarningsand one without, and asserted to find both and flag exactly the one missing it. A "no payload lackswarnings" pass from an extractor that finds nothing would otherwise read the same.Clause ② — assessed against what was built: yes
The
--jsonmachine contract widens on the failure exits. A consumer that branches onwarningsbeing absent from a failure payload sees a different shape after this change.needs:contract-reviewis hung on this PR and on the card; ⛔ this seat does not clear it.The changeset names which exits gain the field and what a consumer keying off its absence must do instead — read
success === falsefor "is this a failure payload", and ⛔ never readwarnings: []on a failure payload as "this tree raises no advisories" (it means the run stopped before those advisories were computed).Checks — each gate's own verdict line
Gate family re-derived from the actual change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(the script derives its own change set; 21 gates over 4 paths).All quoted from each gate's own verdict line, at
8c7bf1249(the final commit;git rev-parse --short HEAD), with exit codes captured before any pipe.Full-repo lint — the standing ask, run whole rather than narrowed:
Package checks
⛔ Two gates initially refused rather than failed, and
refuse ≠ pass— so neither is reported from that state. Both were missing-build preconditions that CI satisfies and this worktree did not, and both were driven to a real measurement before being quoted above:check:i18n-coveragefirst printedCOULD NOT MEASURE — 1 of 12 config(s) failed to lint(@objectstack/connector-mcphad no build output), with its own note that the result "says NOTHING about whether any declared label went untranslated". Building that one package turned it into theOK (12 config(s) … none new)line above.check:type-check-debtfirst threw--re-measure cannot run: 1 workspace dependenc(ies) … have no built type entry point on disk -- @objectstack/service-knowledge. Building it produced the re-measured OK line above.⛔ The
TEST_DEBT/ type-check-debt ledger was not touched (#11788 owns it this round). The gate notes@objectstack/plugin-approvalscould be lowered by 1; that is left for its owner rather than absorbed here.The one test failure I saw, and why it is not this change.
serve-node-env-production-default.e2e.test.tsfailed withcommand serve not foundwhilepackages/cli/distwas absent. That file's own header states it spawns the shippedbin/run.jsand that "bin/run.jsplus a genuinely builtdist/is the only shape" it works in. Building@objectstack/climade it pass 3/3 — so it was this worktree's missing build, measured, not assumed, and independent ofcompile.ts.Out-of-scope finding
os validate --jsondrops advisory lists it has already computed on all five of its failure exits #12047 (unassigned, unlabelled):os validate --jsondrops advisory lists it has already computed on all five of its failure exits — including its parse-failure exit, which dropsunknownKeyWarningseven thoughvalidate.tscomputes that list pre-parse specifically so it survives a schema error. Same defect class, but extending this widening to a second command's public--jsonpayload is its own Clause-② decision and not a mechanical port of a ruling worded forcompile.ts, so it is recorded rather than fixed here.os build --json's failure payloads carry nowarnings, so the truncation notice's--jsonpointer is a dead end when a later gate fails #11772 is not addressed by that issue.⛔ Left for the reviewer, per the dispatch: this PR stays draft, auto-merge is not enabled, and
needs:contract-reviewis not cleared by this seat — the chain clears it and records its verdict on the card.Generated by Claude Code
Generated by Claude Code