Repository navigation
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
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
… so an honest new exit cannot red `main` at night (objectstack-ai#18966) Fixes objectstack-ai#18894 Clause-②: no ## What this changes `packages/cli/test/build-json-failure-warnings.e2e.test.ts` and `packages/cli/test/build-json-failure-conversions.e2e.test.ts` each froze three integers read off `packages/cli/src/commands/compile.ts` — 11 payload literals, 10 `success: false`, 1 `success: true`. **All six were correct**, and that is the defect: the *shape*, not a stale number. One honest new exit — the very thing these two files exist to enforce — turns all six red, and because both files are nightly-tier by NAME they turn red only at night, hours after the PR that did it has merged. That is objectstack-ai#18848's `priority:p1` pre-staged on `compile.ts`. Ported from the shape PR objectstack-ai#18880 landed on the sibling `validate-json-failure-*` pair: - the count is **derived** — `toHaveLength(callSites.length)` against a word-boundary `emitJson` call-site match over **the same masked source** the payload extractor reads; - the old integer is demoted to a **floor**, `toBeGreaterThanOrEqual(10)`, which rots only in the exits-are-REMOVED direction; - one **partition** assertion (`success: true` ×1, `success: false` = `literals.length - 1`) replaces the two frozen counts. ⛔ No production file is touched. The diff is those two test files and nothing else — `compile.ts` is card objectstack-ai#18431's carrier (branch `ccdc9f5389`) and is left alone. ## Premises, re-measured on `origin/main` before a line was written Re-derived with the tests' OWN extractor against the masked `compile.ts` at `f112a740bd` (`origin/main` at branch point), ⛔ not inherited from the card: ``` payload literals 11 the pins' 11 was correct success: false 10 correct success: true 1 correct /\bemitJson\s*\(/g 11 symmetric with the extractor ``` **Collection, re-measured in this worktree** (`vitest list --filesOnly`, `packages/cli`): `OS_TEST_TIERS` unset ⇒ 267 files, **0** of these two among them, lit control `artifact-packages.test.ts` present; `=nightly` ⇒ 68 files, **both** present, lit control absent. Both files sit in the `integration` project (`--project unit` collects 0 of them). ##⚠️ The trap this card names — the verdict key, MEASURED not copied **`compile.ts` spells its verdict key `success:`. `validate.ts` spells it `valid:`.** Measured over the 11 masked payload literals of `compile.ts`: | needle | count | |:--|--:| | `success: false` | 10 | | `success: true` | 1 | | `valid: false` | **0** | | `valid: true` | **0** | A verbatim port of objectstack-ai#18880's two partition lines would have asserted over an **empty population**. The lines here read `success:`, and the reading is recorded in the code comment beside them so the next author does not have to re-derive it. ## The symmetry the derived form needs — every `emitJson` occurrence enumerated The looser `/\bemitJson\s*\(/g` can only be trusted equal to the extractor's `await emitJson(` needle if nothing else in the file matches it. Enumerated with left context on `compile.ts`, **13 raw occurrences**: | where | reading | |:--|:--| | `:43` | `emitJson,` — an **import** from `../utils/format.js`. No paren ⇒ matches neither side. | | `:104`, `:694` | prose inside comments — the shared `maskComments` blanks both (objectstack-ai#18520, applied to these two files by objectstack-ai#18924) | | the other 10 | `await emitJson(` call sites, plus the multi-line ones ⇒ 11 payload literals | ⭐ `emitJson` is **IMPORTED here, never declared** — there is no `function emitJson(` in `compile.ts` (grep: zero hits for `function emitJson` / `const emitJson`), so the looser pattern cannot over-count and red the pin by construction. Raw and masked agree: 11 literals, 11 call sites, both ways. That agreement is a **reading, not a law**, and the derived equality is exactly where a future file that broke it would report. ## The floor's anchor — 10, and why not 11 objectstack-ai#18880 chose "the population the governing ruling was made over", not today's count. The equivalent anchor here is the commit that landed each file's ruling, read out of git: | file | ruling | landed by | count pinned that day | |:--|:--|:--|--:| | `build-json-failure-warnings.e2e.test.ts` | maintainer, 2026-08-25, option 1 (objectstack-ai#11772) | `d114d5e140` (objectstack-ai#12079) | `toHaveLength(10)` — 9 failure + 1 success | | `build-json-failure-conversions.e2e.test.ts` | the same 2026-08-25 ruling applied to `conversions` (objectstack-ai#12125) | `79cf692b0e` (objectstack-ai#12285) | `toHaveLength(10)` — "all nine of its failure exits" + 1 | Control for the anchor rule: the sibling `validate-json-failure-warnings.e2e.test.ts` was created at `33e81a5fe0` (objectstack-ai#12130) pinning `toHaveLength(6)`, and objectstack-ai#18880's floor is `6`. Same rule, same arithmetic. ⇒ **the floor here is 10.** Today's eleventh exit is ADR-0130 D4's per-package author-time rule leg (objectstack-ai#14439), which arrived *after* the ruling — exactly what a derived count absorbs and a frozen one trips over. ## Ablation battery — every leg's mutation proven on disk, every restore proven by hash Harness: a bash trap on EXIT, INT and TERM whose handler is the restore function, absolute paths, restore is `git checkout HEAD -- PATH` (never a bare `git checkout --`), verified by comparing `git hash-object` against the HEAD blob hash **and** by `git diff HEAD` being empty. Each leg prints the extractor's counts BEFORE and AFTER the mutation and a marker grep, so a no-op leg is refused rather than read as green. All runs `OS_TEST_TIERS=nightly`, exit codes captured before any pipe. No rebuild is needed or done: both files read `compile.ts` from disk and the CLI is loaded from `src/` through `bin/run-dev.js`, as each file's own header states. | leg | mutation to `compile.ts` | measured on disk after | THIS branch | the frozen `11/10/1` | |:--|:--|:--|:--|:--| | 0 control | none | literals 11 · sites 11 | **GREEN** 7/7 | — | | A | honest 12th exit carrying **both** keys | literals 12 · sites 12 | **GREEN** 7/7 | **RED** — `expected [ …(12) ] to have a length of 11` (both files) | | B | 12th exit **missing** both keys | literals 12 · sites 12 | **RED** — `an os build --json exit publishes no warnings` / `no conversions` | — | | C | 12th call site spelled **without `await`** | literals 11 · sites **12** | **RED** — `an emitJson( call site the payload extractor could not read: expected [ …(11) ] to have a length of 12` | **GREEN** 7/7 | | D | two exits deleted (truncation) | literals 9 · sites 9 | **RED** — `publishes fewer exits than the ruling above was made over: expected 9 to be greater than or equal to 10` | — | ⭐ **Leg C is the one that decides it.** A twelfth call site the payload extractor cannot read leaves the frozen integer **green** and reddens the derived equality. The frozen form is not a parallel option — it is strictly weaker. Leg A is the mirror: the honest exit this contract exists to welcome now stays green instead of reddening `main` at 20:44Z. The "old" arms of A and C ran the pre-change files restored worktree-only with `git restore --source=f112a740bd -- PATH`, hashes printed and matched against the BASE blobs, then reset with `git checkout HEAD -- PATH`. ## `skip-changeset`, by measurement with controls both ways `packages/cli` `files[]` is `["dist","README.md","CHANGELOG.md"]`; `tsconfig.build.json` has `rootDir: "src"`, `include: ["src"]` and an `exclude` that drops every `*.test.ts`. Measured against the freshly built `packages/cli/dist`: | probe | direction | hits in `dist/` | |:--|:--|--:| | `an emitJson( call site the payload extractor could not read` | new, this diff | **0** | | `publishes fewer exits than the ruling above was made over` | new, this diff | **0** | | `ABLATION` | new, this diff | **0** | | `access matrix drift` | positive control, lives in `src/commands/compile.ts` | 1 | | `warningsSoFar` | positive control | 2 | | `runtime bundle failed` | positive control | 1 | | `build-json-failure` |⚠️ 1 hit — `dist/commands/compile.js:621`, a **comment** compiled from `src/commands/compile.ts:698` that names the test file. Not this diff's content; `compile.ts` is untouched. | 1 | The positive controls prove the grep was reading `dist`. Nothing this diff adds reaches any published path from any released package ⇒ `skip-changeset`, label applied on this PR. ##⚠️ The adjacent axis — a POSITION, not a silent widening Comment 5726117707 on the card asked for a judgement on the byte-exact multi-line spread ORDER pin, and warned its own probe was narrow. **Re-derived here with a different predicate** (`expect(SRC)` / `SRC.match` / `SRC.indexOf` across all four files), which finds MORE than the seat's word probe did — including one in `validate-json-failure-conversions.e2e.test.ts`, where the seat's probe scored 0 and recorded it as **未判**: | file | line | what it binds byte-exactly | |:--|--:|:--| | `build-json-failure-warnings.e2e.test.ts` | `:536` `:537` | `const warningsSoFar = () => [` , and the 4-member spread ORDER | | `build-json-failure-conversions.e2e.test.ts` | `:450` | sink declaration + blank line + `try {` adjacency | | `validate-json-failure-warnings.e2e.test.ts` | `:571` `:572` | the same two, 5 members | | `validate-json-failure-conversions.e2e.test.ts` | `:486` | the same adjacency ⇐ **the seat's 0 was 未判, not absent** | **Judgement: named, measured, and left to the seat — deliberately NOT done in this PR.** Against the lane's standing three-rung ruling (claim 5725386686 on objectstack-ai#18520): - **Rung 1** — 「⛔ nightly-only 的 pin 永不绑一个逐 PR 兄弟已经绑着的字节精确拼写。」 Its precondition is measured **FALSE**. Repo-wide, the only other reader of those four list identifiers is `build-json-advisory-parity.e2e.test.ts`, which is itself **nightly-tier** and compares with `new Set(...)` — order-blind by construction. No per-PR sibling binds this order behaviourally or textually. ⇒ rung 1 does not authorise deleting it. - **Rung 2** — 「⭐ nightly-only 的 pin 绑那条性质本身,从源码推导 —— 读出来的计数、分区、下限 —— ⛔ 永不绑一个写死的数。」 It governs frozen **numbers**. The order pin binds a *spelling*. ⇒ rung 2 does not reach it; this PR discharges rung 2 for the counts, which is this card. - **Rung 3** — 「⭐ 一个读原始源码文本的读者,要么能把代码和散文分开,要么就不该对位置/顺序下 断言。」 Already satisfied: PR objectstack-ai#18924 put the shared `maskComments` in place on both files. ⛔ Not re-added here. ⇒ Under the ruling **as written and as measured**, the order pin in these two files is currently compliant and needs no change. What is *not* settled is a rung-1 **extension**: may a nightly-only pin bind a byte-exact spelling when **no** per-PR sibling binds the property at all? That is a new question, not an application of the existing three rungs, and it sits identically on the `validate-json-failure-*` pair, **which has no carrier card**. Widening this PR to answer it would convert half a family under a rule nobody has made yet. Handed to the `domain:cli` seat as a finding with dedupe words. ## Verification Measured on `git rev-parse --short HEAD` = `0eb90c0a91`, in worktree `/home/user/objectstack-issue-18894`, branched from `origin/main` at `f112a740bd`. | what | how | result | |:--|:--|:--| | the two files, BEFORE the edit | `OS_TEST_TIERS=nightly … --project integration` on both files | **27 passed (27)** — the pins were correct, as the card said | | the two files, AFTER the edit | same | **27 passed (27)** | | `packages/cli` WHOLE, default tier | `pnpm --filter @objectstack/cli test` (bare `vitest run`, ⛔ no `--project`) | **267 files / 3485 tests passed** | | `packages/cli` WHOLE, nightly tier | `OS_TEST_TIERS=nightly pnpm --filter @objectstack/cli test` | **68 files / 695 tests passed** — the tier that actually collects these two files | | typecheck | `pnpm --filter @objectstack/cli typecheck` | exit 0 (incl. `check:test-typecheck`) | | dependency closure + package build | `pnpm --filter '@objectstack/cli^...' build` then `pnpm --filter @objectstack/cli build` | exit 0 both — required, several pins refuse at load without it | | gate families | `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, then `--ran` with exit codes | 47 derived, **46 run green**, **1 NOT MEASURED** | | whole-repo lint | `pnpm lint` (`eslint . --no-inline-config`), NOT narrowed | exit 0 | **NOT MEASURED (1)**: `pnpm check:dual-build-cjs-loads` exits **3** — `PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/`. It needs a whole-repo `pnpm build`; this worktree built only the `@objectstack/cli` closure. ⛔ Recorded as NOT MEASURED, not as a pass and not as a red. CI's `Build Core` job builds the tree and measures it there. `dispatch-gates --ran` classified it from the recorded exit code rather than from a claim. Two derivation caveats stated rather than hidden: `dispatch-gates` printed a **STALE TREE** warning (`origin/main` moved 2 commits under this branch, touching `scripts/check-adr-0087-registration.mjs` and `scripts/check-single-claim-paths.mjs`); the command list was re-derived after `git fetch origin main` and came back **byte-identical**, so the 47 stands. And the tool's own footer is quoted rather than paraphrased: the 47 is not a complete account of CI — 51 artifact-roster families, 11 wide-population families, 13 pending-changeset families and 5 path-scheduled CI jobs are outside it by construction. ## Acceptance notes Noted, not filed — observations from the two files, none of them a reproducible defect, a contract violation, or a metadata-authoring trap: - The `describe` blocks in both files are still titled with their originating card numbers (`objectstack-ai#11772`, `objectstack-ai#12125`) while the assertions inside them now answer objectstack-ai#18894 too. Cosmetic; renaming a `describe` moves every `-t` filter that names it. Carrier: whoever next opens these files for a behavioural change. - `packages/cli/test/build-json-advisory-parity.e2e.test.ts` is nightly-tier and is the only other reader of the four advisory-list identifiers; its `new Set(...)` comparisons are deliberately order-blind, so the family has **no** order coverage in the per-PR tier at all. Recorded as the measurement behind the adjacent-axis position above, not as a defect. Carrier: the `domain:cli` seat, together with the rung-1 extension question. --- _Generated by [Claude Code](https://claude.ai/code/session_01DvvamiacK328idtBYJBxV3)_ Co-authored-by: Claude <noreply@anthropic.com>
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