feat(qa): os test prints suite and scenario names and selects scenarios by --tags - #20341
Conversation
TestResult now carries suiteName, scenarioName and description; os test prints the suite name and each scenario's name beside its id, prints a failed scenario's description, and gains a --tags flag (comma list, any-of) whose deselected scenarios are counted and never reported as passed. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…ger rows live qa.json: suite name, scenario name, description and tags flip to live citing their readers; requires stays dead with a re-measured note, and its describe() now says NOT CHECKED instead of reading as a guard. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
…names and --tags Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Generated by `pnpm --filter @objectstack/spec check:generated --fix` (check:docs was the one stale artifact). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
📓 Docs Drift CheckThis PR changes 3 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 146 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 51b76c46ea20de097e32027226e3e8f292d09060 && git checkout 51b76c46ea20de097e32027226e3e8f292d09060
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d498113b505f7b29b5e2c554f3152145edeb3461 5b15c28fb146529543f02290ce18bf81d0f63df8 && git checkout -B drift-repro d498113b505f7b29b5e2c554f3152145edeb3461 && git merge --no-ff 5b15c28fb146529543f02290ce18bf81d0f63df8
node scripts/docs-audit/affected-docs.mjs --json d498113b505f7b29b5e2c554f3152145edeb3461
|
…lsify cli.mdx documents --tags and the new report lines; the platform checklist's os test knownGap and qa.json source note say what stays true (tags select, requires NOT CHECKED), revision 5; the liveness checker's qa comment names requires as the one key still unread. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Contract reviewServed-tier: 68/68 ① Derived judgments
② Semver levelminor. ③ Boundary flags
Implemented-by: VERDICT: PASS |
…-test-names-tags-requires # Conflicts: # packages/spec/liveness/README.md
The merge with origin/main took main's side of the generated state-counts.md (os-regen step 2); gen:liveness-counts re-derives it: qa 8/1 on top of main's totals. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Review nits, text only: a scenario naming a plugin the target lacks can also pass, so cli.mdx, the checklist knownGap, the changeset and the liveness README say the unmet requirement surfaces only as whatever failure it causes, if any; the checklist history notes the id is bare when it equals the name; the README qa row uses qa.json's wording for suiteName (every TestResult the suite produces). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV
Contract reviewServed-tier: 94/94 ① Derived judgments
② Semver levelUnchanged: minor. cli minor, core minor, spec patch, one ③ Boundary flags
Implemented-by: VERDICT: PASS |
…el, so `os g` scaffolds reach the stack (objectstack-ai#20333) (objectstack-ai#20363) Fixes objectstack-ai#20333 Clause-②: no ## Summary `npm create objectstack`'s blank starter imported `./src/objects` alone, so everything `os g view|action|flow|dashboard|app|skill` wrote was never loaded and `os validate` counted 0 of it. The starter now wires the seven generator barrels `os init` wires since PR objectstack-ai#20329, in the lines `os init` renders: `exportsOf` over `export {};` barrels, and `requires: ['automation', 'triggers']`. The copy is bound to the CLI's single source (`SCAFFOLD_WIRED_BARRELS` / `SCAFFOLD_WIRED_REQUIRES`, derived from `GENERATOR_SCAFFOLD_TARGETS`) by a parity pin, so it is not a second wiring rule. A per-PR pin drives `npm create objectstack` → `os g object` (control) → `os g flow` → `os validate` and reads `Logic: 1 Flows`. ## What changed - `packages/create-objectstack/src/templates/blank/objectstack.config.ts`: imports every wired barrel, declares the `exportsOf` helper, and hands each barrel to its stack key. The objects import changes from `'./src/objects/index.js'` to `'./src/objects'`, the extensionless form `os init` renders, which the parity pin compares verbatim; the template's `moduleResolution: bundler` resolves the directory index, and a fresh scaffold type-checks. It carries `requires: ['automation', 'triggers']`. `automation` was already there for the three connector plugins, and its comment keeps that reason. - Six new `src/{views,actions,flows,dashboards,apps,skills}/index.ts` barrels, byte-identical to what `os init` writes. - Two pins in `packages/cli/test/` (below). The CLI is the only package that can call the renderer, and it already depends on `create-objectstack`. - `packages/cli/package.json` gains `@objectstack/connector-{rest,openapi,mcp}` as devDependencies (lockfile +9 lines, one importer block). They exist only so the scaffolded project the chain pin builds under the CLI's `node_modules` can resolve the blank config's connector imports, and so CI builds them in `@objectstack/cli#test`'s closure. - `scripts/cross-package-test-inputs.mjs` and `turbo.json` declare the blank config and `src/**` as inputs of `@objectstack/cli#test`, with a witness for the barrel glob the scan cannot name. - Docs this change made false (see below), and a `create-objectstack` patch changeset. ## Why a static copy, and what binds it `create-objectstack` cannot import the roster. The dependency edge runs the other way, and the npx entry must not pull the CLI's closure: the boundary `scripts/sync-scaffold-emission-policy.mjs` already documents. Measured options: - **Generate at build time.** The roster is computed from the `GENERATORS` literal in `generate.ts`. Reading it at `create-objectstack`'s build would need either text-parsing that literal, or evaluating the CLI's source before the CLI's own dependencies are built, which is a build-order cycle. - **Parity pin over a static copy.** Chosen as the least machinery. `create-objectstack-wiring-parity.test.ts` reads every expected line off the CLI: the barrel import lines, the `exportsOf` line and the stack-key lines of `TEMPLATES.app.configContent`, the `requires` tokens as a superset of `SCAFFOLD_WIRED_REQUIRES`, and each empty barrel byte for byte from `TEMPLATES.app.srcFiles`. A generator added to the roster, a renderer change or a hand edit of the template reddens it (ablations A1 to A3). ## Measured before and after, through the real commands The on-ramp's real `bin/` scaffolded `my-app --skip-install --skip-skills` into a directory where the config's imports resolve, then this repo's CLI ran. | step | `origin/main` `c74de10a9` | this branch | |:---|:---|:---| | `os g object order_line` (control) | exit 0, reaches the stack | exit 0, reaches the stack | | `os g flow order_line` | exit 0, **Not wired** | exit 0, reaches the stack | | `os validate` | exit 0, `Data: 2 Objects`, `Logic: 0 Flows` | exit 0, `Data: 2 Objects`, `Logic: 1 Flows` | - **`exportsOf` is required here too.** A fresh starter type-checks (`tsc --noEmit`, 6.0.3, exit 0). The same starter with `Object.values` on the empty barrels fails with 4 x TS2322 (actions, flows, dashboards, apps). After generating the object and the flow it still type-checks. - **`requires` boots.** `os dev --fresh` on a random port: the flow-less fresh starter was healthy after about 22s, `/api/v1/ready` answered 200, and `AutomationServicePlugin` and the record-change, schedule, time-relative and api trigger plugins loaded, resolved through the CLI's own dependencies. With the generated flow it was healthy after about 24s and reported `Flows: 1 flow(s) 1 bound to triggers`. Neither boot printed "not enabled" or "NOT installed". - **Census.** `src/templates/` holds one starter, `blank`, which is also the registry's only entry. ## Pins - `packages/cli/test/create-objectstack-wiring-parity.test.ts` (unit, per-PR): 20 cases, described above. - `packages/cli/test/create-objectstack-stack-reach.test.ts` (integration, per-PR, not `.e2e`): the chain above, with item names read off the generator roster. It asserts the exit codes, the named subjects, the absence of the wiring lines and of a `requires` line from `os g flow`, and the `Data: 2 Objects` / `Logic: 1 Flows` counts. No prose is pinned. ## Ablations Each ran after the fix was committed. Mutations went through `scripts/ablation-replace.mjs` in wrap mode, which verified the anchor count and the blob change and restored with blob equal to HEAD and an empty `git diff HEAD`. - **A1, the wiring reverted** (the `flows: exportsOf(flows),` line deleted, then `create-objectstack` rebuilt). `ablation-dist-preflight --absent` confirmed the line was gone from `dist/`. Chain pin: 2 failed, 2 passed. The control and the scaffold stayed green, and `os g flow` printed the wiring lines while validate read no `Logic: 1 Flows`. Parity pin: 1 failed, 19 passed, on the stack-key comparison. Direction: red. - **A1 restore.** Rebuilt; `ablation-dist-preflight` found the marker present in `dist/templates/blank/objectstack.config.ts`, and the whole tree was clean. Chain pin 4/4, parity pin 20/20. - **A2, a barrel dropped from the template** (the `skills` key deleted): parity 1 failed, 19 passed. Red. - **A3, one barrel's bytes drifted from what `os init` writes** (`views/index.ts` reworded): parity 1 failed, 19 passed. Red. - After A2 and A3 the whole tree was clean, and parity was 20/20. ## Verification Patch round 1, at HEAD `702a27775` (origin/main `a88a1bb39` merged at `df0c0c846`): the 124 derived gates, `check-issue-citations --base origin/main` and `check:scaffold-emission-policy` all exited 0 on the first pass (`--ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN), including `check:doc-anchors`, `check:docs-audit-scope` and `check-affected-docs`; `pnpm lint` exited 0; the parity pin 20/20, the chain pin 4/4, and `create-objectstack` 16 files, 232 passed. Round 0: all of the following ran at HEAD `d50d46fe0` (origin/main `26daf0b03` merged). - `pnpm --filter create-objectstack test`: 16 files, 232 passed. `typecheck`: exit 0. - `pnpm --filter @objectstack/cli typecheck`: exit 0, including `check:test-typecheck`, whose ledger is unchanged. - CLI `unit` project: 231 files, 3316 passed. - CLI `integration`: this chain pin plus `generate-stack-reach.test.ts`, 2 files, 11 passed. - `pnpm lint`: exit 0 over the whole repo, not narrowed. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: 124 derived, 124 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first exited 3 with PREREQUISITE NOT MET (`check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`) and exited 0 after a full build. - `pnpm check:scaffold-emission-policy`: exit 0. ## Docs this change made false, and a surface note These published lines described an objects-only starter and are corrected in place: - the blank starter's `README.md` Layout, plus its app remedy, which now says to export the file from `src/apps/index.ts`; - the shipped `AGENTS.md` rule 3, which prescribed `Object.values()` (measured TS2322 on the now-empty barrels); - the package `README.md` tree; - `content/docs/getting-started/your-first-project.mdx`: its section-2 tree and config block; - `content/docs/getting-started/build-with-claude-code.mdx` (patch round 1): step 3 said the agent wires the action, view and app through `actions:` / `views:` / `apps:` keys in `defineStack()`. It now says each file is exported from its directory's barrel (`src/actions/index.ts`, `src/views/index.ts`, `src/apps/index.ts`), which the starter's config already hands to `defineStack()`, matching the shipped `AGENTS.md` rule 3. A sweep of `content/docs/` found no other sentence telling a starter author to add a collection key; - `content/docs/deployment/cli.mdx`. In `cli.mdx`, the `os generate` section's "Not wired" example named "the `npm create objectstack` starter", and its first-app walkthrough ran `os generate action approve`. On the wired starter that action is refused with exit 1: "Action 'approve' references object 'my_app_approve' which is not defined in objects". The walkthrough now runs `object customer`, then `flow customer`, then `action customer`, measured `UI: 1 Actions` and `Logic: 1 Flows`. Its fixture callout now names the extra action. `content/docs/**` and `packages/create-objectstack/README.md` were outside the claim's first file surface; the seat amended the claim in place to name them. They are edited under the agent contract's rule that a published line this change makes false is repaired in the same PR. PR objectstack-ai#20341 edits `cli.mdx` around lines 1619 to 1690, disjoint from these hunks; PR objectstack-ai#20258 edited lines 1 to 7 of `your-first-project.mdx` and `build-with-claude-code.mdx`, has since landed, and merged into this branch without conflict. `skills/objectstack-platform/SKILL.md` line 192 says the template declares `requires: ['automation']`. That is now stale, but `skills/**` is a governed Tier H surface, so it is **not** edited here; the seat files it for the skills lane once this PR lands. ## Acceptance notes - Byte-identical barrels inherit the article slip in `init.ts`'s `renderEmptyWiredBarrel` ("a action", "a app"). `init.ts` is read-only here. Whoever next edits that renderer carries it, and the parity pin will then require the starter to follow. - The old walkthrough's `os generate flow onboarding` also bound its flow to an undeclared object. That was not silent: `os dev` warned "the flow will never fire". The new walkthrough binds to the object it creates. - Measured in patch round 1, on a scaffolded starter holding the Build with Claude Code step-3 files: exporting each from its barrel, with the config untouched, gives `os validate` exit 0 with `Data: 2 Objects 6 Fields` and `UI: 1 Apps 1 Views 1 Actions`, the page's step-4 counts. Adding `actions:` / `views:` / `apps:` keys beside the wired ones instead still validates (the later key wins), but the starter's `tsc --noEmit` fails with 3 x TS1117, and a later `os g view customer` then reports Not wired, while the barrel-wired project reports it reaches the stack. - The `requires` pair is PR objectstack-ai#20329's shape. The standing family cards for the rest of that seam are objectstack-ai#20331 and objectstack-ai#20332, both named on objectstack-ai#20215. --- _Generated by [Claude Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…stem-* migration entries states each lesson in words, not tracker numbers (stage 3) (objectstack-ai#20384) Part of objectstack-ai#20233 Clause-②: no **Stage 3 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on the parent card sets the shape: the lesson in words, and no number, dead or alive. This stage covers the next three families by site count, `driver-`, `kernel-` and `system-`: **132 sites → 0** in the three prose fields. None of the 26 entries carries a tracker id in `surface` (ruling A of the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do here). Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The stage-1 pin now holds `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-` and `system-`. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-2 AST instrument, unchanged: a TypeScript-AST walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each `entry` object literal it evaluates the string value of `replacement`, `reason`, `acceptanceCriteria` and (counted separately) `surface`, joining string literals with `+`, then counts `#` followed by 4 or 5 digits at a word boundary. **Validated first** by reproducing the stage-1 readings on the stage-1 tree (`443b2f4fdc`, extracted with `git archive`): `driver-` 7 entries / 44 sites (0 / 44 / 0, 25 distinct), `kernel-` 9 / 44 (1 / 41 / 2, 11 distinct), `system-` 10 / 44 (0 / 42 / 2, 6 distinct), `engine-` 5 / 67, whole tree 266 entries / 1,016 sites / 9 `surface` sites — every figure equal to the stage-1 census. **Tree measured:** `objectstack-ai/objectstack` at `569d4d2dbf` (this branch's base). Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), the reading stages 1 and 2 took. - **Dark (comment lines):** 794 `//` lines in entry files carry a tracker id, and none is counted. Comment lines belong to the sibling card, and ⛔ this PR touches none (794 before and after). - **Dark (field boundary):** the 7 `surface` sites left in the tree (other families) count 0 in the three-field total and 7 in the `surface` column. **Re-measured on the base, matching the stage-1 census:** `driver-` 7 entries, **44** sites (replacement 0 / reason 44 / acceptanceCriteria 0), 25 distinct ids; `kernel-` 9 entries, **44** (1 / 41 / 2), 11 distinct; `system-` 10 entries, **44** (0 / 42 / 2), 6 distinct. 37 distinct ids across the three (the families share `objectstack-ai#14478`, `objectstack-ai#15939`, `objectstack-ai#17635` and `objectstack-ai#3733`). `surface`: 0 in all three. Whole tree: 300 entries, **843** sites, 7 `surface` sites. **After this PR:** `driver-` 0, `kernel-` 0, `system-` 0; `engine-`, `ui-`, `plugin-` still 0; whole tree **843 → 711** sites; `surface` 7 (unchanged, other families). | entry | sites (replacement / reason / acceptanceCriteria) | |---|---| | `17.driver-aggregate-undeclared-key-aliases-removed` | 6 (0 / 6 / 0) | | `17.driver-capabilities-inert-bits-removed` | 4 (0 / 4 / 0) | | `18.driver-options-timeout-to-timeout-ms` | 1 (0 / 1 / 0) | | `17.driver-sql-distinct-bare-filter-typed` | 9 (0 / 9 / 0) | | `18.driver-sql-unresolvable-where-column-refused` | 14 (0 / 14 / 0) | | `18.driver-sql-upsert-cross-row-identity-merge-refused` | 9 (0 / 9 / 0) | | `18.driver-turso-config-local-path-wasm-retired` | 1 (0 / 1 / 0) | | `18.kernel-compatibility-matrix-estimated-migration-time-unit-in-key` | 5 (0 / 5 / 0) | | `18.kernel-context-preview-mode-retired` | 5 (1 / 4 / 0) | | `18.kernel-event-bus-retention-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-health-check-and-hot-reload-durations-unit-in-key` | 7 (0 / 5 / 2) | | `18.kernel-package-lifecycle-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-plugin-health-report-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-plugin-security-durations-unit-in-key` | 4 (0 / 4 / 0) | | `18.kernel-runtime-config-timeout-unit-in-key` | 11 (0 / 11 / 0) | | `18.kernel-startup-orchestrator-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-cache-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-collaboration-durations-unit-in-key` | 4 (0 / 4 / 0) | | `18.system-failover-health-check-interval-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-metrics-jsdoc-durations-unit-in-key` | 12 (0 / 12 / 0) | | `18.system-metrics-window-durations-unit-in-key` | 5 (0 / 3 / 2) | | `18.system-object-storage-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-registry-config-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-tracing-otel-exporter-durations-unit-in-key` | 5 (0 / 5 / 0) | | `18.system-tracing-span-duration-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-worker-queue-rate-limit-duration-unit-in-key` | 3 (0 / 3 / 0) | | **total, 26 entries** | **132 (1 / 127 / 4)** | ## Every citation read, and what the text now says I read each cited issue or PR myself with single-card REST reads: the body, and the comments where a ruling or a measurement lives. Ids are in code spans so this body posts no cross-references. All 36 bare ids were resolved against this repository, because every sentence that cites one is about this repository's code; the one cross-repo id is `cloud#1651`. | cited | what it decided (read) | how the text now carries it | |---|---|---| | `objectstack-ai#3733` | The pruned `cached` field key: measured, the parse succeeded and the removed key was dropped without a word; the orphan schema was deleted. | "an earlier field-key prune measured exactly that — the parse succeeded and the removed key was dropped without a word" (health-check, OTel exporter) | | `objectstack-ai#3821` | The sharing-rule page: an unsortable query fell through to an empty page, and the driver fix made an unsortable query lose its ORDER BY, not its rows. | "the unknown-column recovery ladder (an unsortable query loses its ORDER BY, not its rows)"; "the ladder's own premise — rows matter more than their order"; "the ladder's recoveries" | | `objectstack-ai#4484` | `IDataDriver.findStream` removed: no production caller, two of three implementations buffered the whole set, and no tombstone because nothing parses a driver object. | "Retiring `IDataDriver.findStream` (it had no production caller, and two of its three implementations read the whole result set into memory …)"; "(`IDataDriver.findStream`, removed with no tombstone because nothing parses a driver object)"; by entry id in the `distinct` entry | | `objectstack-ai#4583` | The datasource ledger's dead keys removed; `capabilities.*` went as a whole block (11 of 11 unread). | "was retired separately, as a whole block nothing read" | | `objectstack-ai#4634` | Audit of all 34 `DriverCapabilities` bits: 3 live, 31 dead and tombstoned. | the entry already states the audit ("the follow-up audit checked every bit"); the trailing id is dropped | | `objectstack-ai#4914` | Maintainer, 2026-08-04: remove `manifest.loading` and `PluginHotReloadSchema`; keep `HotReloadConfigSchema`, the side with an implementation (`HotReloadManager`), as the start point. | "kept twice: as the hot-reload vocabulary that had an implementation when the manifest-side copy was removed, …" | | `objectstack-ai#4984` | An org-axis red-line gate read only aliases the schema rejects while its own fixtures spelt them: tests green, rule dead. | "the family of the org-axis red-line gate that read only rejected aliases while its own fixtures spelt them, so its tests stayed green and the rule stayed dead" | | `objectstack-ai#5181` | Narrow the query parameter of `IDataDriver`'s methods (`DriverQuery`, no redundant `object`). | "neither the narrowing of `IDataDriver`'s query parameters to `DriverQuery` nor the follow-through …" | | `objectstack-ai#5499` | Maintainer, 2026-08-05: freeze investment in `driver-memory` / `driver-mongodb`; fully lifted 2026-08-11 (comments `5249019855`, `5252526378`). | "the maintainer's 2026-08-05 investment freeze on driver-memory, which was lifted on 2026-08-11" | | `objectstack-ai#5540` | Remove `IStorageService.list(prefix)`: zero consumers, and the two adapters answered differently and both incompletely. | "(the zero-consumer `IStorageService.list`, whose two adapters answered differently and both incompletely)" | | `objectstack-ai#6011` | Maintainer: close the `ctx.user` `roles` alias now. | "(the `ctx.user` `roles` alias, closed at once on the maintainer's word rather than given a window)" | | `objectstack-ai#6075` | **404** — see Acceptance notes. | "the follow-through that brought five drivers' implementations in line" | | `objectstack-ai#6320` | `distinct`'s third argument meant different things on memory and sql; the sql half was dispatched, the memory half held under the freeze. | "(the measurement that found the two drivers reading this argument differently split the fix: the sql half is this entry, and the memory half was held back by that freeze)" | | `objectstack-ai#6321` | `query.aggregate` / `agg.func` are undeclared aliases whose only writers are driver fixtures; order: re-spell the fixtures, delete the aliases, then narrow the signature. | "The removal ran in a fixed order — the fixtures re-spelt first, the two alias branches deleted second, the parameter narrowed to `DriverQuery` last — because the reverse order yields red nobody can explain." | | `objectstack-ai#6404` (PR) | Executed that order and narrowed `aggregate`'s query parameter to `DriverQuery`. | the same sentence | | `objectstack-ai#7929` | Maintainer, 2026-08-12, ruling B: `driver-sql`'s filter refusal stops echoing `$field` operands, for every caller; the full diagnostic goes to the server log. | "the same predicate-text disclosure shape the driver's field-reference filter refusals had already been made to stop echoing (the full diagnostic goes to the server log, never the response)"; "that disclosure shape closed on the last dialect" | | `objectstack-ai#8371` | Ruled option 2: a dotted filter key whose head is a relation, a formula or a scalar is refused at both doors; a structured head stays unjudged. | "the axis owned by the dotted-filter verdict, which refuses a dotted key whose head is a relation, a formula or a plain column at the protocol and engine doors" | | `objectstack-ai#8592` | Measured on live MySQL: knex compiles the named conflict target away. | stated by the entry ("knex drops the named keys before the statement leaves the process"); the trailing id is dropped | | `objectstack-ai#8621` | Option A: a pre-flight refusal when no unique index backs the caller-named conflict target. | "Two earlier pre-flight refusals closed the half where no unique index backed a caller-named target …" | | `objectstack-ai#8622` | `id` becomes insert-only on the merge path: a merge on a non-primary conflict key was measured rewriting the existing row's primary key. | "`id` is insert-only on the merge path (made so once a merge on a non-primary conflict key was measured rewriting the existing row's primary key)" | | `objectstack-ai#8755` | Ruling option A: a pre-flight refusal when a second unique key could absorb a backed, caller-named target. | "… and the half where a rival unique key could absorb a caller-named one" | | `objectstack-ai#8790` | Maintainer, 2026-08-15: refuse both halves with `INVALID_FILTER` / 400, naming the column. | "Ruled by the maintainer on 2026-08-15: refuse BOTH halves …"; "Recover-both was excluded by the ruling's own argument" | | `objectstack-ai#8807` | Maintainer, 2026-08-15: an upsert must never modify a row whose identity the caller did not supply and whose conflict key it did not name; enforcement delegated, blanket refusal excluded. | "Ruled by the maintainer on 2026-08-15, as a contract principle …" (the principle itself was already quoted verbatim) | | `objectstack-ai#8926` | Maintainer, 2026-08-16, option A: MySQL's spelling joins the one shared predicate (envelope and recoveries together). | "Addendum 2026-08-16." — the paragraph already states option A | | `objectstack-ai#9061` (PR) | Implemented that option A. | the same | | `objectstack-ai#11825` | Maintainer, 2026-08-25: retire the declarative `AdvancedPluginLifecycleConfig` container; the classes stay a host-driven library. | "… and as a host-driven library when the declarative lifecycle config container was retired" | | `objectstack-ai#11846` | **404** — see Acceptance notes. | "maintainer ruling 2026-08-27 (Option A: remove)"; "(as the removal ruling recorded)" | | `objectstack-ai#14478` | Ruling B, 2026-09-02: a no-baseline gate plus an ADR-0087 rename of every offender (`DriverOptions.timeout` among the seven named); ruling B again, 2026-09-05: the population is every authored and every runtime-emitted duration, minus exemptions declared on the schema. | "Maintainer ruling B on duration units (2026-09-02, its population widened on 2026-09-05 to every authored and every runtime-emitted duration, bar the exemptions a schema declares on the key itself)"; "the duration-unit rule (…)" | | `objectstack-ai#14519` | The two tenant timeouts published a describe naming no unit (the unit sat in the JSDoc only); folded into the rename. | "the unit-nowhere shape (no unit in the name or in the published describe, first measured on two tenant timeouts)" | | `objectstack-ai#15626` (PR) | Landed the gate and the seven founding renames, the tenant `idleTimeout` → `idleTimeoutSeconds` among them. | "The tenant half was already renamed, in the same change that landed the duration gate itself" | | `objectstack-ai#15678` | `kernel/`: the 14 remaining duration keys carry their unit in the key name. | "the kernel-directory duration renames" / "the kernel-directory round"; trailing ids dropped | | `objectstack-ai#15679` | `system/`: the 15 remaining duration keys carry their unit in the key name; `size` got an honest name. | "the system-directory duration round"; trailing ids dropped | | `objectstack-ai#15939` | The gate did not read JSDoc. Ruled 2026-09-07: refuse the JSDoc / describe divergence. Ruled A 2026-09-11: remediate the population per file first, land the widened gate last. | "Director-seat ruling A of 2026-09-11 on the JSDoc-channel finding … a duration key whose JSDoc names a unit its describe does not is refused, and the keys in that shape are remediated per file before that refusal lands" | | `objectstack-ai#16024` | Maintainer, 2026-09-06, per key: forward `timeout`, remove `localPath` and `wasm`. | "ruled per key by the maintainer on 2026-09-06, once all three of this package's unread config keys had been measured" | | `objectstack-ai#17635` (PR) | The widened gate: refuse a duration key whose JSDoc names a unit its describe does not; landed last. | "lands that widened gate last, into a tree already clean" | | `objectstack-ai#18669` | Ruling A, 2026-09-17: rename `FileValue.duration` and `estimatedMigrationTime`, each with an ADR-0087 entry; no new closed type, no narrowing of stored data. | "Maintainer ruling A of 2026-09-17 on the last two duration keys no closed duration type could express …" | | `cloud#1651` | **Not readable from this session** — see Acceptance notes. | "cloud — a census closed 2026-08-26: OS_PREVIEW_MODE there is a routing-only switch …" | No call-shaped token moves: a `name(` census over `registry.ts` is identical before and after (297 distinct tokens), so textual call-spelling ratchets read the same. ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-`, `system-`. The pin still spawns the real CLI (`os migrate meta --from 16 --to 18`) once, locates each covered block **verbatim** in stdout, and asserts the printed block — `surface` included — carries no `#` plus 4 or 5 digits. Anti-vacuity: - the `REWRITTEN` floor rises from 29 to **55** ids: the 26 entries of this stage (7 `driver-`, 9 `kernel-`, 10 `system-`) are added, and every covered prefix must still select at least one entry; - presence in stdout is asserted before cleanliness (the `driver-sql-unresolvable-where-column-refused` reason carries two blank-line paragraph breaks, and its block is found verbatim); - the detector is exercised on both sides first (lit on 4 and 5 digits, dark on 3, 6 and `ADR-0112`). The file keeps its stage-1 name; the header lists the six covered families. ## Ablation — the widened pin can fail on a `driver-` block From committed state, HEAD `1c0dc7ad54`, with `scripts/ablation-replace.mjs` in wrap mode (it owns the restore trap) and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated. - **Mutation.** In `registry.ts`, the reason of `driver-sql-upsert-cross-row-identity-merge-refused`: anchor `pre-flight refusals closed the half` → `pre-flight refusals (objectstack-ai#8621) closed the half`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `b41e1d44` → `8f8d226e`. - **Mutate leg** (one lock turn: build, preflight, pin). Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `driver-sql-upsert-cross-row-identity-merge-refused: the printed guidance cites a tracker id: expected 'objectstack-ai#8621' to be undefined`. - **Restore.** Tool-proven: blob `b41e1d44` == HEAD, `git diff HEAD` empty. - **Restore leg.** One lock turn, taken on the second try (the first waited out its 540 s budget, exit 99, NOT MEASURED, the tree already restored). Spec build exit 0. The `--absent` preflight found the marker in none of 222 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. ## Verification Final head **`1c0dc7ad54`** for every line below; each heavy run went through `scripts/pm/os-verify-lock.sh` (one turn, `VERDICT command-exit 0`, per-step exits recorded separately). - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 55 successful, 55 total`. - **Pin and its neighbour:** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own pre-existing `skipIf`). - **Spec tests that read these entries or the registry:** `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations src/kernel/preview-mode-retirement.test.ts scripts/build-schemas-check-mode.test.ts` plus the 19 other spec test files that read `MIGRATIONS_BY_MAJOR`, the registry or an entry file: `Test Files 24 passed`, `Tests 696 passed`. - **CLI unit:** `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts`: `Test Files 2 passed`, `Tests 28 passed`. - **The call-spelling census that reads `registry.ts`:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-query-signature.test.ts` gives 15 passed. - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 255 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (test layer: 3 files / 28 errors held, unchanged). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives **89** families at `1c0dc7ad54` (after `git fetch origin main`). `--ran` over the recorded exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16466 customer-facing string(s) across 1135 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (300 semantic, 219 retired-key, 199 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:duration-unit-keys`, `check:nul-bytes`, `check:adr-0087-registration` and `check:changeset-no-major`. - `check:dual-build-cjs-loads` refused first with `PREREQUISITE NOT MET` (exit 3: twelve packages outside the CLI closure had no `dist/`). Those `dist/` directories were written later in the same pass (04:48–04:49Z, inside the `check:type-check-debt` run, whose re-measure builds them); re-run at the same head it exits 0 (104 entries / 66 packages / 659 CJS files). The reconciled list takes that latest run. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 28 changed `.ts` files reports 28 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 28 are in it (no file-ignored warning). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** see Acceptance notes (driver-free `merge-tree` against `862b6ce869` exits 0). ## Acceptance notes - **Two dead ids, rewritten from the code on `main`.** `objectstack-ai#6075` and `objectstack-ai#11846` answer 404 on both the issues and the pulls endpoint, re-probed with a 200 control (`objectstack-ai#14478`). - `objectstack-ai#6075` (`distinct`'s "never reached it" sentence): `packages/drivers/driver-sql/CHANGELOG.md` (commit `d367f03`) and `sql-driver-query-signature.test.ts` record what it did — the five drivers' implementations followed `IDataDriver`'s `DriverQuery` narrowing. The sentence now says exactly that. - `objectstack-ai#11846` (preview mode): `packages/spec/CHANGELOG.md` (commit `0c2334f`), `packages/spec/src/kernel/context.zod.ts` and `preview-mode-retirement.test.ts` record the 2026-08-27 ruling (Option A: remove), the three-repo zero-consumer measurement and the re-declare-fresh condition. Dropped because `main` does not state them: "decision-inbox batch 2" and "all four decision facets pointed the same way". "The objectstack-ai#11846 card records the measurement" (objectui leg) now reads "zero consumers, measured when the removal was ruled", which is what the changelog and the test header say. - **One cross-repo id this session cannot read.** `cloud#1651` answers 403 here: `objectstack-ai/cloud` is not attached to this session (`add_repo` refused: no access). It is neither confirmed nor refuted, so its sentence was rewritten from what `main` records about it — `packages/spec/CHANGELOG.md` (`0c2334f`: closed 2026-08-26 with positive controls, `RuntimeMode` zero hits, `ArtifactKernelFactory` 20+ hits and never touching `previewMode`) and `context.zod.ts` (`OS_PREVIEW_MODE` there is routing-only). The cloud-side detail `main` does not state — `previewMode` "only as a local variable" whose effect is adding wildcards to "CSRF" trusted origins — is dropped; the parenthesis that replaces it describes this repository's own `serve.ts` (the one reader of `OS_PREVIEW_MODE` here only widens better-auth's trusted origins to preview-domain wildcards), which is measured on `main`. - **Decision-batch numbers went too (invisible to the regex).** Nineteen sites cited a decision batch as `#` plus two or three digits (`objectstack-ai#43` ×13, `objectstack-ai#115` ×4, `objectstack-ai#151` ×1, `objectstack-ai#158` ×1). They are numbers an author is shown and cannot follow, so each is dropped. One consequence worth naming: 13 entries said `Maintainer ruling B on objectstack-ai#14478 (2026-09-02, decision batch objectstack-ai#43)`, which fused two rulings on the same card — B of 2026-09-02 (the gate and the no-baseline rename) and B of 2026-09-05, decided in that batch (the population: every authored and every runtime-emitted duration, minus schema-declared exemptions). The sentence now names both dates. The `objectstack-ai#158` sentence (the agreement shape ruled an offence on 2026-09-18) is corroborated by `.changeset/18075-agreement-shape-is-an-offence.md` on `main`. - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** No bare-number spelling exists in these 26 entries' author-shown text; the two `PR` citations (`PR objectstack-ai#6404`, `PR objectstack-ai#9061`) were `#`-spelled, so the instrument saw them and they are gone. The only `#` left in these 26 files is on `//` comment lines (sibling card's surface), including a `Prime Directive objectstack-ai#13` reference. - **A citation whose page says something narrower than the text.** `kernel-health-check-and-hot-reload-durations-unit-in-key` called `shutdownTimeout`'s shape "the objectstack-ai#14519 unit-nowhere shape". `objectstack-ai#14519`'s keys carried their unit in the JSDoc; "unit nowhere" is the gate's name for it (`check-duration-unit-keys.ts` header: "no unit ANYWHERE (the objectstack-ai#14519 shape)"), because the gate did not read JSDoc. The sentence now says what the shape is — no unit in the name or in the published describe — and that it was first measured on two tenant timeouts. - **A comment that my text edit makes slightly stale.** `18.system-metrics-window-durations-unit-in-key.ts` carries a `//` comment saying its acceptanceCriteria sentence "is objectstack-ai#15679's, left word for word". That sentence now says "that JSDoc-channel gap is filed as a finding of its own" where it said "is objectstack-ai#15939": same content, no number. The comment is the sibling card's surface (comment lines), so it is untouched here. - **Three "card" references re-anchored.** Removing an id left "the same card" in the Turso entry pointing at nothing; it now says "the same measurement". The kernel entries' "renamed by this same card" carry no number and were not otherwise rewritten, so they are left. - **Cross-PR check: no open PR adds or edits a `driver-`, `kernel-` or `system-` semantic entry.** Read at 2026-09-28T04:0xZ: the 18 open PRs' file lists (`GET /pulls/{n}/files`) carry 0 files matching `migrations/entries/semantic/NN.(driver|kernel|system)-*`. The Version Packages PR (`objectstack-ai#17076`) lists more than 1,000 files; the 1,100 rows read carry no entry file, and it is the bot-generated release PR. Nothing in flight will be held by the widened pin on arrival. - **`main` moved 4 commits past the base** (`862b6ce869`: `objectstack-ai#20364`, `objectstack-ai#20341`, `objectstack-ai#20366`, `objectstack-ai#20352`); none touches `packages/spec/src/migrations/` or the pin. A driver-free bare-clone `merge-tree --write-tree` of this head against `862b6ce869` exits 0 with no conflicted path, so `registry.ts` needs no merge, and `main` was not merged in. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1 and 2; their `--check` legs are green. Only the three `driver-` entries registered at protocol 17 appear in them, which is why those diffs are small. - **No other test pins these entries' text.** A `git grep` of test files for the 26 entry ids finds one (`preview-mode-retirement.test.ts`), which names the entry in a comment and reads no prose; a grep of tests for the 37 cited numbers finds only comment lines. So no test needed re-pinning this stage (stage 2's `migrations.test.ts` case has no counterpart here). ## Line budget Entry files: **352 changed lines** (+228 / −124) across 26 files, against the stage-1 ≈400 budget. The whole diff is **776 lines** (+516 / −260) in 31 files. Of the rest, `registry.ts` is 352, the two projections are 18 (`spec-changes.json` 12, the upgrade guide 6), the widened pin is 33 and the changeset is 21. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #20289
Clause-②: no
Rework round 1
The seat's order of record is comment
5861125164on #20289, which also amended the claim's file surface to these three files, text only. This PR had made three texts false or stale. Commit5d31b4d587corrects them. It is a new commit on top ofdd6b472c63, with no rebase, amend or force-push.docs/qa/platform-checklist/areas/cli.json, itemcli.qa-suite-execution:os testhas exactly three flags …" now says what stays true.--tagsselects scenarios (any-of, exact, case-sensitive). Deselected scenarios are counted and never passed.requiresis declared but NOT CHECKED, pending the maintainer's decision on qa:os testprints suite and scenario names, filters bytags, and skips on unmetrequires(5 keys) #20289.sourcenote onqa.json#tagssaid the same false thing ("its deadtags/requiresrows are why no scenario selection exists"), so it is corrected in the same edit.revisiongoes from 4 to 5 with a history entry, as the checklist's change rule requires.content/docs/deployment/cli.mdx, §os test:--tags: comma list, any-of, exact and case-sensitive. Deselected scenarios are counted and never passed. A listed tag no loaded scenario carries is named, and an empty entry is refused. A selection that matches no scenario exits 0, or 1 under--fail-on-empty.NAME (FILE), the scenario lineNAME [ID], and a failed scenario's description.requiresis not checked.os test --help, read at this head.packages/spec/scripts/liveness/check-liveness.mts, theqaparagraph of the header comment:suite.nameandscenario.tagsnow have readers inos test.scenario.requiresis the one key still unread: declared, NOT CHECKED, and its ledger row stays dead.This round changes no code, and the
requiresquestion stays with the maintainer.Verification at
5d31b4d587:dispatch-gates --repo objectstack-ai/objectstack --commandswas re-derived for the widened diff. It now names 118 families: the 114 of round 0 pluscheck:cli-examples-parity,check:pm-dispatch-gates,check:pm-governed-mergesandbare-root-worklist.mjs --self-test.check:dts-closure, "declarations missing").check:pm-dispatch-gatesonce the rebuild had finished. All 117 exited 0.check:pm-dispatch-gatesreads no built output; it exited 0 on the first pass after 1270 s.--ran(every line isCOMMAND :: exit CODE):118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.check:platform-checklist:OK — 15 areas, 266 items.check-liveness, 6 of them beside it inscripts/liveness/. All passed: 8 files, 262 tests.os testnow reports the suite and scenario names an author writes and selects scenarios with--tags. That makes four of theqa-runnerfamily's five ledger keys live. The fifth,TestScenario.requires, is not guessed. This PR leaves it dead with an honest describe(), and the report on #20289 puts the question to the maintainer. The card stays open for that half. This PR does not deliver the "unmetrequiresis SKIPPED with its reason" leg or its pin.What changes
@objectstack/core:QA.TestResultgainsscenarioNameanddescriptionon both result envelopes: the completed run and the setup-failure early return. It also gainssuiteNameon every result thatrunSuiteproduces.suiteNameis absent only from a lonerunScenariocall, which has no suite.@objectstack/cli(os test):📄 Running suite: NAME (FILE). A file that fails to load is still headed by the file alone.Scenario: NAME [ID] (Nms), and shows the id once when the name and id are equal.descriptionunder its line.--tags TAG[,TAG...]flag. Deselected scenarios are counted on the summary and never counted as passed. A requested tag that no loaded scenario carries is named there. An empty entry is refused at parse time.@objectstack/spec: only the describe() ofrequires,requires.paramsandrequires.pluginschanged. They now say NOT CHECKED instead of reading as a guard, and the reference page was regenerated. Inliveness/qa.json,name,scenarios.name,scenarios.descriptionandscenarios.tagsmove toliveand cite their readers asfile#symbol.tagsalso cites aproducer, the--tagsvalue.requiresstaysdeadwith a re-measured note. Thestate-counts.mdrow moved from live 4 / dead 5 to live 8 / dead 1, and the README row was rewritten to match.--tagssemantics: any-of, exact, case-sensitiveA comma list selects a scenario that carries at least one listed tag. Why this form:
--test-tags, a metadata platform, and in Cucumber's comma form.--grep "@smoke|@critical": name the sets you want and get their union.-m) is a grammar to specify, parse and report errors against. No suite has needed one. The only suite in this repo,examples/app-showcase/qa/platform-smoke.test.json, tags one scenariosmoke, crud.Matching is exact and case-sensitive, because a tag is a name the author chose, not a pattern. With the flag, an untagged scenario is never selected. Without the flag, nothing changes: the selection is not consulted and every scenario runs. The help text states all of this.
Exit status, and the convention it follows
The repo's
os testconvention is that a run that executed nothing is not a failure by default, and--fail-on-emptyis the opt-in strict reading. It is stated inTest.descriptioninpackages/cli/src/commands/test.tsand incontent/docs/deployment/cli.mdx, §os test: "A pattern that matches no suite is not a failure by default".A
--tagsselection that matches no scenario takes that same posture:No scenario matched --tags X.and exits0.--fail-on-emptyit exits1. The flag's description now names both cases.A renamed tag in a CI step is the same trap as a renamed directory, and it gets the same opt-out. The
Found N test suites.,SUCCESS: All N scenarios passed.andFAILED: N scenarios failed. M passed.lines keep their spelling. The platform checklist item foros testreads the latter two.Measured before and after
The target was a stub HTTP server that answers every request with 200. The suite had four scenarios:
smoke;regression;requires: { plugins: ["plugin-that-does-not-exist"], params: ["OS_TEST_PARAM_THAT_IS_NOT_SET"] };10ea9eb2ed📄 Running suite: probe.test.json📄 Running suite: Probe suite: names, tags and requires (probe.test.json)✅ Scenario: s-tagged-smoke (28ms): the id only (control: the id is printed)✅ Scenario: A scenario tagged smoke [s-tagged-smoke] (23ms)--tags smokeNonexistent flag: --tags(control:--fail-on-emptyparses, exit 0)--tags smoke selected 1 of 4 scenarios; 3 deselected (not run, not counted as passed).requires✅PASSED, same as the no-requirements controlrequires: why it is left dead and put to a decisionThe dispatch rule was: "A sub-key you cannot judge honestly is a
needs_decision, and ⛔ you don't guess." Neither sub-key can be judged honestly today:plugins:os testreaches the target only over HTTP, throughHttpTestAdapter. No server surface lists the loaded plugins. The discovery document (DiscoverySchema) advertisesservicesandcapabilities, not plugins./.well-known/objectstackdoes not list them either.kernel.getPlugins()is in-process only. The spelling is also undefined: the npm package name orplugin.name.params: it is undefined whose environment is meant. The runner'sprocess.envis observable, but no step can consume a runner variable, becauseresolveVariablesinterpolates only captured context. The server's environment is not observable at all.The decision is posted on #20289 with its options and the four-axis analysis. Until it is taken, the describe() says NOT CHECKED rather than implying a guard. The PR contains no speculative SKIPPED machinery, because every enforce option needs it and the retire option would leave it dead.
Verification
Every run below is at
dd6b472c63, the final commit.pnpm --filter @objectstack/core test: 56 files and 1483 tests passed.src/qa/runner.test.tshas 20 tests, 3 of them new: names on every result, names on the setup-failure envelope, and a lonerunScenariowith no suite.pnpm --filter @objectstack/core typecheck: exit 0.vitest run --project unit: 227 of 230 files passed on the first run. Two files failed becausepackages/cliwas not built yet. One file,hook-timeout-override-refusal.test.ts, timed out twice at 5000 ms under an 861 s shared-box run. After the CLI build, those 3 files passed 33 of 33 on a re-run.test/qa-tags-selection.test.ts(unit) has 13 tests, including the unfiltered CONTROL.test/qa-names-and-tags-run.test.ts(integration: it spawnsbin/run-dev.jsagainst anode:httpstub) has 8 tests. They pin the names printed,--tags smokeexiting 0 because the failing scenario never ran, the unfiltered control exiting 1, a no-match selection exiting 0 by default and 1 under--fail-on-empty, and a malformed list refused before anything runs.pnpm --filter @objectstack/cli typecheck: exit 0.vitest run src/qa: 26 tests passed.check:generated:check:docswas the one stale artifact.--fixregenerated it, and it is clean on re-check.check:liveness: green, withqa 9 classified (live 8, dead 1).node scripts/ablation-replace.mjs, WRAP mode):return hits.length > 0;→return true;inselectScenariosByTags. The tool reportedanchor 1 -> 0, and the blob went from774d2436cf4ftocab531acb335. That is the only mutation.--tags smoke,--tags regressionand no-match. The unfiltered control and the names pins stayed green.774d2436cf4f), andgit diff HEADis empty.src/, so nodist/leg applies.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands).check:skill-examples,check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parityandcheck:type-check-debt(the last reported--re-measure: OK … none above its recorded number). The first pass ofcheck:type-check-debthad died in its own internal build:plugin-pinyin-searchcould not find@objectstack/objectqldeclarations while a prerequisite build was rewritingdist/. The gate labelled that pass not measured itself.--ran(every line isCOMMAND :: exit CODE):114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN.pnpm exec eslint --no-inline-config --format jsonover the 6 changed TypeScript files: 6 results, 0 errors, 0 warnings.isPathIgnoredis false for all 6, and the 5 changed JSON, Markdown and MDX files are ignored.parserOptions.projectandprojectServiceresolve to null for all 6, so this diff cannot move any untouched file's verdict.Acceptance notes
cli.mdx, the checklist knownGap and the checker's comment) are corrected in Rework round 1 above, after the seat amended the surface in5861125164.@objectstack/cliisminor, for the new flag.@objectstack/coreis alsominor, notpatch.QA.TestResultgains three public fields, which is an additive API that programmatic consumers can read.@objectstack/specispatch, because only describe text and ledger data changed.fixedgroup.origin/mainatd498113b50is merged in withscripts/pm/os-regen-merge.sh, as merge commit9056455134. The one conflict waspackages/spec/liveness/README.md: main'stranslationandrest_apirows and this PR'sqarow are all kept. The generatedstate-counts.mdwas regenerated, not hand-resolved, in76e9a48821:qais 8/1 on top of main's totals. Against that merge,git diff d498113b50 HEADtouches only this PR's 14 files.Generated by Claude Code