You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 87847a2
Browse filesBrowse the repository at this point in the historyBrowse files
fix(cli): os validate / build / lint --json report the conversions a refusing defineStack applied, beside the refusal (#20926)
Fixes#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 #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
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 (#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
#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 #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>
**When a `defineStack` or `composeStacks` call converts a deprecated spelling and then refuses the config, `objectstack validate --json`, `objectstack build --json` and `objectstack lint --json` now report both the refusal and the ADR-0087 conversions it applied.**
6
+
7
+
`defineStack` rewrites a deprecated metadata spelling to its canonical shape when the config loads, such as `description` on a `page:header` component (canonical `subtitle`). When the same call then refused the config, for example on an unknown `requires` token (`STACK_CAPABILITY_UNKNOWN`), each of the three commands exited 1 with the refusal's `error` and `code` and with `conversions: []`. The conversion reached stderr only, as a warn-once line.
8
+
9
+
The refusal now carries the conversions the producer applied before it refused (`stackConversionsOf(error)` in `@objectstack/spec`), and each command adds them to the `conversions` list of its failure payload, beside the refusal. Each conversion is listed once. A refusal whose source needed no conversion, and any other failure at load, still answers `conversions: []`.
10
+
11
+
Nothing is accepted or refused differently: the exit code, `error`, `code` and every other key of each payload are unchanged, and no key is added. The text face is unchanged.
0 commit comments