Repository navigation
fix(cli): os lint --json reports the ADR-0087 conversions defineStack applied - #20617
objectstack-fleet[bot] merged 3 commits into
Conversation
…pplied into conversions os lint filled conversions only from its own normalizeStackInput pass over the loaded config, which a defineStack default export hands over already canonical, so the notice reached stderr alone. Fold LoadedConfig.stackConversions right after loadConfig, the step 1b fold os validate / os build make. No one-authoring-shape refusal is added: an unbuilt export carries no record and converts through the pass as before. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…applied Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…nversions-lint-and-refusal
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. 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): |
Contract reviewServed-tier: Inputs, and nothing else: card #20583 (body and all 4 comments: triage Check-runs at read (newest per name, 31 names): 21 completed, 18 success and 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke); 10 in progress (Dogfood Regression Gate 1/3 to 3/3, Lint and Repo Gates, Test Core 1/6 to 4/6, Type Check consumer gates, Type Check workspace); 0 failed, 0 queued. Success includes Build Core, Test Core 5/6 and 6/6, Type Check source gates and debt ledger, Dogfood Verify CLI, Temporal Conformance, Check Changeset, Check PR Size, both single-claim guards, ① Derived judgmentsThe diff is one executable line in (1) (2) A conversion cannot be listed twice, and the pins prove exactly-once. RIGHT, for the driven conversion. The one array has two fillers: the fold (what the producer converted, recorded on the default export before the spread drops it) and the pass over the merged (3) The Public surface: no export, flag, error code or schema key moves. ② Semver level
③ Boundary flagsDev deviations (
PR-body flag: verification ran UNLOCKED (no Escalations: none. Implemented-by: VERDICT: PASS |
…refusing defineStack applied, beside the refusal (objectstack-ai#20926) Fixes objectstack-ai#20583 Clause-②: no ## What this lands: location 2's CLI half A `defineStack` (or `composeStacks`) call that converts an ADR-0087 D2 spelling and then refuses now reports both on `os validate --json`, `os build --json` and `os lint --json`: the refusal's `error` and `code`, and the conversions the producer applied before it refused, in `conversions`. The spec half landed in PR objectstack-ai#20651 (objectstack-ai#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 answer `5886671384` kept on this card: one line in each of the three catch-alls. - `validate.ts`, `compile.ts` (`os build` inherits it) and `lint.ts`: inside the catch-all's `--json` branch, `conversionNotices.push(...stackConversionsOf(error))` runs before `emitJson`. The payload still reads the one shared sink (`conversions: conversionNotices`), which the nightly source-scan pins require of every exit. - **Folded, never recomputed.** There is no second conversion pass over the authored source and no reading of the producer's stderr line. Both were declined as consumer-side reconstructions (`5886671384`). - **Exactly once, by construction.** None of the three commands calls a stack producer itself (`git grep` over `packages/cli/src`: every `defineStack(` / `composeStacks(` hit is prose or scaffold text; the door dependencies `packages/lint/src` and `packages/objectql/src` hit 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's `loaded.stackConversions` fold and step 2's own pass). `stackConversionsOf` answers `[]` for every other throw: a plain `Error`, this CLI's own refusals (`refuseUnbuiltStack`, `ConfigRefusalError`) and non-refusal throws. - **`loadConfig` passes the refusal through untouched,** so `packages/cli/src/utils/config.ts` is not edited. Measured at `165c1d49e3` by calling `loadConfig` from source on the fixture below. The caught error is a `StackCapabilityUnknownError` (`code` `STACK_CAPABILITY_UNKNOWN`, `instanceof Error`) that carries the own symbol `objectstack.stack.conversions`, and `stackConversionsOf(error)` answers the one `page-header-subtitle-alias` notice. The canonical control answers `[]`. - The text face does not change. The fold sits inside the `--json` branch, and the producer's own stderr line is unchanged. - The comments that said "a throw at load reports `[]`" now name the one exception. `validate.ts`'s hoist note counts three fillers. - `test/validate-build-gate-parity.test.ts`: `stackConversionsOf` is now a bare call site in both `compile.ts` and `validate.ts`, so the closed call-site ledger needed a row for it. It gets its own `NOT_A_GATE` reason ("carries the conversion record a stack producer stamped on the refusal it threw ... refuses nothing"), not the nearest bucket. `os lint` still 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 (objectstack-ai#20367's OQ3 stays unopened). ## Measurements: the three doors through `bin/run-dev.js` (CLI from source) The fixture is a strict `defineStack` whose `page:header` authors `description` (the live conversion `page-header-subtitle-alias`) and which then declares `requires: ['no-such-capability']`. The control is the same config with `subtitle`. | run | before (`165c1d49e3`) | after (fold applied) | |:--|:--|:--| | `os validate --json`, convert then refuse | exit 1, `STACK_CAPABILITY_UNKNOWN`, `conversions: []`, 1 producer stderr line | exit 1, `STACK_CAPABILITY_UNKNOWN`, `conversions` = the one notice, 1 stderr line | | `os build --json`, the same config | exit 1, `STACK_CAPABILITY_UNKNOWN`, `conversions: []`, 1 stderr line | exit 1, `STACK_CAPABILITY_UNKNOWN`, the one notice, 1 stderr line | | `os lint --json`, the same config | exit 1, `STACK_CAPABILITY_UNKNOWN`, `conversions: []`, 1 stderr line | exit 1, `STACK_CAPABILITY_UNKNOWN`, the one notice, 1 stderr line | | all three, the canonical control | exit 1, `STACK_CAPABILITY_UNKNOWN`, `conversions: []`, 0 stderr lines | unchanged | The notice is `page-header-subtitle-alias` at `pages[0].regions[0].components[0].properties.subtitle`, `description` to `subtitle`, `retiresIn` 18. The "after" column was also read at the final head: at `dd6fa55374` all six rows answer exactly as in the table. ## Tests - **The pins,** in `packages/cli/test/stack-conversion-record-door.test.ts` (the per-PR `integration` tier; `*.e2e.*` files run nightly only, which is why the objectstack-ai#20617 lint rows live here too). A new block drives triage's convert-then-refuse fixture through each door. Each row asserts exit 1, `code` `STACK_CAPABILITY_UNKNOWN`, the catch-all's `error` string plus the door's own verdict key (`valid: false` / `success: false`), and `conversions` equal to exactly the one notice. The control (canonical then refuse) asserts the same envelope with `conversions: []`. That is 6 new rows, and the file now holds 21. - At the final head `dd6fa55374`, through `scripts/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). - The nightly pins that read these catch-alls were run too, because the diff edits exactly what they assert. At `dd6fa55374`, `OS_TEST_TIERS=nightly vitest run` over `validate-json-failure-conversions.e2e.test.ts`, `build-json-failure-conversions.e2e.test.ts` and `lint-conversion-notices.e2e.test.ts` gave `Test Files 3 passed (3)` / `Tests 35 passed (35)`. That covers every exit still reading the one sink, the load-throw rows (a plain `Error`) still answering `[]`, and the normalize call still below `loadConfig`. - The first unit run, at `649236e024`, failed one test: `validate-build-gate-parity.test.ts` found the new call site unclassified. `dd6fa55374` adds 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 of `node scripts/ablation-replace.mjs`, plus a shell `trap` restoring the three absolute paths with `git checkout HEAD --`. The CLI loads these files from `src/` through tsx, so no `dist/` sits on the measured path and no rebuild was needed. - **Landed on disk:** the anchor `conversionNotices.push(...stackConversionsOf(error));` went from 1 to 0 in each file. The blobs moved: `validate.ts` `11b7ea3e79e2` to `f744edd59909`, `compile.ts` `1e716a6e53fa` to `3f653b1a82ba`, `lint.ts` `4837e2246a13` to `5df926e26a3e`. - **Direction observed: red, and exactly where expected.** The result was `Tests 3 failed | 18 passed (21)`. The three red rows are the convert-then-refuse rows on `validate`, `build` and `lint`, each failing on `conversions`: `expected [] to deeply equal [ { …(5) } ]`. Their exit and `code` assertions held. The three controls and all 15 earlier rows stayed green. - **Restore proven:** each file's blob equals HEAD again (`11b7ea3e79e2`, `1e716a6e53fa`, `4837e2246a13`), the anchor count is 1 in each, and `git diff HEAD` over 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 --commands` derived 63 commands. I ran each one and captured its exit code before any pipe. - 61 exited 0 on the first run. - `check:dual-build-cjs-loads` and `check:i18n-coverage` first answered PREREQUISITE NOT MET (exit 3: no `dist/` for packages outside the CLI closure). After `pnpm turbo run build --filter=!@objectstack/docs`, both exited 0. `check:i18n-coverage` printed `OK (13 config(s), 621 baselined untranslated string(s), none new)`. - `--ran` reconciliation: `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 at `dd6fa55374`. - Remote CI: not waited on. ## Acceptance notes - No hand-written docs page is touched. A grep of `content/docs` finds no page describing the `conversions` field of a failure payload, so no doc sentence was made false. - The macOS `TMPDIR` note on PR objectstack-ai#20617 does not apply here: this run is on Linux. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20583
Clause-②: no
What this lands: location 1 only
os lint --jsonnow foldsLoadedConfig.stackConversionsinto itsconversionslist, right afterloadConfig: the same one-line foldvalidate.ts/compile.tsmake at their step 1b since PR #20579.defineStackconverts at load, so the configos lintreceived was already canonical and its ownnormalizeStackInputpass found nothing of the default export to convert. The notice reached stderr alone.os lintkeeps accepting exactly what it accepts today. There is norefuseUnbuiltStackhere and none is implied; the one-shape rule is not extended to this command. An unbuilt default export carries no record, sostackConversionsOfanswers an empty list for it and the command's own pass converts it as before.--jsonkey, andpassed,issues, the counts and the exit code do not move. The text face prints the folded notice in its warning block, asos builddoes.What this leaves: location 2 is a fork, not shipped
A
defineStackthat converts and then refuses still reportsconversions: []onos validate --json,os build --jsonandos lint --json(the third door, measured below). The dispatch's hypothesis was that the CLI can recover those conversions without a spec edit. Measured, it cannot recover them; it can only reconstruct them:loadConfigloses notices.warnConversionNoticeis warn-once per process, keyed on conversion id, path, from and to. The record is not subject to that warn-once.eb4b17c346:composeStacks([defineStack(A), defineStack(B)]), where both carrypage:headerdescriptionatpages[0]. The record carries 2 notices, and stderr carries 1 line.requires: ['no-such-capability']), the one stderr line belongs to A, whosedefineStackdid not refuse. B's own notice was suppressed.surface,toMajor,messageandcodefields, so the structured notice would have to be re-derived from prose. The conversion types document that prose as derived, never the source of truth.normalizeStackInputon the authored argument is a second conversion pass. The CLI would shimdefineStackin every config load, or re-load the module in authored-source mode, and rerun the pass when the call refuses. ThestackConversionsOfTSDoc rules this out: a door never runs a second pass to reconstruct the record. It would also copy the producer's record formula (input record plus pass notices) into the CLI, where it drifts.warnConversionNoticeand its warn-once set are module-private, and the refusal errors (StackRefusalErrorsubclasses) carryissuesonly.So the only channel that is not a workaround is a spec change: the refusal carries the notices it applied.
packages/specbelongs to the spec seat under this dispatch, so no spec edit is made here. The dev report carries the fork, with options.Measurements (CLI from source,
bin/run-dev.js)eb4b17c346)ca74de14aa)os lint --json, the card'spage:headerdescriptioncaseconversions: [], 1 stderr lineconversions= the onepage-header-subtitle-aliasnotice, 1 stderr lineos validate --json, convert-then-refuse (requires: ['no-such-capability'])STACK_CAPABILITY_UNKNOWN,conversions: []os build --json, the same configSTACK_CAPABILITY_UNKNOWN,conversions: []os lint --json, the same configSTACK_CAPABILITY_UNKNOWN,conversions: [](location 2, third door)Tests
packages/cli/test/stack-conversion-record-door.test.tsgains anos lint --jsonblock. It covers the card's plain case, the record across the named-export spread,composeStacks, a key merged from a named export (the pass converts it once, with no producer stderr line), and the canonical control. Every non-empty row asserts exactly one entry. This file is in the per-PRintegrationtier. The existinglint-conversion-notices.e2e.test.tsis*.e2e.*and runs nightly only, which is why the new rows are not in it.ca74de14aa: theunittier passed 234 of 234 files (3342 tests), and the door file passed 15 of 15 tests.pnpm --filter @objectstack/cli typecheckexits 0, and the door file is in the test-layer program (--listFilesOnly).lint-conversion-notices.e2e.test.tsunderOS_TEST_TIERS=nightlypassed 6 of 6 ata80b61dad0. Its unbuiltexport defaultfixtures still lint and convert through the pass.a80b61dad0throughscripts/ablation-replace.mjs(WRAP mode, with a trap). Deleting the fold line took the anchor count from 1 to 0 on disk (blob37bf1203bff7to95dcca7f308a).37bf1203bff7) andgit diff HEADis empty.lint.tsis loaded fromsrc/by the child, so nodist/sits on the measured path.Gates (at
ca74de14aa, after mergingorigin/main)dispatch-gates --commands: 63 commands, all exit 0.dispatch-gates --ranreconciles 63 derived, 63 run, 0 not measured, 0 unrun, each with its exit code. The first runs ofcheck:dual-build-cjs-loadsandcheck:i18n-coverageanswered PREREQUISITE NOT MET (exit 3) before a full build. Both were rerun green afterpnpm turbo run build --filter=!@objectstack/docs.check-closing-target-claim,check-partof-closing-keywordandcheck-single-claim-pathsneed PR context and are rerun against this PR.pnpm lint(the repo-wideeslint . --no-inline-config) exits 0 in 29s.node scripts/check-issue-citations.mjs --base origin/mainexits 0 (1 citation, resolves).Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Acceptance notes
Part of, so merging it leaves the card open for the fork above.os lint's text face now prints the producer's notice in its warning block for adefineStackconfig with a retiring spelling, in the same wording asos build.check:i18n-coveragerunsos lintover the 13 example configs and stays green.test/published-subpath-console.pin.test.tsandtest/published-subpath-hook-body.pin.test.tsfail 5 assertions whenTMPDIRis the/var/folders/...symlink. The resolver answers the/private/var/...realpath. WithTMPDIRset to its realpath, both pass 29 of 29. CI runs on Linux. Carrier: none.content/docs/deployment/cli.mdx's "Warnings checked" list foros validatenames no conversion notices.Generated by Claude Code