From 45902f7df98787c3578263f41ea803a877005b66 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 10:00:04 +0000 Subject: [PATCH 1/4] =?UTF-8?q?docs(qa):=20cli.json=20=E2=80=94=20QA=20rou?= =?UTF-8?q?nd=203=20surface:cli=20findings=20C1=E2=80=93C9=20and=20build?= =?UTF-8?q?=20B6=20(#24579=20round=2013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten cli.json items take one revision bump and one history row each: migrate-duplicates-inventory (C1, C2), dev-boot-contract (C3, C4), migrate-meta-codemod (C5), doctor-health-report (C6), hook-body-extraction-gates (C7), validate-v18-authoring-verdicts and verify-derived-proof-families (C8, theme-B narrowing), qa-suite-execution (C9) and build-own-contract (B6). Each finding re-read at base 7697ddcf94. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/cli.json | 92 ++++++++++++++++------- 1 file changed, 65 insertions(+), 27 deletions(-) diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 569b4f2be2e..70aeba5f67c 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -9,7 +9,7 @@ "title": "os dev boots to healthy with a loginable seeded admin, honest DB selection, reported port shifts, and a staleness warning", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P0", "surface": "cli", "personas": ["operator (local shell)"], @@ -17,7 +17,8 @@ "app": "showcase", "requires": [ "a free non-default port (lsof -nP -iTCP: -sTCP:LISTEN empty first — dogfood skill §0) and a scratch DB path under /tmp//", - "⚠️ an app directory NO other runner boots, for steps[4], steps[6] and steps[7]: each writes into the directory it runs in. steps[4] bumps a tracked source file's mtime, so every other runner booting that tree reads its artifact as stale — and an `os dev` already running there is watching `objectstack.config.ts, src` and rebuilds and restarts on the change; steps[6] creates the unified default DB under .objectstack/data/ in it; steps[7] plants a legacy dev.db. Use your own worktree, or a worktree copy of examples/app-showcase (the same prescription cli.build-own-contract carries), and discard it after" + "⚠️ an app directory NO other runner boots, for steps[4], steps[6] and steps[7]: each writes into the directory it runs in. steps[4] bumps a tracked source file's mtime, so every other runner booting that tree reads its artifact as stale — and an `os dev` already running there is watching `objectstack.config.ts, src` and rebuilds and restarts on the change; steps[6] creates the unified default DB under .objectstack/data/ in it; steps[7] plants a legacy dev.db. Use your own worktree, or a worktree copy of examples/app-showcase (the same prescription cli.build-own-contract carries), and discard it after", + "for acceptance[0]'s console leg, a BUILT Console: in the framework repo run `pnpm objectui:build` at the repo root first (scripts/build-console.sh builds @object-ui/console at the .objectui-sha pin into packages/console/dist, which `pnpm build` never produces). On an unbuilt tree the boot still reaches health and ready, and GET /_console/ answers 503 text/plain 'The ObjectStack Console is not built, so this server has no Console to serve at /_console/.' followed by that build remedy (packages/cli/src/utils/console.ts#createConsoleNotBuiltPlugin): record the console leg blocked(environment), not a FAIL of the clause" ] }, "steps": [ @@ -88,7 +89,7 @@ "OS_DATABASE_URL / DATABASE_URL / TURSO_DATABASE_URL env (env wins over the default)", "-d :memory: (SQLite's own in-memory database — the replacement for the retired in-memory engine; nothing persists across restarts)", "--database-driver memory / OS_DATABASE_DRIVER=memory|mingo|in-memory / -d memory:// or mingo:// → REFUSED (the retired in-memory engine; see the negative for where each is refused and what it names)", - "config-declared default datasource (the project config's default home wins over the unified default) — authored as a NAMED datasource plus a datasourceMapping rule { default: true, datasource: NAME } (readConfigDeclaredDefault); a datasource literally named `default` is refused by defineStack as reserved for the host's primary, and a mapping rule naming 'default' is declined, so neither can stand for this variant", + "config-declared default datasource (the project config's default home wins over the unified default) — authored as a NAMED datasource plus a datasourceMapping rule { default: true, datasource: NAME } (readConfigDeclaredDefault); a datasource literally named `default` is refused by defineStack as reserved for the host's primary, and a mapping rule naming 'default' is declined, so neither can stand for this variant. The scratch stack's object must declare sharingModel (e.g. 'private'): without one the boot's compile refuses the stack on the error rule security-owd-unset (packages/lint/src/validate-security-posture.ts#SECURITY_OWD_UNSET) and `os dev` exits 1 with 'Compile failed — fix errors above before starting dev server' (packages/cli/src/commands/dev.ts)", "legacy compat-read: an existing dev.db / standalone.db when no objectstack.db exists (resolved WITH one loud notice line; probe order objectstack.db → dev.db → standalone.db)" ], "traps": ["stale-dist", "stale-console-bundle", "shared-browser-tab"], @@ -118,6 +119,12 @@ "date": "2026-10-17", "change": "the config-declared-default variant named no authorable shape, and the obvious one fails (checklist-accuracy finding, #23878 close-out): a datasource literally named `default` is refused by defineStack, so the variant is a named datasource routed by datasourceMapping { default: true, datasource: NAME }, the shape readConfigDeclaredDefault reads. Re-run of that variant on main c91af8ed6 on a minimal scratch stack: datasources [{ name: 'qa_main', driver: 'sqlite', config: { filename: SCRATCH/data.db } }] with the default mapping rule, os dev -p PORT with no -d and no env database → '🗄️ Database: file:SCRATCH/data.db', and no .objectstack/data file was created; a datasource named 'default' → build exit 2 with the reserved-name refusal", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "fixtures.requires[2] added and variants[6] extended — checklist-accuracy findings C3 and C4 of QA run #24786 (wave anchor #24579 round 13), re-read at base 7697ddcf94. C3: acceptance[0]'s console leg needs `pnpm objectui:build` in the framework repo; on an unbuilt tree /_console/ answers 503 'The ObjectStack Console is not built …' (console.ts createConsoleNotBuiltPlugin), which QA run #24786 met, so the requirement now says so and how to record the leg. C4: the config-declared-default variant's scratch object must declare sharingModel, or the compile fails on security-owd-unset and os dev exits 1 (dev.ts). No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] }, @@ -126,7 +133,7 @@ "title": "os build's own contract: no computed value in the exit slot, located errors for schema and author-time rule failures, artifact + stats output, warnings never flip the exit", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "build", "personas": ["operator (local shell)"], @@ -134,6 +141,9 @@ "app": "showcase", "requires": [ "a scratch copy of a known-good app config (scaffolded blank app or a worktree copy of examples/app-showcase) so deliberate breakage never touches shared fixtures" + ], + "knownGaps": [ + "no spec docblock or ADR states the --json payload shape of os build or os validate; the one governed element is the conversion notice both publish under conversions (packages/spec/src/stack-provenance.ts#stackConversionsOf). So acceptance[3] follows the code at base 7697ddcf94 (the emitJson payloads in compile.ts and validate.ts): the two commands spell the verdict (success / valid) and the finding list (issues / errors) differently, and build's own failure exits differ among themselves — the author-time rule and --strict-body failures carry issues, while the post-lowering schema parse, the container-name and the picklist refusals carry errors. A refusal at load carries its message and code only: packages/cli/src/utils/format.ts#errorCodeFields copies code and httpStatus, so the zod issues the STACK_SCHEMA_INVALID ledger row says that error carries ('issues carries the zod issues structurally', packages/spec/src/api/error-code-ledger.zod.ts) reach neither payload. Which spelling is canonical is unruled: score acceptance[3] on the warnings/conversions parity, and record both payloads as they are" ] }, "steps": [ @@ -167,9 +177,9 @@ "evidence": "the rule-failure output + exit code" }, { - "clause": "--json failure and advisory shapes match os validate --json (same warnings/conversions keys) — the #3782 parity class: the two surfaces must not disagree about what an author is told", + "clause": "--json failure and advisory payloads carry the same warnings and conversions keys os validate --json carries — the #3782 parity class: the two surfaces must not disagree about what an author is told. The verdict and finding keys differ by command: os build answers success and, for an author-time rule failure, issues; os validate answers valid and errors; and a config that throws at load answers error and code on both, with no finding list", "oracle": "log", - "verify": "the failure payload is { success: false, … issues } and success payloads carry warnings + conversions under the same keys validate emits", + "verify": "pair each fixture's two --json payloads. The author-time rule failure (the steps[3] flow): build answers { success: false, error: 'author-time rules failed', issues, warnings, conversions } and validate { valid: false, errors, warnings, conversions, duration }. The schema break (steps[2], refused by defineStack at load): build answers { success: false, error, code: 'STACK_SCHEMA_INVALID', warnings, conversions } and validate { valid: false, error, code, warnings, conversions, duration }, neither with issues or errors. The clean config: build's { success: true, …, warnings, conversions } beside validate's { valid: true, …, warnings, conversions }. The parity judged is warnings and conversions, key for key; success/valid and issues/errors are each command's own spelling (packages/cli/src/commands/compile.ts, packages/cli/src/commands/validate.ts) and never a FAIL of this clause", "evidence": "the paired --json payloads" }, { @@ -204,7 +214,8 @@ { "revision": 3, "date": "2026-08-18", "change": "reconciled clause 2 with clause 5, which contradicted each other. Clause 2 demanded exit 1 on a schema violation while clause 5 explicitly blesses 'an oclif 2 from a this.error() path'. Measured in compile.ts (build is an alias of it): the --json failure path emits the payload and calls this.exit(1); the human path falls through the same catch to this.error(), which exits 2. The clause now names both and asks which was run (#9417)", "ref": "#9386" }, { "revision": 4, "date": "2026-10-03", "change": "text refresh only, no status change: six live texts named compile.ts's this.error() as the mechanism of the human path's exit 2. Since bf36edd0a1 that catch prints its own ✗ line and ends in this.exit(2), which raises the same status without oclif's second Error: rendering; every oracle (exit 2 on the human path, 1 under --json) still holds. Revisions 2 and 3 above keep their wording: they record what was true when written", "ref": "#21492" }, { "revision": 5, "date": "2026-10-17", "change": "steps[5] re-pointed (checklist-accuracy finding, #23878 close-out): as written ('add one undeclared authoring key') it is now a hard refusal, because object, field, report and enable shapes are closed, so it could only fail clause 5 for the wrong reason. The step now places the key at a nested strip-mode site (a field's object-form visibleWhen) under defineStack(…, { strict: false }) and names what the closed site does instead; clause 5's verify says the same. Re-run on main c91af8ed6 on a scratch stack: the key on a field → exit 2 'Unrecognized key(s) on this field: qaUndeclared' (default) and exit 1 'Validation failed … unrecognized_keys' (strict:false); the key inside visibleWhen under strict:false → exit 0 with '⚠ Undeclared authoring keys (1) — dropped at load; reported here, never refused' naming objects.qa_thing.fields.note.visibleWhen.qaUndeclared, and the artifact's visibleWhen without it; the same key under the default defineStack → exit 0 with defineStack's own one-line warning and no compile-side block", "ref": "#24036" }, - { "revision": 6, "date": "2026-10-19", "change": "steps[2] and steps[5] name where the deliberate breaks must sit — checklist-accuracy findings of the 18.0 QA sweep (QA run #24551, cli.build-own-contract; wave anchor #24579 §3). Both plants were helper-authored on the stock fixtures, and ObjectSchema.create() parses at import (re-derived at base 9a7838edf5, packages/spec/src/data/object.zod.ts): the schema break died at config load with a raw Zod issue dump, and the nested strip-mode key was dropped by the helper with no advisory. The plants now sit on plain-literal objects. No clause changed", "ref": "#24649" } + { "revision": 6, "date": "2026-10-19", "change": "steps[2] and steps[5] name where the deliberate breaks must sit — checklist-accuracy findings of the 18.0 QA sweep (QA run #24551, cli.build-own-contract; wave anchor #24579 §3). Both plants were helper-authored on the stock fixtures, and ObjectSchema.create() parses at import (re-derived at base 9a7838edf5, packages/spec/src/data/object.zod.ts): the schema break died at config load with a raw Zod issue dump, and the nested strip-mode key was dropped by the helper with no advisory. The plants now sit on plain-literal objects. No clause changed", "ref": "#24649" }, + { "revision": 7, "date": "2026-10-20", "change": "acceptance[3] (clause, verify) states what each command answers, and fixtures.knownGaps[0] names the gap — checklist-accuracy finding B6 of QA run #24787 (wave anchor #24579 round 13). No spec docblock or ADR states either command's --json shape, so the clause follows the code at base 7697ddcf94 (compile.ts, validate.ts): os validate answers valid/errors where os build answers success/issues, and a refusal at load answers error and code (STACK_SCHEMA_INVALID) with no finding list on both. The '{ success: false, … issues }' the verify promised held for the author-time rule failure only. The parity the clause judges, warnings and conversions, is unchanged", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { @@ -504,14 +515,14 @@ "title": "os test: a Quality Protocol suite is validated at LOAD, executed against a booted app, and its verdict is the exit code — capture/interpolation thread state, an unevaluable assertion FAILS", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "cli", "personas": ["operator (local shell)", "suite author (writes qa/*.test.json)"], "fixtures": { "app": "showcase", "requires": [ - "a free non-default port and an ephemeral boot: `pnpm dev -- --fresh -p ` from the repo root (showcase + --seed-admin; dogfood skill §0)", + "a free non-default port and an ephemeral boot: `pnpm dev -- --fresh -p ` from the repo root (showcase + --seed-admin; dogfood skill §0). It needs a built workspace and NO console build: the root dev chain runs scripts/check-dev-prereqs.mjs first, which refuses an unbuilt workspace and prescribes `pnpm build`, then `pnpm check:console-sha`, which on an unbuilt console prints 'ℹ No console dist at packages/console/dist — skipping SHA check. Build it with: pnpm objectui:build' and exits 0 (scripts/check-console-sha.mjs), before the showcase boots. Every step here drives REST and the CLI, never /_console/, so that line is no blocker", "a session token for the data steps — POST /api/v1/auth/sign-in/email with the seeded admin (admin@objectos.ai / admin123) and read `.token`; pass it as `os test --token ` (the CLI sends it as `Authorization: Bearer`)", "the shipped fixture suite examples/app-showcase/qa/platform-smoke.test.json — the repo's only Quality Protocol suite, authored so `os test`'s DEFAULT glob (`qa/*.test.json`) finds it from that directory with no argument", "a scratch directory with its own qa/ for the negative and variant probes — deliberately broken suites are STAGED per run, never committed, so the repo's own suite stays green" @@ -626,7 +637,8 @@ { "revision": 4, "date": "2026-08-18", "change": "corrected the flag count from two to three. packages/cli/src/commands/test.ts declares url, token AND fail-on-empty; the same revision's own acceptance already describes --fail-on-empty, so the knownGap contradicted its own item. The gap's point is unchanged and now stated directly: none of the three flags selects scenarios (#9417)", "ref": "#9386" }, { "revision": 5, "date": "2026-09-28", "change": "the knownGap and the qa.json source note were falsified by #20289 (PR #20341) and are corrected: `os test` gained `--tags` (any-of, exact, case-sensitive; deselected scenarios are counted and never passed; a selection that matches no scenario exits 0, or 1 under --fail-on-empty), and the report now heads each suite with its `name` and prints each scenario's `name` beside its id. What stays true is narrower: `scenario.requires` is declared but NOT CHECKED, pending the maintainer's decision on #20289. Steps and acceptance are unchanged — the green-path clause's scenarioId is still on every scenario line, now in brackets after the name, or bare when the name equals the id", "ref": "#20289" }, { "revision": 6, "date": "2026-09-28", "change": "the knownGap and the qa.json source note were falsified again by #20289's `requires` round (ruling B) and are corrected: `scenario.requires` is now CHECKED before the first step — `params` against the `os test` process environment, `services` against the target's discovery `services` (enabled and `available`) — and an unmet entry SKIPS the scenario with its reason, counted apart and never passed; an all-skipped run exits 0, or 1 under --fail-on-empty. `requires.plugins` is retired (refused at load, prescription names `requires.services`). Steps and acceptance are unchanged: the fixture suite declares no `requires`, so the green path runs every scenario exactly as before", "ref": "#20289" }, - { "revision": 7, "date": "2026-10-19", "change": "steps[6] names an oracle that can be consulted — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.qa-suite-execution steps[6]; wave anchor #24579 §3): \"the server log carries no request attributable to it\" cannot be read, because the stock dev boot logs no per-request lines. Re-derived at base 9a7838edf5: the Hono server plugin logs a handler throw with method and path and no access line (packages/plugins/plugin-hono-server), and os test prints one `Scenario:` line per executed scenario under its suite heading (packages/cli/src/commands/test.ts). The step now reads the transcript. No clause changed", "ref": "#24649" } + { "revision": 7, "date": "2026-10-19", "change": "steps[6] names an oracle that can be consulted — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.qa-suite-execution steps[6]; wave anchor #24579 §3): \"the server log carries no request attributable to it\" cannot be read, because the stock dev boot logs no per-request lines. Re-derived at base 9a7838edf5: the Hono server plugin logs a handler throw with method and path and no access line (packages/plugins/plugin-hono-server), and os test prints one `Scenario:` line per executed scenario under its suite heading (packages/cli/src/commands/test.ts). The step now reads the transcript. No clause changed", "ref": "#24649" }, + { "revision": 8, "date": "2026-10-20", "change": "fixtures.requires[0] states what the boot needs — checklist-accuracy finding C9 of QA run #24786 (wave anchor #24579 round 13): `pnpm dev -- --fresh -p PORT` boots without a console build (check:console-sha skips with 'No console dist' and exits 0), and the root dev chain refuses an unbuilt workspace first (check-dev-prereqs.mjs), read at base 7697ddcf94. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { @@ -732,7 +744,7 @@ "title": "os migrate meta replays the spec migration chain over authored sources and PRINTS the mechanical edit list the retirement messages prescribe — by default it rewrites no source file, and --write rewrites in place exactly the edits it can prove, held to a re-run of the chain; its --out snapshot is schema-valid, replay is idempotent, and a below-floor --from is refused", "since": "v14.7", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "cli", "personas": ["build-time author (no session)"], @@ -755,7 +767,7 @@ "run `os migrate meta --from ` and capture stdout + the exit code", "confirm every planted spelling appears in the printed `Applied mechanical change(s):` block as `: → ()`, naming the successor the registry declares", "re-checksum the scratch tree and confirm every authored file is byte-identical — the run's only file write is --out", - "re-run with `--out .json`, read the snapshot back, and confirm the run states its schema verdict honestly: `Migrated stack is schema-valid` when it is, and the `Migrated stack does not yet pass schema validation — resolve the manual changes above, then run \\`os validate\\`` warning when manual changes are still outstanding", + "re-run with `--out .json`, read the snapshot back, and confirm the run states its schema verdict honestly: `Migrated stack is schema-valid` when it is, and, when the migrated stack still fails the schema, the `Migrated stack does not yet pass schema validation — refusal(s) left after the chain. Resolve them, then run \\`os validate\\`:` warning, followed by each refusal (packages/cli/src/commands/migrate/meta.ts#printSchemaVerdict)", "idempotence: replay a CANONICAL source — one carrying no shape the 17 → 18 chain converts; the tree the WRITE MODE leg wrote is one, while the stock showcase tree is NOT (three edge-branched decisions omit `mode`, so flow-decision-mode-inclusive-explicit lists three edits over it) — over a real range — `os migrate meta --from 17` (--to defaults to 18, CHAIN_TERMINUS_MAJOR: the higher of the runtime's PROTOCOL_MAJOR and the highest major the registries carry a step for, 18 on both counts since PROTOCOL_VERSION moved to 18 (#22215); the run's `Chain:` line prints both) — and confirm NO `Applied mechanical change(s):` block and exit 0; the step-18 semantic advisories are still listed (every hop lists its semantic entries unconditionally), so `Nothing to migrate …` is NOT the expected line here. Then run the identical `--from ` command on the planted tree a second time and diff the two stdouts with the verdict line's elapsed-time stamp (`(ms)`) normalized away — it is a timing, not report content. Separately record the EMPTY-RANGE answer: `--from 18` (range 18 → 18, no step) prints `⚠ No migration step exists for protocol 18 → 18, so this run replayed nothing — that is not a finding that the metadata is canonical.` — never the canonical success line", "run it with a --from BELOW the supported floor and capture the MigrationFloorError path (`Cannot migrate from protocol : the chain's support floor is `)", "run it with --json where supported and check the exit code in the same way #4873 required of every other migrate subcommand", @@ -866,6 +878,12 @@ "date": "2026-10-16", "change": "stale clause re-pointed (#23950, from QA run #23921): the item's own restart trigger fired. #9591 closed completed — `os migrate meta --write` landed by PR #22108 (a959493cd) and the house retirement sentence names it since PR #22142 — so `--help` now lists --write (and --all), and acceptance[7], which asserted the print-only flag surface, went red against a correct product. As revision 3 prescribed, the source-rewrite assertions are restored rather than the restart clause deleted: acceptance[7] is now the write contract — --write rewrites in place exactly the mechanical edits it can trace to one literal in one project file, lists every other with a closed-set refusal kind, writes no semantic advisory, leaves every byte outside a written site unchanged, and is held to a re-run of the chain (restored, exit 1, on disagreement), so the written tree re-runs clean and a second --write writes nothing. The last step becomes the WRITE MODE leg on its own fresh scratch copy. Following the same landing: the title and fixtures say the rewrite is opt-in and needs its own copy; the third knownGap now describes what --write refuses instead of saying it does not exist; step 2 quotes the current house sentence (with its --write clause); acceptance[1] is scoped to runs without --write and drops a quotation ('unsafe and lossy') meta.ts no longer carries; and the mutation negative splits into a no-write half and a write half. Source gains printWriteOutcome, CodemodRefusalKind and the migrate-meta-write.test.ts pin. Measured on the re-run and corrected here, same class (a step that reds a correct product): step 8 called the unmodified tree canonical for the `--from 17` replay, but the stock showcase carries three edge-branched decisions with no `mode`, which flow-decision-mode-inclusive-explicit converts, so that replay lists three edits; the canonical source is now the tree the WRITE MODE leg wrote, and the identical-run diff normalizes the verdict line's elapsed-time stamp. Same re-run, same class: step 8 still said the --to default ran ahead of the runtime's PROTOCOL_MAJOR 17, but PROTOCOL_VERSION is 18 since #22215, so the default (meta.ts CHAIN_TERMINUS_MAJOR, the higher of the two) is 18 on both counts; the parenthetical now says so, and the measured `--from 17` / `--from 18` expectations are unchanged", "ref": "#23950" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[6] quotes the schema-verdict warning from its producer — checklist-accuracy finding C5 of QA run #24786 (wave anchor #24579 round 13): meta.ts printSchemaVerdict at base 7697ddcf94 prints 'Migrated stack does not yet pass schema validation — N refusal(s) left after the chain. Resolve them, then run `os validate`:' and then each refusal, where the step still quoted the older '… resolve the manual changes above …' wording. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] }, @@ -961,7 +979,7 @@ "title": "os doctor: every check emits an attributed row, an unparseable posture is an ERROR not a swallow, ledger rows print under every posture, the ✓ is withheld when the read was incomplete — and the two monorepo-anchored scans announce a named skip in a user app instead of blessing a tree they never walked", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "cli", "personas": ["operator (local shell)"], @@ -978,7 +996,7 @@ ] }, "steps": [ - "baseline: from the untouched scaffold run `os doctor; echo $?` and `os doctor -v; echo $?` — capture both full reports (the unconditional environment rows print first: Node.js, pnpm, TypeScript, Dependencies, Git, Environment files — then whichever finding-only rows are live, then the extended checks)", + "baseline: from the untouched scaffold run `os doctor; echo $?` and `os doctor -v; echo $?` — capture both full reports (the environment rows print first: the unconditional Node.js, pnpm, TypeScript, Dependencies, Git, Environment files and Package-authored scheduled work, with whichever finding-only rows are live in their places — then the extended checks, then, when a config loads, the config block, which opens with its Platform protocol row)", "stage the config findings in the scratch copy: a view whose object does not exist (orphan), two objects whose lookup fields point at each other (circular), and one object nothing references (unused); run `os doctor` and capture the three warning rows — then restore and capture the three ✓ rows (Dependencies / Object usage / View integrity). The untouched blank scaffold is NOT that clean baseline: its starter object — the template's blank_note, renamed to the project's namespace (qa_doctor_note for the qa-doctor scaffold) — is referenced by no view, app or flow (its src/views, src/apps and src/flows barrels are empty), so findUnusedObjects warns on it and the report never reaches its healthy line. Add one view over it to the scratch copy (e.g. views: [{ name: 'qa_doctor_note', object: 'qa_doctor_note', list: { columns: ['title'] } }]) before the clean run. Stage the orphan as a WELL-FORMED view container whose name equals its object key and whose object does not exist, e.g. views: [{ name: 'qa_missing', object: 'qa_missing', list: { columns: [...] } }]: the default defineStack load accepts it and all three checks run. A malformed view body (e.g. a single view's viewKind/config keys on a container) is refused by the default load, and doctor then prints '⚠ Config load Could not load config for analysis (config checks skipped)' and runs none of the three — a staging fault, not a doctor finding; defineStack(…, { strict: false }) loads any shape, and is the route when the staging itself needs a shape the closed schema refuses", "unparseable posture: `OS_TENANCY_POSTURE=isolatd os doctor; echo $?` — the Tenancy posture ERROR row must print naming the variable and the allowed set, the REST of the report must still run, and the exit must be 1 (#5382: report-not-refuse, resolved OUTSIDE the config-analysis try at doctor.ts, row pushed)", "env provenance: put the same bad posture in a .env FILE (not the shell) and re-run — the finding must still fire (#5387 dotenv cascade) and the Environment files row must attribute the value to the file that supplied it", @@ -988,9 +1006,9 @@ ], "acceptance": [ { - "clause": "every environment check emits its own row, and the finding-only rows appear exactly when their finding is live — Node.js, pnpm, TypeScript, Dependencies, Git and Environment files are unconditional; @objectstack/spec prints only where /packages/spec/package.json exists (#10679), NODE_ENV only while NODE_ENV is unset (#5673), Tenancy posture only on an unrecognized value (#5382), and Installed packages only on a ledger failure or a skipped entry (#5429) — and -v adds the fix/detail lines without changing the verdicts", + "clause": "every environment check emits its own row, and the finding-only rows appear exactly when their finding is live — Node.js, pnpm, TypeScript, Dependencies, Git, Environment files and Package-authored scheduled work (ON or OFF, #17396) are unconditional; @objectstack/spec prints only where /packages/spec/package.json exists (#10679), NODE_ENV only while NODE_ENV is unset (#5673), Tenancy posture only on an unrecognized value (#5382), and Installed packages only on a ledger failure or a skipped entry (#5429); once a config loads, the config block opens with its own Platform protocol row (✓ when the declared engines.protocol covers the installed platform, ⚠ when checkProtocolVersionGap reports a gap) — and -v adds the fix/detail lines without changing the verdicts", "oracle": "log", - "verify": "both transcripts carry one row per check through renderHealthCheckResult (doctor.ts); the six unconditional rows are pushed across doctor.ts and the four finding-only rows (spec workspace, gated on the package.json probe), (nodeEnvCheck), (postureReading) and (installedPackageLedgerChecks, which returns [] on a clean ledger); the -v run differs only by detail lines", + "verify": "both transcripts carry one row per check through renderHealthCheckResult (doctor.ts); the unconditional rows are the run body's results.push calls that no condition guards — Node.js, pnpm (pnpmCheck), TypeScript (typescriptCheck), Dependencies, Git (gitCheck), Environment files (environmentSourcesCheck) and Package-authored scheduled work (packages/cli/src/commands/doctor.ts#scheduledWorkCheck) — and the four finding-only rows are pushed under their guards: the spec workspace (gated on the package.json probe), nodeEnvCheck, postureReading and installedPackageLedgerChecks (which returns [] on a clean ledger); the Platform protocol row is printed by the config block from checkProtocolVersionGap, not pushed among them; the -v run differs only by detail lines", "evidence": "the paired scaffold transcripts, plus the same pair from a monorepo cwd — a conditional row that is absent and a row that was dropped read identically in one transcript alone" }, { @@ -1055,7 +1073,8 @@ { "revision": 1, "date": "2026-08-20", "change": "new — scoped scan-functionality sweep (扫描功能): `os doctor` as a whole had NO checklist item; it appeared only as a --help variant on cli.flag-command-error-ux while carrying seven unit-pinned seams (env provenance #5387, posture honesty #5382, ledger independence #5429, withheld ✓ #5412/#5413/#5644). Authored from the run body at doctor.ts with the withhold-the-✓ discipline the command itself established turned back on its own monorepo-anchored Test-coverage/Deprecations ✓s, which bless a tree they never examined in every user app — encoded as an expected-fail probe rather than silently accepted", "ref": "claude/new-session-0pv25p" }, { "revision": 2, "date": "2026-08-23", "change": "text refresh only, no capability change: #10679 landed the fix this item's expected-fail predicted, so that clause inverts into an ordinary pass. findMissingTests/findDeprecatedUsages now return a MonorepoTreeScan and the ✓ is reachable only from the scanned arm; outside a monorepo each check prints 'ℹ Skipped — no packages/spec/src in this directory (monorepo-only check)', with the resolved directory under -v. Rewritten against a REAL run of the shipped CLI in a `create-objectstack -t blank` scaffold and again from a monorepo checkout, not from source: (a) the expected-fail clause, step 7 and the item title inverted; (b) the row enumeration in acceptance[0] and step 1 corrected — measurement falsified the premise that only @objectstack/spec became conditional: `Installed packages` was ALREADY findings-only (installedPackageLedgerChecks returns [] on a clean ledger) and had never been an unconditional row, and NODE_ENV/Tenancy posture are finding-only too, so the enumeration now splits unconditional from finding-only rather than naming one exception; (c) the '@objectstack/spec Not built' negative flipped from an accepted note into a regression, its probe being gated; (d) two negatives added for the skip row's own wrong shapes (a ⚠ instead of a ℹ, and the row vanishing); (e) doctor-unexamined-spec-tree.test.ts added to the pins; (f) every line anchor in the item re-derived at this base — doctor.ts grew ~90 lines, so every number quoted at revision 1 had moved", "ref": "#10798" }, { "revision": 3, "date": "2026-10-17", "change": "steps[1] said how to stage the three config findings but not that the staged config must load (checklist-accuracy finding, #23878 close-out: 'orphan-view staging needs defineStack strict:false, the default load refuses it, and doctor then skips config analysis'). Re-measured on main c91af8ed6, the finding holds for a malformed view body only: a well-formed container bound to a missing object loads under the default defineStack and doctor reports it, so strict:false is an option, not a requirement. The step names the well-formed shape, the skip row a refused load prints, and the strict:false route; knownGaps[0] says the staged config must load. Re-run on a scratch config (circular lookup pair qa_a/qa_b, unreferenced qa_unused): with the container { name: 'qa_missing', object: 'qa_missing', list } under the default load, os doctor exit 0 with '⚠ Circular dependency: qa_a → qa_b → qa_a', '⚠ Object \"qa_unused\" is defined but not referenced …' and '⚠ View \"qa_missing\" references non-existent object \"qa_missing\"'; the same three under strict:false with a mismatched-name container; a container carrying viewKind/config → '⚠ Config load Could not load config for analysis (config checks skipped) — defineStack validation failed …' and none of the three rows; the restored config → '✓ Dependencies No circular references detected', '✓ Object usage All objects are referenced', '✓ View integrity All views reference valid objects', exit 0", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[1] and acceptance[5] (clause, verify) name the clean baseline — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.doctor-health-report fixtures; wave anchor #24579 §3): the stock blank scaffold always carries an Object usage ⚠, so the restored config never reached the three ✓ rows. Re-derived at base 9a7838edf5: the blank template ships src/objects/note.object.ts (blank_note, renamed to the project namespace at scaffold) and empty views/apps/flows barrels (packages/create-objectstack/src/templates/blank), and findUnusedObjects (doctor.ts) counts only view, flow and app references. The clean run now adds one view over that object", "ref": "#24649" } + { "revision": 4, "date": "2026-10-19", "change": "steps[1] and acceptance[5] (clause, verify) name the clean baseline — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.doctor-health-report fixtures; wave anchor #24579 §3): the stock blank scaffold always carries an Object usage ⚠, so the restored config never reached the three ✓ rows. Re-derived at base 9a7838edf5: the blank template ships src/objects/note.object.ts (blank_note, renamed to the project namespace at scaffold) and empty views/apps/flows barrels (packages/create-objectstack/src/templates/blank), and findUnusedObjects (doctor.ts) counts only view, flow and app references. The clean run now adds one view over that object", "ref": "#24649" }, + { "revision": 5, "date": "2026-10-20", "change": "steps[0] and acceptance[0] (clause, verify) name every unconditional row — checklist-accuracy finding C6 of QA run #24786 (wave anchor #24579 round 13). At base 7697ddcf94 doctor.ts's run body pushes seven rows under no condition: the six the item named and Package-authored scheduled work (scheduledWorkCheck, ON or OFF, #17396); and once a config loads, the config block prints its own Platform protocol row (checkProtocolVersionGap). The clause names that row beside the others, and the verify names the rows by their push sites and checks rather than by a count. The finding-only rows are unchanged", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { @@ -1154,7 +1173,7 @@ "title": "os migrate duplicates: a read-only JSON inventory of identifiers minted across partitions — within-partition repeats excluded, nothing written, the live two-counter condition reported, and runnable BEFORE the #8686 repair destroys the evidence", "since": "v17", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "cli", "personas": ["operator (local shell, pre-repair audit)"], @@ -1162,7 +1181,7 @@ "app": "showcase", "requires": [ "a scratch app + persistent sqlite DB whose base schema was synced by ONE prior `os dev` boot (the same recipe cli.migrate-plan-apply-json uses), e.g. -d file:/tmp//dup.db — the app config must declare an org-scoped object with an autonumber (or unique) field", - "direct SQL seeding after that boot (sqlite3 CLI or a node better-sqlite3 one-liner): one identifier value held by a row with organization_id NULL AND a row with organization_id '' (the cross-partition duplicate); the same value twice WITHIN '' on a second identifier (the excluded repeat); and two _objectstack_sequences rows for the same object/field — one __global__ tenant, one org-scoped (the live condition)" + "direct SQL seeding after that boot (sqlite3 CLI or a node better-sqlite3 one-liner): one identifier value held by a row with organization_id NULL AND a row with organization_id '' (the cross-partition duplicate); the same value twice WITHIN '' on a second identifier (the excluded repeat); and two _objectstack_sequences rows for the same object/field — one __global__ tenant, one org-scoped (the live condition). The stock showcase declares ONE org-scoped identifier (showcase_field_zoo.f_autonumber), so the second identifier the within-partition repeat sits on must be declared in the scratch app (another autonumber, or a field carrying unique). Drop that field's partitioned unique index first — uniq_OBJ_organization_id_FIELD (an autonumber defaults to unique: 'organization'; the SQL driver names the index in packages/drivers/driver-sql/src/schema-drift.ts#buildIndexName) — or the repeat's second insert is refused. After the drop, `os migrate plan` over the same database reports that index as an index_mismatch drift (actual '(absent)'): the staging's own footprint, not a finding of this item" ], "knownGaps": [ "ORDERING IS LOAD-BEARING: do NOT boot the dev server again between seeding and scanning — a #8686-repairing boot backfills organization_id = NULL, which is exactly the evidence this report inventories (the command's own header calls the evidence perishable). The duplicates command itself boots read-only (deferSchemaDdl + readOnlyProbe) and is safe to run any number of times", @@ -1202,7 +1221,7 @@ { "clause": "the kernel:ready pre-flight (#8725) reports the RUNTIME-migration class os migrate plan cannot see: one entry per index the two kernel:ready migrations tighten (three — the overlay migration owns two; the third migration, sys_view_definition's, retired with its table under ADR-0131 D13), each blocked|clear|table-absent|unreadable, a blocked one naming every colliding key group and its row count", "oracle": "log", - "verify": ".runtimeIndexPreflight names idx_sys_metadata_overlay_active as blocked with one key group carrying the seeded type and name, package_id_key '' and bundle_member_key '', and its row count — and the SAME database run through `os migrate plan` mentions neither the index nor the seeded name in its plan REPORT (the boot's own log lines are not the report: plan's stdout carries '[Registry] Registered view: NAME' for every row it hydrates, and stderr a [metadata_spec_invalid] line for a body the spec refuses) (the matched control: the declared-index duplicate above IS reported by plan, this one is not); no entry names sys_view_definition", + "verify": ".runtimeIndexPreflight names idx_sys_metadata_overlay_active as blocked with one key group carrying the seeded type and name, package_id_key '' and bundle_member_key '', and its row count — and the SAME database run through `os migrate plan` mentions neither the index nor the seeded name in its plan REPORT (the boot's own log lines are not the report. Under `--json`, plan's stdout is the plan document alone and every boot line goes to stderr: '[Registry] Registered view: NAME' for every row it hydrates, which the objectql registry prints through console.log and bootSchemaStack moves to stderr (packages/cli/src/utils/json-stdout.ts#reserveStdoutForJson), and a [metadata_spec_invalid] line for a body the spec refuses. Only on the human path, without `--json`, do the '[Registry]' lines print on stdout, above the report) (the matched control: the declared-index duplicate above IS reported by plan, this one is not); no entry names sys_view_definition", "evidence": "the pre-flight section beside the plan output for one database" }, { @@ -1271,6 +1290,12 @@ "date": "2026-10-19", "change": "steps[4] and acceptance[3].verify narrowed to what can be read — checklist-accuracy findings of the 18.0 QA sweep (QA run #24551, cli.migrate-duplicates-inventory steps[4] and acceptance[3]; wave anchor #24579 §3). A minimal { name, label, object } view body is refused for carrying identity keys only (view.zod.ts assertViewIdentity at base 9a7838edf5), so the step names a container slot; and plan's stdout does name the seeded row in the registry's boot lines (objectql registry.ts logs '[Registry] Registered TYPE: NAME' through console.log), so the clause's control reads the plan report, not the boot log", "ref": "#24649" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "fixtures.requires[1] and acceptance[3].verify corrected — checklist-accuracy findings C1 and C2 of QA run #24786 (wave anchor #24579 round 13), re-read at base 7697ddcf94. C1: under `--json`, plan's '[Registry] Registered view: NAME' lines go to stderr and stdout is the plan document alone (bootSchemaStack reserves stdout through json-stdout.ts reserveStdoutForJson); only the human path prints them on stdout. The control's conclusion holds. C2: the stock showcase declares one org-scoped identifier (showcase_field_zoo.f_autonumber), so requires[1] now asks for a second one, and says its partitioned unique index (uniq_OBJ_organization_id_FIELD) must be dropped before the repeat is seeded, after which os migrate plan reports the drop as index_mismatch (schema-drift.ts, the declared-index-missing arm). No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] }, @@ -1396,7 +1421,7 @@ "title": "Hook/action body extraction: --strict-body refuses every forbidden pattern with its own worded reason, capability tokens are inferred (and crypto.hash is NOT, #4391), free identifiers fall back to bundling — and the DEFAULT build's warn-and-bundle fallback SURFACES every refusal (#10678)", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "cli", "personas": ["build-time author (no session)"], @@ -1459,7 +1484,7 @@ { "clause": "the DEFAULT `os build` warn-and-bundle path is NOT silent (#10678): a forbidden-pattern hook still ships through the .mjs bundle and the build exits 0 — lower-callables.ts catches the extraction failure, and making the default fatal would change what os build accepts — but compile.ts prints 'N handler(s) could not be lowered to a metadata body — bundled via the legacy runtime module instead (build still succeeds)' with one line per refused handler naming that handler's FORBIDDEN_PATTERNS reason, and the --json success payload carries the same records under bodyExtractionWarnings (a key of its own, apart from the rule-advisory warnings). The extractor's contract header now documents this default. A default run that is silent again, or a refused handler missing from either list, FAILS the clause", "oracle": "build", - "verify": "the default run over the forbidden seven exits 0 and prints the warning block naming each of the seven hooks with its own reason; the --json run exits 0 with success true and bodyExtractionWarnings holding one {origin, reason} record per refused hook; jq confirms all seven hooks are body-less and the runtime bundle was emitted (compile.ts, the #10678 surfacing; lower-callables.ts, the catch-and-bundle)", + "verify": "the default run over the steps[0] hooks — one per FORBIDDEN_PATTERNS entry (packages/cli/src/utils/extract-hook-body.ts#FORBIDDEN_PATTERNS; recount the array, never a remembered number) — exits 0 and prints the warning block naming each of those hooks with its own reason (the block lists at most 20, then '… and N more'); the --json run exits 0 with success true and bodyExtractionWarnings holding one {origin, reason} record per refused hook; jq confirms every one of those hooks is body-less and the runtime bundle was emitted (compile.ts, the #10678 surfacing; lower-callables.ts, the catch-and-bundle)", "evidence": "the warning block + the --json bodyExtractionWarnings + the jq body-absence sweep" } ], @@ -1483,7 +1508,8 @@ { "revision": 1, "date": "2026-08-20", "change": "new — scoped scan-functionality sweep (扫描功能): a security grant derived from a regex had no checklist item — only the runtime side of one token (records-forms) and the build's generic exit contract were covered. RE-VERIFIED against source with one material correction to the hunter brief: a forbidden pattern does NOT fail a default `os build` — lower-callables.ts catches every extraction error and falls back to the bundle, the warnings print nowhere on that path, and only --strict-body (compile.ts) produces the worded refusals; hook-bodies.mdx documents exactly that, while the extractor's own header still promises fail-with-no-fallback. The worded-refusal clauses are therefore pinned to --strict-body, and the default path's silence is encoded as the expected-fail contradiction clause", "ref": "claude/new-session-0pv25p" }, { "revision": 2, "date": "2026-08-22", "change": "the '// @capabilities' first-line override was RETIRED (#10917, maintainer ruling under ADR-0049 enforce-or-remove): measured on all four ordinary authoring shapes (.ts/.js/.mjs/an imported handler) it reached the extractor from NONE of them, so a documented directive silently taught a wrong convention and the mismatch surfaced far from its cause as a sandbox refusal. Its acceptance clause asserted behaviour that no longer exists — a runner would have filed a FAIL against the ruling — and is replaced by the covered route (body.capabilities as data) plus the inertness guarantee owed to configs that still carry the comment. Steps, fixtures and the extract-hook-body.ts line citations moved with it. No other clause changed; note the expected-fail clause still describes the pre-#10678 silent default and is stale on its own account, which is NOT this revision's subject", "ref": "claude/issue-10917-retire-capabilities-hook-directive" }, { "revision": 3, "date": "2026-10-17", "change": "clause 6's EXPECTED-FAIL has flipped (checklist-accuracy finding, #23879 and the #23878 close-out; the r2 history already flagged it stale): since #10678 the default build prints the refused handlers and --json carries them as bodyExtractionWarnings. The title, clause 6 (now a positive assertion that fails if the default path goes silent again), its verify and evidence, steps[1], knownGaps[1] and two source parentheticals now describe the surfaced default. Re-run on main c91af8ed6 on a scratch stack with the forbidden seven, a module-scope helper and a clean find hook: default os build exit 0 with '⚠ 8 handlers could not be lowered to a metadata body — bundled via the legacy runtime module instead (build still succeeds)' and one bullet per hook carrying its own reason (import and fetch prescribe a Connector recipe, require, process, globalThis, eval and new Function name themselves, the helper hook names 'helper'); --json exit 0, success true, bodyExtractionWarnings 8 records; --strict-body exit 1 '--strict-body: 8 callable(s) lack a metadata body'; the artifact carries no body on the eight, body.capabilities ['api.read'] on the find hook, and one objectstack-runtime bundle in dist", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[0] and acceptance[0] (clause, verify, evidence) cover every FORBIDDEN_PATTERNS entry — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.hook-body-extraction-gates steps[0]; wave anchor #24579 §3): the array has grown past the original seven sandbox escapes. Re-derived at base 9a7838edf5 (extract-hook-body.ts): fourteen entries, the seven escapes plus .sudo( (#14010), .create( with Object.create exempt (#16249), ctx.dispatch.scope and ctx.submitted (#22810), and the hook-face-only ctx.provenance, ctx.ql and ctx.transaction (#22837). The clause already said EACH pattern; its count and its evidence now match the array", "ref": "#24649" } + { "revision": 4, "date": "2026-10-19", "change": "steps[0] and acceptance[0] (clause, verify, evidence) cover every FORBIDDEN_PATTERNS entry — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.hook-body-extraction-gates steps[0]; wave anchor #24579 §3): the array has grown past the original seven sandbox escapes. Re-derived at base 9a7838edf5 (extract-hook-body.ts): fourteen entries, the seven escapes plus .sudo( (#14010), .create( with Object.create exempt (#16249), ctx.dispatch.scope and ctx.submitted (#22810), and the hook-face-only ctx.provenance, ctx.ql and ctx.transaction (#22837). The clause already said EACH pattern; its count and its evidence now match the array", "ref": "#24649" }, + { "revision": 5, "date": "2026-10-20", "change": "acceptance[6].verify counts from the roster — checklist-accuracy finding C7 of QA run #24786 (wave anchor #24579 round 13): it still said 'the forbidden seven' while steps[0] and acceptance[0] say fourteen, and QA run #24786 measured 14. At base 7697ddcf94 FORBIDDEN_PATTERNS (extract-hook-body.ts) holds 14 entries; the verify now names one hook per entry of that array, with no number, and notes that compile.ts lists at most 20 in the warning block. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { @@ -2051,7 +2077,7 @@ "title": "`os validate` / `os build` verdicts new on v18: an unknown seed record key (with did-you-mean), a misspelt record[…] / previous[…] field, a duplicate stack entry name, a controlled_by_parent object without exactly one required master_detail (also through objectExtensions), a component-props finding (now an error), and a credential in stack email/sms config each fail the build", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "cli", "personas": [ @@ -2090,7 +2116,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/spec/src/data/define-seed-record-keys.test.ts + packages/formula/src/validate-field-existence-spellings.test.ts + packages/spec/src/stack-duplicate-entry.test.ts + packages/lint/src/data-model-rules.master-detail-required.test.ts + packages/spec/src/stack-cbp-extension-master.test.ts + packages/lint/src/validate-component-props.test.ts + packages/spec/src/stack-email-sms.test.ts" + "ref": "packages/spec/src/data/define-seed-record-keys.test.ts + packages/formula/src/validate-field-existence-spellings.test.ts + packages/spec/src/stack-duplicate-entry.test.ts + packages/lint/src/data-model-rules.master-detail-required.test.ts + packages/spec/src/stack-cbp-extension-master.test.ts + packages/lint/src/validate-component-props.test.ts + packages/spec/src/stack-email-sms.test.ts — acceptance[0] ONLY, the clause that scores these seven spec, formula and lint unit files themselves. None of them runs `os validate` or `os build`, so no pin covers acceptance[1] (each arm's `os validate` exit and rule at its path, and the build refusing) or acceptance[2] (the controls pass): drive them by hand with the CLI" }, "source": [ "packages/spec/src/data/seed.zod.ts#defineSeed", @@ -2105,6 +2131,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-20", + "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 13, round 6's theme-B rule): the ref listed seven spec, formula and lint unit pins with no clause qualifier, though acceptance[1] and acceptance[2] need os validate and os build. None of the seven runs the CLI (read at base 7697ddcf94). The pins are acceptance[0]'s own subject, so `automated` stays, narrowed to it. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] }, @@ -2113,7 +2145,7 @@ "title": "`os verify` gains five derived proof families on v18 — --reference-shapes, --delete-behaviors, --unique-scopes, --parent-write-reach, --dashboard-windows — each clean (exit 0) on the showcase with its report in --json, and non-zero on a planted defect", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "cli", "personas": [ @@ -2151,7 +2183,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/cli/test/verify-reference-shapes.test.ts + packages/cli/test/verify-delete-behaviors.test.ts + packages/cli/test/verify-unique-scopes.test.ts + packages/cli/test/verify-parent-write-reach.test.ts + packages/cli/test/verify-dashboard-windows.test.ts" + "ref": "packages/cli/test/verify-reference-shapes.test.ts + packages/cli/test/verify-delete-behaviors.test.ts + packages/cli/test/verify-unique-scopes.test.ts + packages/cli/test/verify-parent-write-reach.test.ts + packages/cli/test/verify-dashboard-windows.test.ts — acceptance[0] for all five (the clause that scores these files), and acceptance[2] through verify-delete-behaviors.test.ts's 'NEGATIVE CONTROL: reports a cascade a hook undid, counts it, and exits 1', which spawns the CLI over its own stack plus a beforeDelete hook that detaches a declared cascade's holders, and asserts exit 1, that field's status 'contradicted' and deleteBehaviors.summary contradicted 1, hardFailures 1. No pin covers acceptance[1]: each file runs its family over its own fixture stack, never the showcase, so each family's exit 0, hardFailures 0 and --json report on the showcase need the live run" }, "source": [ "packages/cli/src/commands/verify.ts", @@ -2171,6 +2203,12 @@ "date": "2026-10-19", "change": "steps[1] and acceptance[2] (clause, verify) name a plant the family can catch — checklist-accuracy finding of the 18.0 QA sweep (QA run #24558, cli.verify-derived-proof-families step 2; wave anchor #24579 §3): a wrong deleteBehavior alone is enforced as declared, so it verifies clean. Re-derived at base 9a7838edf5 from the pin's own negative control (packages/cli/test/verify-delete-behaviors.test.ts 'NEGATIVE CONTROL: reports a cascade a hook undid, counts it, and exits 1'): the defect is a beforeDelete hook detaching the children of a declared cascade", "ref": "#24649" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 13, round 6's theme-B rule): the five pins spawn the CLI over their own fixture stacks, never the showcase, so acceptance[1]'s 'on the showcase' needs the live run, which QA run #24786 drove. The ref now names acceptance[0] for all five and acceptance[2] for verify-delete-behaviors.test.ts's NEGATIVE CONTROL case (read at base 7697ddcf94). No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] } From c121ba0dcf309d5a7d9a8b80688bf26cd1fc73e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 10:01:31 +0000 Subject: [PATCH 2/4] =?UTF-8?q?docs(qa):=20access-security.json=20?= =?UTF-8?q?=E2=80=94=20ADR-0131=20D7=20reading=20and=20the=20draft=20re-re?= =?UTF-8?q?ad=20(#24579=20round=2013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit no-active-org-session-semantics knownGaps[3] gains ADR-0131 D7's reading of sys_audit_log.tenant_id (attribution field, object permission governs); acceptance[4] stays, no ADR states the member-less answer. owd-save-gate acceptance[0] re-reads the probe with ?state=draft for its ?mode=draft leg (META_ITEM_READ_PARAMS). Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 11575739429..c5a49198494 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1406,7 +1406,7 @@ "title": "OWD save gate: the retired external dial is refused on every object write, and a packaged object's OWD cannot be changed at runtime — the managed-content seal refuses the write before any OWD rule judges it — server-side", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1435,8 +1435,8 @@ { "clause": "the server refuses the retired externalSharingModel on ANY object write, draft included: the writable-object PUT carrying it answers 422 INVALID_METADATA with one issue at externalSharingModel carrying the prescription (\"`object.externalSharingModel` was removed in @objectstack/spec 18 … Delete the key.\"), and nothing is stored", "oracle": "api", - "verify": "PUT /api/v1/meta/object/qa_owd_probe (active state, and again with ?mode=draft) answers 422 with code INVALID_METADATA, the issue path names externalSharingModel and its message carries the prescription; a follow-up GET finds no qa_owd_probe. ⛔ security-external-wider-than-internal and 403 owd_external_wider are both RETIRED spellings: the lint rule left with the key, and object-posture-gate.ts R2 was retired by the maintainer ruling on #8310", - "evidence": "the two PUT traces + the read" + "verify": "PUT /api/v1/meta/object/qa_owd_probe (active state, and again with ?mode=draft) answers 422 with code INVALID_METADATA, the issue path names externalSharingModel and its message carries the prescription; a follow-up GET /api/v1/meta/object/qa_owd_probe finds no qa_owd_probe, and for the ?mode=draft leg so does the draft read GET /api/v1/meta/object/qa_owd_probe?state=draft — a GET takes `state`, never the save door's `mode`, which it refuses with 400 (packages/rest/src/meta-item-read-gate.ts#META_ITEM_READ_PARAMS). ⛔ security-external-wider-than-internal and 403 owd_external_wider are both RETIRED spellings: the lint rule left with the key, and object-posture-gate.ts R2 was retired by the maintainer ruling on #8310", + "evidence": "the two PUT traces + the plain and the ?state=draft reads" }, { "clause": "the server admits the same object without the key: the PUT with only sharingModel saves 2xx — the refusal keys on the retired key, not on the posture", @@ -1493,7 +1493,8 @@ { "revision": 2, "date": "2026-08-18", "change": "corrected clauses 1-2 and steps 2-3 for the #8310 retirement (maintainer ruling 2026-08-13). The item tested R2 owd_external_wider / 403 at the ?mode=draft door; R2 is RETIRED from object-posture-gate.ts as a duplicate of the runtime lint door, whose refusal is 422 INVALID_METADATA rule security-external-wider-than-internal raised at the ACTIVE-state door — and draft saves deliberately defer to the draft-to-active promotion gate (#4463 D1), so the clause was unobservable where it was pointed (#9453 CF-2). ⚠️ #9453 CF-3 (that the R1 knownGap mispredicts which layer answers) was NOT applied: object-posture-gate.ts states verbatim that R1 applies only to overlay writes over an artifact-backed object and that 'the default deploy already 403s these before this gate runs' (if (!ctx.isArtifactBacked) return), which is what the knownGap and clause 4 already say", "ref": "#9386" }, { "revision": 3, "date": "2026-08-19", "change": "the R1 knownGap, step 5 and clause 4 named the WRONG answering layer, and #9453 CF-3 was RIGHT — re-measured on a genuinely stock showcase boot and OVERTURNING the revision-2 note above (#9477). Proven stock from the server process's own /proc//environ: zero METADATA_WRITABLE occurrences among 147 vars. Target artifact-backed, proven by GET ?layers=true returning a populated `code` layer (_packageId com.example.showcase) with `overlay: null`. A widening PUT then answered 403 declaredCode owd_widening_forbidden — reproduced twice, object byte-identical after. The mechanism the old text missed: saveMetaItem runs runAuthoringGate BEFORE the overlay/package doors on the host-config kernel the showcase boots (environmentId undefined, so protocol.ts's env-partitioned NOT_OVERRIDABLE branch never arms — the #7674 note in that file says the showcase is exactly that shape). So the deploy posture does not select the layer; the body's DIRECTION does. Control leg (same boot, same object, tightened body) passed R1 and answered 403 NOT_OVERRIDABLE, i.e. the door the knownGap called 'earlier' is in fact LATER. Six legs measured (widen/tighten x stock/hatch x ?package= present/absent) and WRITABLE_PACKAGE_REQUIRED appeared on none. ⛔ Not a product defect: every widening leg refused and the object never changed — object-posture-gate.ts behaved exactly as its executable guard (if (!ctx.isArtifactBacked) return) specifies; only the prose parenthetical in that file's header, and this item, mispredicted the door order", "ref": "#9477" }, { "revision": 4, "date": "2026-10-13", "change": "clause 4, acceptance[3], re-pointed to the managed-content seal, with step 5, the R1 knownGap, the first knownGap's packaged tighten-only parenthetical, the R1 variant, the title and two new source notes (stale spec, #23104 A3). Revision 3 measured, at #9477, that a widening PUT on showcase_announcement met R1 first and answered 403 PERMISSION_DENIED with declaredCode owd_widening_forbidden. Two later changes reorder that. #22338 made saveMetaItem ask the package door on every kernel topology, ahead of the checks that judge the body. ADR-0131 D6 (#22401, e02833c24) made the OS_METADATA_WRITABLE hatch stop opening an item a managed package ships. A packaged object, whose type opens no overlay channel, is now refused 403 NOT_OVERRIDABLE (ITEM_LOCKED with ?package=) whatever the body says, so R1 is unreachable on this door on stock. The protection holds: nothing widens. Per the #23121 triage, no fixture package is built to reach R1. Its oracle is its unit pins, named in the clause: plugin-security object-posture-gate.test.ts for the verdict, and rest meta-object-owd-gate.test.ts, whose R1 block now pins the package door answering first, hatch open", "ref": "#23121" }, - { "revision": 5, "date": "2026-10-14", "change": "the external clauses retired with the key (#23268, ruling of record 6159300530, letter A): externalSharingModel was removed from the object schema (ADR-0049), so the Studio external-wider clause goes and the external-wider refusal, admit and shipped-declarations clauses and the D11 knownGap are rewritten: clause 1 now asserts the tombstone's 422 refusal on every object write, draft included, clause 2 the admit without the key, and the last clause that the showcase ships no retired key. The packaged-object seal clauses are unchanged (the seal is now clause 3, and step 4 names it so). surface mixed → api: no browser step remains", "ref": "#23268" } + { "revision": 5, "date": "2026-10-14", "change": "the external clauses retired with the key (#23268, ruling of record 6159300530, letter A): externalSharingModel was removed from the object schema (ADR-0049), so the Studio external-wider clause goes and the external-wider refusal, admit and shipped-declarations clauses and the D11 knownGap are rewritten: clause 1 now asserts the tombstone's 422 refusal on every object write, draft included, clause 2 the admit without the key, and the last clause that the showcase ships no retired key. The packaged-object seal clauses are unchanged (the seal is now clause 3, and step 4 names it so). surface mixed → api: no browser step remains", "ref": "#23268" }, + { "revision": 6, "date": "2026-10-20", "change": "acceptance[0] (verify, evidence) adds the draft re-read — carried finding 3 of round 12's report on PR #24824 (wave anchor #24579 round 13), from QA run #24779: 'a follow-up GET finds no qa_owd_probe' named no read for the ?mode=draft PUT leg, and a GET with ?mode=draft answers 400. A GET takes state (META_ITEM_READ_PARAMS, meta-item-read-gate.ts at base 7697ddcf94), so the verify now re-reads the probe with ?state=draft beside the plain GET, and 'nothing is stored' holds for both legs. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { @@ -2358,7 +2359,7 @@ "title": "A session with no active organization is a LEGAL state with fail-closed semantics: tenant-scoped reads resolve to nothing, tenant-scoped writes are refused loudly naming the missing org, and nothing is ever NULL-stamped", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P0", "surface": "api", "personas": [ @@ -2376,7 +2377,7 @@ "The state needs a producer, and two are durable and non-racy on the walled boot (knownGaps[2]), both measured by QA run #24783 on `isolated` and `group`. (1) NO membership: under OS_AUTH_MEMBERSHIP_POLICY=invite-only a user minted by POST /api/v1/auth/admin/create-user answers membershipCreated:false and holds no sys_member row, because the reconciler answers `policy-skip` under invite-only (packages/plugins/plugin-auth/src/reconcile-membership.ts#reconcileMembership, packages/plugins/plugin-auth/src/admin-user-endpoints.ts#bindUserToSoleOrganization), so every session it mints reads activeOrganizationId null. (An explicitly declared `auto` binds nobody under a wall either, ADR-0093 D1 `no-target-org`; no run has measured that boot.) (2) A member who CLEARS its active organization: POST /api/v1/auth/organization/set-active {\"organizationId\": null}, after which GET /api/v1/auth/get-session reads null. The two differ under `group`, where the scope is the membership set (acceptance[0]), so record WHICH producer each clause was scored on. Two former producers no longer yield the state. The signup race is gone: the session seam settles a newly created user's membership before its first session resolves an organization (ADR-0123 D3; packages/plugins/plugin-auth/src/auth-manager.ts#composeDatabaseHooks, its defaultActiveOrg hook), so only a policy that binds nobody leaves that session without one. The removed-member path ends the session's claim instead: removing a user's last membership revokes each live session that named that organization, and a user still holding another membership has such a session re-pointed to it (packages/plugins/plugin-auth/src/membership-ended-session.ts#endSessionClaimsForEndedMembership); QA run #24783 measured the removed member's token answering 401 UNAUTHENTICATED", "`invite-only` is no longer a fixture gap: the walled boot must declare a membership policy anyway (RUNNER.md 'A walled boot is a fixture of its own', prerequisite 3), and declaring `invite-only` gives knownGaps[0]'s producer (1). SSO-JIT-pending still has no stock fixture: that producer is blocked(fixture) unless the run configures SSO.", "requires[0]'s tenancy-walled deployment is a walled boot. On 18.0.0 os serve — and os dev / os start, which boot through it — boots OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true): 18.x step 4 (#24580) deleted the walled boot refusal (serve.ts walledPostureRefusal) before 18.0.0, and of the published prereleases only 18.0.0-next.5 carries it. The walled boot is provisioned per RUNNER.md's environment-facts entry 'A walled boot is a fixture of its own': the multi-org runtime declared by the app (no example app declares it), OS_PLATFORM_OWNER_EMAIL, and a declared OS_AUTH_MEMBERSHIP_POLICY; each one absent refuses an os dev boot naming itself, while a bare os serve in production mode judges none of them until OS_AUTH_SECRET is set (that entry). The item stays blocked (fixture, #9334) because every clause is a walled leg and the stock showcase provisions no walled boot, not for want of a producer (knownGaps[0]); its pin (automated.ref) boots the posture in-process.", - "acceptance[4] has an open question for a user who holds NO membership at all (producer (1) of knownGaps[0]): its login/logout rows in sys_audit_log carry tenant_id null, because no organization exists to stamp and ADR-0123 D3's ordering fix 'must not manufacture a membership that policy says must not exist'. QA run #24783 read those rows only as the platform operator (GET /api/v1/data/sys_audit_log filtered by user_id answered 200), while the user itself gets 403 on sys_audit_log, on both postures. ADR-0123 does not state whether that is the intended answer: D3 forbids a NULL-tenanted row under this state 'in any subsystem' and pays the audit ledger by ordering, which binds nobody under invite-only. The clause stands as written. Until the question is ruled (raised by QA run #24783's finding F3 for wave anchor #24579; no card carries it yet), score acceptance[4] on that user from what is measured (the raw rows, their tenant_id and the readers that see them) and name this question beside the verdict", + "acceptance[4] has an open question for a user who holds NO membership at all (producer (1) of knownGaps[0]): its login/logout rows in sys_audit_log carry tenant_id null, because no organization exists to stamp and ADR-0123 D3's ordering fix 'must not manufacture a membership that policy says must not exist'. QA run #24783 read those rows only as the platform operator (GET /api/v1/data/sys_audit_log filtered by user_id answered 200), while the user itself gets 403 on sys_audit_log, on both postures. ADR-0123 does not state whether that is the intended answer: D3 forbids a NULL-tenanted row under this state 'in any subsystem' and pays the audit ledger by ordering, which binds nobody under invite-only. ADR-0131 D7 moves the clause's premise without answering the question: sys_audit_log is a deployment-level ledger with no organization column, its tenant_id is an attribution field the tenant-field resolver does not claim, 'never the tenancy anchor', and such objects are 'governed by object permission, not by the wall'. So Layer 0 is inert on the ledger, and the clause's 'invisible to RLS readers' (written for #8245, when the ledger was walled) no longer names its read path; ADR-0131 D9 scopes the no-NULL rule to tenant-column objects ('column ⇒ in scope'). Neither ADR states whether a member-less actor's row may carry a NULL attribution, nor who must be able to read it. The operator's 200 and the user's 403 are the implementation: packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts, whose tenant_id docblock says NULL means a deployment-level action and whose defaultRowScope sys_audit_log_org reads tenant_id == current_user.organization_id, which a NULL row matches for no organization reader. The clause stands as written. Until the question is ruled (raised by QA run #24783's finding F3 for wave anchor #24579; no card carries it yet), score acceptance[4] on that user from what is measured (the raw rows, their tenant_id and the readers that see them) and name this question beside the verdict", "On `group`, producer (2), a member with no active organization, gets 500 {\"code\": \"ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED\"} on a tenant-scoped create, where `isolated` and a member-less caller answer the acceptance[1] 403 naming the missing organization; nothing is written (QA run #24783: acceptance[1], acceptance[5], acceptance[6] and negative[3] FAIL on `group` only). That is the open product card #24784. The clauses stand as written: score them against it until it lands, and score the member-less caller's write separately", "acceptance[7] (the walled `group` leg for a member with no active organization) stays red until #24784 lands: on a walled group boot at 580fd4ed (QA run #24783, re-derived under RUNNER rule 7) the create answers 500 ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED and writes no row. It is scored only on a live walled group boot; a run without one records it blocked(fixture) citing RUNNER.md's 'A walled boot is a fixture of its own' — ⛔ never from automated.ref, whose group CONTROL stops at the plugin-security layer (RUNNER rule 6, a pin scores only the layer it drives). When #24784's fix merges, re-run the clause and drop this line" ] @@ -2507,6 +2508,12 @@ "date": "2026-10-20", "change": "acceptance[7], steps[8] and knownGaps[5] added, and automated.ref now names the layer its pin drives — for #24784, which the walled pass of round 3 (QA run #24783, 580fd4ed) found. Rounds 1 and 2 scored the walled legs from the pin because a walled boot was refused then; the pin's group CONTROL 'lands' a write by a member with no active organization at the plugin-security layer, and on a full walled group boot the engine refuses that write with a 500. The new clause asserts only no 5xx and no NULL stamping (which answer group gives is #24784's to decide) and stays red until #24784's fix lands. Merged onto revision 6 (round 11 of wave anchor #24579, #24801), whose knownGaps[0] producer (2) is the one steps[8] drives and whose knownGaps[4] names the same card for acceptance[1], [5], [6] and negative[3]; knownGaps[5] adds only the new clause's red state and the rule that it is never scored from the pin", "ref": "#24802" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "fixtures.knownGaps[3] sharpened with ADR-0131 D7's reading — carried item of wave anchor #24579 round 13 (PR #24801 named it in knownGaps[3]; #24343 reads sys_audit_log.tenant_id as D7's attribution field, not the anchor). Decided by governing text, read at base 7697ddcf94: D7 makes sys_audit_log a deployment-level ledger with no organization column, whose tenant_id is an attribution field and never the tenancy anchor, governed by object permission and not by the wall, and D9 scopes the no-NULL rule to tenant-column objects. No ADR states that a member-less actor's audit row may read NULL, or that the platform operator reads such rows, so acceptance[4] stays as written and knownGaps[3] names the question with the D7 reading. No clause changed", + "ref": "claude/issue-24579-v18-sweep-round13" } ] }, From 1ce9cecc17f3b831809fb61d600ee23e6e385728 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 10:01:32 +0000 Subject: [PATCH 3/4] =?UTF-8?q?docs(qa):=20approvals.json=20=E2=80=94=20a?= =?UTF-8?q?=20pin=20for=20email-action-token-door=20negative[2]=20(#24579?= =?UTF-8?q?=20round=2013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit action-page-member.integration.test.ts's dead-link case compares the invalid, expired and consumed pages byte for byte with renderResultPage(kind) and no request, which renders no summary rows. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/approvals.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 2f29a7867e2..451860f616e 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1145,7 +1145,7 @@ "title": "The anonymous /approvals/act door: a minted one-tap token renders a confirm page and one POST decides as exactly the bound approver — every dead-token shape is a designed refusal page with no state change", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": ["dev admin (submitter of the seeded invoice request — remind is submitter-only). Since PR #24620 the remind fan-out mints no token, so the admin drives only the live legs: the reminder at-rest read and the server reads beside the anonymous probes", "the ANONYMOUS bearer of an action link: a clean HTTP client holding NO session cookie and NO bearer token"], @@ -1215,7 +1215,7 @@ "traps": ["dispatcher-vs-hono-route", "auth-state-leak"], "automated": { "kind": "unit", - "ref": "packages/plugins/plugin-approvals/src/approval-service.test.ts (acceptance[0] mint + FORBIDDEN and the remind-carries-no-link pin; acceptance[1] peek; acceptance[2]–[3] redeem + replay) + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (acceptance[1] the confirm page; acceptance[2] the redemption) + packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts (acceptance[0] the closed door; acceptance[3] the replay) + packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts (acceptance[0] nothing redeemable at rest; acceptance[2]–[3] the positive control) + packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts (acceptance[0] the internal digest). Full clauses: acceptance[0]–[3] only. No pin covers acceptance[4] or acceptance[5]. acceptance[4] is pinned in halves: the invalid / expired / consumed pages (action-page-member's 'a dead link — unknown, expired, consumed — is told on the page with 200, through GET and POST alike') and the not_pending / not_approver reasons (approval-service's 'redeem: dead tokens — invalid, expired, decided request, reassigned slot'). The summary-rendering split, the not_pending / not_approver PAGES rendered WITH their summary rows, has no page-level pin, and the live invalid capture stays the run's. acceptance[5]'s bearer probe has no pin: action-page-member's 'the token is the only credential: a cookie or Authorization header is never read, and changes nothing' pins its converse only. The at-rest read and the token=garbage page still need the live run" + "ref": "packages/plugins/plugin-approvals/src/approval-service.test.ts (acceptance[0] mint + FORBIDDEN and the remind-carries-no-link pin; acceptance[1] peek; acceptance[2]–[3] redeem + replay) + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (acceptance[1] the confirm page; acceptance[2] the redemption; negative[2] the dead-link pages) + packages/plugins/plugin-approvals/src/sys-approval-token-generic-door.integration.test.ts (acceptance[0] the closed door; acceptance[3] the replay) + packages/plugins/plugin-approvals/src/reminder-action-link-at-rest.integration.test.ts (acceptance[0] nothing redeemable at rest; acceptance[2]–[3] the positive control) + packages/plugins/plugin-approvals/src/approval-token-internal-hash.integration.test.ts (acceptance[0] the internal digest). Full clauses: acceptance[0]–[3], and negative[2]: action-page-member's 'a dead link — unknown, expired, consumed — is told on the page with 200, through GET and POST alike' compares each of the three pages, through GET and POST, byte for byte with renderResultPage(kind) called with no request, which renders the summary rows only when it is handed one (packages/plugins/plugin-approvals/src/action-link-pages.ts#renderResultPage), so a page that leaks the process, record or requester fails it; the same file's two parity cases hold the self-hosted mount to the member's bytes for the same three shapes. No pin covers acceptance[4] or acceptance[5]. acceptance[4] is pinned in halves: the invalid / expired / consumed pages (action-page-member's 'a dead link — unknown, expired, consumed — is told on the page with 200, through GET and POST alike') and the not_pending / not_approver reasons (approval-service's 'redeem: dead tokens — invalid, expired, decided request, reassigned slot'). The summary-rendering split, the not_pending / not_approver PAGES rendered WITH their summary rows, has no page-level pin, and the live invalid capture stays the run's. acceptance[5]'s bearer probe has no pin: action-page-member's 'the token is the only credential: a cookie or Authorization header is never read, and changes nothing' pins its converse only. The at-rest read and the token=garbage page still need the live run" }, "source": [ "packages/plugins/plugin-approvals/src/approvals-plugin.ts#http (the raw Hono mount at /api/v1/approvals/act — GET renders via peekActionToken, POST redeems; mounted on kernel:ready off the canonical http.server handle, #4251 B5)", @@ -1234,7 +1234,8 @@ { "revision": 2, "date": "2026-10-10", "change": "re-pointed after PR #22633 (#22616) closed sys_approval_token to the automatic API (apiEnabled: false, apiMethods: [] — 404 OBJECT_API_DISABLED for every caller): the token-row oracles move to the act door's outcome pages, the request's action history (GET /:id/actions) and the two pinned integration tests (internal-hash, generic-door); the mint clause is scored by test; the expiry leg, whose only recipe was backdating expires_at over the data API, has no surviving door (ttlMs is not REST-reachable and the plugin threads no clock) and is recorded in knownGaps as blocked(fixture) with the pinned 'expired' reason as its oracle. Two steps had contradicted the door before that PR already: the digest has been withheld since #21197, and update was never in the whitelist", "ref": "#22631" }, { "revision": 3, "date": "2026-10-16", "change": "step 7's dead-slot leg said 'reassign the bound approver away first' without saying who can (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on a two-slot request [admin, Ada] minted by remind: the admin's reassign without `from` moved the admin's own slot ([OTHER, Ada]) and Ada's approve token still peeked as the 'Approve · 通过' confirm page; the admin's reassign with from: Ada moved Ada's slot and the same token then rendered 'No longer your approval · 已不在你名下'. The step now passes `from` (or has the bound approver reassign their own slot); the clause is unchanged", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "#24824" } + { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "#24824" }, + { "revision": 6, "date": "2026-10-20", "change": "automated.ref names a pin for negative[2] — carried finding 2 of round 12's report on PR #24824 (wave anchor #24579 round 13), against QA run #24779's 'unpinned' score. action-page-member.integration.test.ts's dead-link case compares the invalid, expired and consumed pages, through GET and POST, byte for byte with renderResultPage(kind) and no request, which renders no summary rows (action-link-pages.ts at base 7697ddcf94); a page leaking the summary fails it. That is negative[2]'s full clause on the member door, and the file's parity cases hold the mount to the same bytes. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } ] }, { From 19eb546d9cfdd10a5d86080301b28ffd5f4e3dac Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 10:17:48 +0000 Subject: [PATCH 4/4] docs(qa): history rows of round 13 name their PR The twelve history rows this round appended carried the branch name as their ref until the pull request existed; they now name it. Only those twelve strings change. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 4 ++-- .../qa/platform-checklist/areas/approvals.json | 2 +- docs/qa/platform-checklist/areas/cli.json | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index c5a49198494..fc32be39bdc 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1494,7 +1494,7 @@ { "revision": 3, "date": "2026-08-19", "change": "the R1 knownGap, step 5 and clause 4 named the WRONG answering layer, and #9453 CF-3 was RIGHT — re-measured on a genuinely stock showcase boot and OVERTURNING the revision-2 note above (#9477). Proven stock from the server process's own /proc//environ: zero METADATA_WRITABLE occurrences among 147 vars. Target artifact-backed, proven by GET ?layers=true returning a populated `code` layer (_packageId com.example.showcase) with `overlay: null`. A widening PUT then answered 403 declaredCode owd_widening_forbidden — reproduced twice, object byte-identical after. The mechanism the old text missed: saveMetaItem runs runAuthoringGate BEFORE the overlay/package doors on the host-config kernel the showcase boots (environmentId undefined, so protocol.ts's env-partitioned NOT_OVERRIDABLE branch never arms — the #7674 note in that file says the showcase is exactly that shape). So the deploy posture does not select the layer; the body's DIRECTION does. Control leg (same boot, same object, tightened body) passed R1 and answered 403 NOT_OVERRIDABLE, i.e. the door the knownGap called 'earlier' is in fact LATER. Six legs measured (widen/tighten x stock/hatch x ?package= present/absent) and WRITABLE_PACKAGE_REQUIRED appeared on none. ⛔ Not a product defect: every widening leg refused and the object never changed — object-posture-gate.ts behaved exactly as its executable guard (if (!ctx.isArtifactBacked) return) specifies; only the prose parenthetical in that file's header, and this item, mispredicted the door order", "ref": "#9477" }, { "revision": 4, "date": "2026-10-13", "change": "clause 4, acceptance[3], re-pointed to the managed-content seal, with step 5, the R1 knownGap, the first knownGap's packaged tighten-only parenthetical, the R1 variant, the title and two new source notes (stale spec, #23104 A3). Revision 3 measured, at #9477, that a widening PUT on showcase_announcement met R1 first and answered 403 PERMISSION_DENIED with declaredCode owd_widening_forbidden. Two later changes reorder that. #22338 made saveMetaItem ask the package door on every kernel topology, ahead of the checks that judge the body. ADR-0131 D6 (#22401, e02833c24) made the OS_METADATA_WRITABLE hatch stop opening an item a managed package ships. A packaged object, whose type opens no overlay channel, is now refused 403 NOT_OVERRIDABLE (ITEM_LOCKED with ?package=) whatever the body says, so R1 is unreachable on this door on stock. The protection holds: nothing widens. Per the #23121 triage, no fixture package is built to reach R1. Its oracle is its unit pins, named in the clause: plugin-security object-posture-gate.test.ts for the verdict, and rest meta-object-owd-gate.test.ts, whose R1 block now pins the package door answering first, hatch open", "ref": "#23121" }, { "revision": 5, "date": "2026-10-14", "change": "the external clauses retired with the key (#23268, ruling of record 6159300530, letter A): externalSharingModel was removed from the object schema (ADR-0049), so the Studio external-wider clause goes and the external-wider refusal, admit and shipped-declarations clauses and the D11 knownGap are rewritten: clause 1 now asserts the tombstone's 422 refusal on every object write, draft included, clause 2 the admit without the key, and the last clause that the showcase ships no retired key. The packaged-object seal clauses are unchanged (the seal is now clause 3, and step 4 names it so). surface mixed → api: no browser step remains", "ref": "#23268" }, - { "revision": 6, "date": "2026-10-20", "change": "acceptance[0] (verify, evidence) adds the draft re-read — carried finding 3 of round 12's report on PR #24824 (wave anchor #24579 round 13), from QA run #24779: 'a follow-up GET finds no qa_owd_probe' named no read for the ?mode=draft PUT leg, and a GET with ?mode=draft answers 400. A GET takes state (META_ITEM_READ_PARAMS, meta-item-read-gate.ts at base 7697ddcf94), so the verify now re-reads the probe with ?state=draft beside the plain GET, and 'nothing is stored' holds for both legs. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 6, "date": "2026-10-20", "change": "acceptance[0] (verify, evidence) adds the draft re-read — carried finding 3 of round 12's report on PR #24824 (wave anchor #24579 round 13), from QA run #24779: 'a follow-up GET finds no qa_owd_probe' named no read for the ?mode=draft PUT leg, and a GET with ?mode=draft answers 400. A GET takes state (META_ITEM_READ_PARAMS, meta-item-read-gate.ts at base 7697ddcf94), so the verify now re-reads the probe with ?state=draft beside the plain GET, and 'nothing is stored' holds for both legs. No clause changed", "ref": "#24850" } ] }, { @@ -2513,7 +2513,7 @@ "revision": 8, "date": "2026-10-20", "change": "fixtures.knownGaps[3] sharpened with ADR-0131 D7's reading — carried item of wave anchor #24579 round 13 (PR #24801 named it in knownGaps[3]; #24343 reads sys_audit_log.tenant_id as D7's attribution field, not the anchor). Decided by governing text, read at base 7697ddcf94: D7 makes sys_audit_log a deployment-level ledger with no organization column, whose tenant_id is an attribution field and never the tenancy anchor, governed by object permission and not by the wall, and D9 scopes the no-NULL rule to tenant-column objects. No ADR states that a member-less actor's audit row may read NULL, or that the platform operator reads such rows, so acceptance[4] stays as written and knownGaps[3] names the question with the D7 reading. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 451860f616e..2246c9cb382 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1235,7 +1235,7 @@ { "revision": 3, "date": "2026-10-16", "change": "step 7's dead-slot leg said 'reassign the bound approver away first' without saying who can (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6 on a two-slot request [admin, Ada] minted by remind: the admin's reassign without `from` moved the admin's own slot ([OTHER, Ada]) and Ada's approve token still peeked as the 'Approve · 通过' confirm page; the admin's reassign with from: Ada moved Ada's slot and the same token then rendered 'No longer your approval · 已不在你名下'. The step now passes `from` (or has the bound approver reassign their own slot); the clause is unchanged", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "#24824" }, - { "revision": 6, "date": "2026-10-20", "change": "automated.ref names a pin for negative[2] — carried finding 2 of round 12's report on PR #24824 (wave anchor #24579 round 13), against QA run #24779's 'unpinned' score. action-page-member.integration.test.ts's dead-link case compares the invalid, expired and consumed pages, through GET and POST, byte for byte with renderResultPage(kind) and no request, which renders no summary rows (action-link-pages.ts at base 7697ddcf94); a page leaking the summary fails it. That is negative[2]'s full clause on the member door, and the file's parity cases hold the mount to the same bytes. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 6, "date": "2026-10-20", "change": "automated.ref names a pin for negative[2] — carried finding 2 of round 12's report on PR #24824 (wave anchor #24579 round 13), against QA run #24779's 'unpinned' score. action-page-member.integration.test.ts's dead-link case compares the invalid, expired and consumed pages, through GET and POST, byte for byte with renderResultPage(kind) and no request, which renders no summary rows (action-link-pages.ts at base 7697ddcf94); a page leaking the summary fails it. That is negative[2]'s full clause on the member door, and the file's parity cases hold the mount to the same bytes. No clause changed", "ref": "#24850" } ] }, { diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 70aeba5f67c..eb0a92b38b6 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -124,7 +124,7 @@ "revision": 6, "date": "2026-10-20", "change": "fixtures.requires[2] added and variants[6] extended — checklist-accuracy findings C3 and C4 of QA run #24786 (wave anchor #24579 round 13), re-read at base 7697ddcf94. C3: acceptance[0]'s console leg needs `pnpm objectui:build` in the framework repo; on an unbuilt tree /_console/ answers 503 'The ObjectStack Console is not built …' (console.ts createConsoleNotBuiltPlugin), which QA run #24786 met, so the requirement now says so and how to record the leg. C4: the config-declared-default variant's scratch object must declare sharingModel, or the compile fails on security-owd-unset and os dev exits 1 (dev.ts). No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }, @@ -215,7 +215,7 @@ { "revision": 4, "date": "2026-10-03", "change": "text refresh only, no status change: six live texts named compile.ts's this.error() as the mechanism of the human path's exit 2. Since bf36edd0a1 that catch prints its own ✗ line and ends in this.exit(2), which raises the same status without oclif's second Error: rendering; every oracle (exit 2 on the human path, 1 under --json) still holds. Revisions 2 and 3 above keep their wording: they record what was true when written", "ref": "#21492" }, { "revision": 5, "date": "2026-10-17", "change": "steps[5] re-pointed (checklist-accuracy finding, #23878 close-out): as written ('add one undeclared authoring key') it is now a hard refusal, because object, field, report and enable shapes are closed, so it could only fail clause 5 for the wrong reason. The step now places the key at a nested strip-mode site (a field's object-form visibleWhen) under defineStack(…, { strict: false }) and names what the closed site does instead; clause 5's verify says the same. Re-run on main c91af8ed6 on a scratch stack: the key on a field → exit 2 'Unrecognized key(s) on this field: qaUndeclared' (default) and exit 1 'Validation failed … unrecognized_keys' (strict:false); the key inside visibleWhen under strict:false → exit 0 with '⚠ Undeclared authoring keys (1) — dropped at load; reported here, never refused' naming objects.qa_thing.fields.note.visibleWhen.qaUndeclared, and the artifact's visibleWhen without it; the same key under the default defineStack → exit 0 with defineStack's own one-line warning and no compile-side block", "ref": "#24036" }, { "revision": 6, "date": "2026-10-19", "change": "steps[2] and steps[5] name where the deliberate breaks must sit — checklist-accuracy findings of the 18.0 QA sweep (QA run #24551, cli.build-own-contract; wave anchor #24579 §3). Both plants were helper-authored on the stock fixtures, and ObjectSchema.create() parses at import (re-derived at base 9a7838edf5, packages/spec/src/data/object.zod.ts): the schema break died at config load with a raw Zod issue dump, and the nested strip-mode key was dropped by the helper with no advisory. The plants now sit on plain-literal objects. No clause changed", "ref": "#24649" }, - { "revision": 7, "date": "2026-10-20", "change": "acceptance[3] (clause, verify) states what each command answers, and fixtures.knownGaps[0] names the gap — checklist-accuracy finding B6 of QA run #24787 (wave anchor #24579 round 13). No spec docblock or ADR states either command's --json shape, so the clause follows the code at base 7697ddcf94 (compile.ts, validate.ts): os validate answers valid/errors where os build answers success/issues, and a refusal at load answers error and code (STACK_SCHEMA_INVALID) with no finding list on both. The '{ success: false, … issues }' the verify promised held for the author-time rule failure only. The parity the clause judges, warnings and conversions, is unchanged", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 7, "date": "2026-10-20", "change": "acceptance[3] (clause, verify) states what each command answers, and fixtures.knownGaps[0] names the gap — checklist-accuracy finding B6 of QA run #24787 (wave anchor #24579 round 13). No spec docblock or ADR states either command's --json shape, so the clause follows the code at base 7697ddcf94 (compile.ts, validate.ts): os validate answers valid/errors where os build answers success/issues, and a refusal at load answers error and code (STACK_SCHEMA_INVALID) with no finding list on both. The '{ success: false, … issues }' the verify promised held for the author-time rule failure only. The parity the clause judges, warnings and conversions, is unchanged", "ref": "#24850" } ] }, { @@ -638,7 +638,7 @@ { "revision": 5, "date": "2026-09-28", "change": "the knownGap and the qa.json source note were falsified by #20289 (PR #20341) and are corrected: `os test` gained `--tags` (any-of, exact, case-sensitive; deselected scenarios are counted and never passed; a selection that matches no scenario exits 0, or 1 under --fail-on-empty), and the report now heads each suite with its `name` and prints each scenario's `name` beside its id. What stays true is narrower: `scenario.requires` is declared but NOT CHECKED, pending the maintainer's decision on #20289. Steps and acceptance are unchanged — the green-path clause's scenarioId is still on every scenario line, now in brackets after the name, or bare when the name equals the id", "ref": "#20289" }, { "revision": 6, "date": "2026-09-28", "change": "the knownGap and the qa.json source note were falsified again by #20289's `requires` round (ruling B) and are corrected: `scenario.requires` is now CHECKED before the first step — `params` against the `os test` process environment, `services` against the target's discovery `services` (enabled and `available`) — and an unmet entry SKIPS the scenario with its reason, counted apart and never passed; an all-skipped run exits 0, or 1 under --fail-on-empty. `requires.plugins` is retired (refused at load, prescription names `requires.services`). Steps and acceptance are unchanged: the fixture suite declares no `requires`, so the green path runs every scenario exactly as before", "ref": "#20289" }, { "revision": 7, "date": "2026-10-19", "change": "steps[6] names an oracle that can be consulted — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.qa-suite-execution steps[6]; wave anchor #24579 §3): \"the server log carries no request attributable to it\" cannot be read, because the stock dev boot logs no per-request lines. Re-derived at base 9a7838edf5: the Hono server plugin logs a handler throw with method and path and no access line (packages/plugins/plugin-hono-server), and os test prints one `Scenario:` line per executed scenario under its suite heading (packages/cli/src/commands/test.ts). The step now reads the transcript. No clause changed", "ref": "#24649" }, - { "revision": 8, "date": "2026-10-20", "change": "fixtures.requires[0] states what the boot needs — checklist-accuracy finding C9 of QA run #24786 (wave anchor #24579 round 13): `pnpm dev -- --fresh -p PORT` boots without a console build (check:console-sha skips with 'No console dist' and exits 0), and the root dev chain refuses an unbuilt workspace first (check-dev-prereqs.mjs), read at base 7697ddcf94. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 8, "date": "2026-10-20", "change": "fixtures.requires[0] states what the boot needs — checklist-accuracy finding C9 of QA run #24786 (wave anchor #24579 round 13): `pnpm dev -- --fresh -p PORT` boots without a console build (check:console-sha skips with 'No console dist' and exits 0), and the root dev chain refuses an unbuilt workspace first (check-dev-prereqs.mjs), read at base 7697ddcf94. No clause changed", "ref": "#24850" } ] }, { @@ -883,7 +883,7 @@ "revision": 6, "date": "2026-10-20", "change": "steps[6] quotes the schema-verdict warning from its producer — checklist-accuracy finding C5 of QA run #24786 (wave anchor #24579 round 13): meta.ts printSchemaVerdict at base 7697ddcf94 prints 'Migrated stack does not yet pass schema validation — N refusal(s) left after the chain. Resolve them, then run `os validate`:' and then each refusal, where the step still quoted the older '… resolve the manual changes above …' wording. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }, @@ -1074,7 +1074,7 @@ { "revision": 2, "date": "2026-08-23", "change": "text refresh only, no capability change: #10679 landed the fix this item's expected-fail predicted, so that clause inverts into an ordinary pass. findMissingTests/findDeprecatedUsages now return a MonorepoTreeScan and the ✓ is reachable only from the scanned arm; outside a monorepo each check prints 'ℹ Skipped — no packages/spec/src in this directory (monorepo-only check)', with the resolved directory under -v. Rewritten against a REAL run of the shipped CLI in a `create-objectstack -t blank` scaffold and again from a monorepo checkout, not from source: (a) the expected-fail clause, step 7 and the item title inverted; (b) the row enumeration in acceptance[0] and step 1 corrected — measurement falsified the premise that only @objectstack/spec became conditional: `Installed packages` was ALREADY findings-only (installedPackageLedgerChecks returns [] on a clean ledger) and had never been an unconditional row, and NODE_ENV/Tenancy posture are finding-only too, so the enumeration now splits unconditional from finding-only rather than naming one exception; (c) the '@objectstack/spec Not built' negative flipped from an accepted note into a regression, its probe being gated; (d) two negatives added for the skip row's own wrong shapes (a ⚠ instead of a ℹ, and the row vanishing); (e) doctor-unexamined-spec-tree.test.ts added to the pins; (f) every line anchor in the item re-derived at this base — doctor.ts grew ~90 lines, so every number quoted at revision 1 had moved", "ref": "#10798" }, { "revision": 3, "date": "2026-10-17", "change": "steps[1] said how to stage the three config findings but not that the staged config must load (checklist-accuracy finding, #23878 close-out: 'orphan-view staging needs defineStack strict:false, the default load refuses it, and doctor then skips config analysis'). Re-measured on main c91af8ed6, the finding holds for a malformed view body only: a well-formed container bound to a missing object loads under the default defineStack and doctor reports it, so strict:false is an option, not a requirement. The step names the well-formed shape, the skip row a refused load prints, and the strict:false route; knownGaps[0] says the staged config must load. Re-run on a scratch config (circular lookup pair qa_a/qa_b, unreferenced qa_unused): with the container { name: 'qa_missing', object: 'qa_missing', list } under the default load, os doctor exit 0 with '⚠ Circular dependency: qa_a → qa_b → qa_a', '⚠ Object \"qa_unused\" is defined but not referenced …' and '⚠ View \"qa_missing\" references non-existent object \"qa_missing\"'; the same three under strict:false with a mismatched-name container; a container carrying viewKind/config → '⚠ Config load Could not load config for analysis (config checks skipped) — defineStack validation failed …' and none of the three rows; the restored config → '✓ Dependencies No circular references detected', '✓ Object usage All objects are referenced', '✓ View integrity All views reference valid objects', exit 0", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "steps[1] and acceptance[5] (clause, verify) name the clean baseline — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.doctor-health-report fixtures; wave anchor #24579 §3): the stock blank scaffold always carries an Object usage ⚠, so the restored config never reached the three ✓ rows. Re-derived at base 9a7838edf5: the blank template ships src/objects/note.object.ts (blank_note, renamed to the project namespace at scaffold) and empty views/apps/flows barrels (packages/create-objectstack/src/templates/blank), and findUnusedObjects (doctor.ts) counts only view, flow and app references. The clean run now adds one view over that object", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "steps[0] and acceptance[0] (clause, verify) name every unconditional row — checklist-accuracy finding C6 of QA run #24786 (wave anchor #24579 round 13). At base 7697ddcf94 doctor.ts's run body pushes seven rows under no condition: the six the item named and Package-authored scheduled work (scheduledWorkCheck, ON or OFF, #17396); and once a config loads, the config block prints its own Platform protocol row (checkProtocolVersionGap). The clause names that row beside the others, and the verify names the rows by their push sites and checks rather than by a count. The finding-only rows are unchanged", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 5, "date": "2026-10-20", "change": "steps[0] and acceptance[0] (clause, verify) name every unconditional row — checklist-accuracy finding C6 of QA run #24786 (wave anchor #24579 round 13). At base 7697ddcf94 doctor.ts's run body pushes seven rows under no condition: the six the item named and Package-authored scheduled work (scheduledWorkCheck, ON or OFF, #17396); and once a config loads, the config block prints its own Platform protocol row (checkProtocolVersionGap). The clause names that row beside the others, and the verify names the rows by their push sites and checks rather than by a count. The finding-only rows are unchanged", "ref": "#24850" } ] }, { @@ -1295,7 +1295,7 @@ "revision": 9, "date": "2026-10-20", "change": "fixtures.requires[1] and acceptance[3].verify corrected — checklist-accuracy findings C1 and C2 of QA run #24786 (wave anchor #24579 round 13), re-read at base 7697ddcf94. C1: under `--json`, plan's '[Registry] Registered view: NAME' lines go to stderr and stdout is the plan document alone (bootSchemaStack reserves stdout through json-stdout.ts reserveStdoutForJson); only the human path prints them on stdout. The control's conclusion holds. C2: the stock showcase declares one org-scoped identifier (showcase_field_zoo.f_autonumber), so requires[1] now asks for a second one, and says its partitioned unique index (uniq_OBJ_organization_id_FIELD) must be dropped before the repeat is seeded, after which os migrate plan reports the drop as index_mismatch (schema-drift.ts, the declared-index-missing arm). No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }, @@ -1509,7 +1509,7 @@ { "revision": 2, "date": "2026-08-22", "change": "the '// @capabilities' first-line override was RETIRED (#10917, maintainer ruling under ADR-0049 enforce-or-remove): measured on all four ordinary authoring shapes (.ts/.js/.mjs/an imported handler) it reached the extractor from NONE of them, so a documented directive silently taught a wrong convention and the mismatch surfaced far from its cause as a sandbox refusal. Its acceptance clause asserted behaviour that no longer exists — a runner would have filed a FAIL against the ruling — and is replaced by the covered route (body.capabilities as data) plus the inertness guarantee owed to configs that still carry the comment. Steps, fixtures and the extract-hook-body.ts line citations moved with it. No other clause changed; note the expected-fail clause still describes the pre-#10678 silent default and is stale on its own account, which is NOT this revision's subject", "ref": "claude/issue-10917-retire-capabilities-hook-directive" }, { "revision": 3, "date": "2026-10-17", "change": "clause 6's EXPECTED-FAIL has flipped (checklist-accuracy finding, #23879 and the #23878 close-out; the r2 history already flagged it stale): since #10678 the default build prints the refused handlers and --json carries them as bodyExtractionWarnings. The title, clause 6 (now a positive assertion that fails if the default path goes silent again), its verify and evidence, steps[1], knownGaps[1] and two source parentheticals now describe the surfaced default. Re-run on main c91af8ed6 on a scratch stack with the forbidden seven, a module-scope helper and a clean find hook: default os build exit 0 with '⚠ 8 handlers could not be lowered to a metadata body — bundled via the legacy runtime module instead (build still succeeds)' and one bullet per hook carrying its own reason (import and fetch prescribe a Connector recipe, require, process, globalThis, eval and new Function name themselves, the helper hook names 'helper'); --json exit 0, success true, bodyExtractionWarnings 8 records; --strict-body exit 1 '--strict-body: 8 callable(s) lack a metadata body'; the artifact carries no body on the eight, body.capabilities ['api.read'] on the find hook, and one objectstack-runtime bundle in dist", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "steps[0] and acceptance[0] (clause, verify, evidence) cover every FORBIDDEN_PATTERNS entry — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.hook-body-extraction-gates steps[0]; wave anchor #24579 §3): the array has grown past the original seven sandbox escapes. Re-derived at base 9a7838edf5 (extract-hook-body.ts): fourteen entries, the seven escapes plus .sudo( (#14010), .create( with Object.create exempt (#16249), ctx.dispatch.scope and ctx.submitted (#22810), and the hook-face-only ctx.provenance, ctx.ql and ctx.transaction (#22837). The clause already said EACH pattern; its count and its evidence now match the array", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "acceptance[6].verify counts from the roster — checklist-accuracy finding C7 of QA run #24786 (wave anchor #24579 round 13): it still said 'the forbidden seven' while steps[0] and acceptance[0] say fourteen, and QA run #24786 measured 14. At base 7697ddcf94 FORBIDDEN_PATTERNS (extract-hook-body.ts) holds 14 entries; the verify now names one hook per entry of that array, with no number, and notes that compile.ts lists at most 20 in the warning block. No clause changed", "ref": "claude/issue-24579-v18-sweep-round13" } + { "revision": 5, "date": "2026-10-20", "change": "acceptance[6].verify counts from the roster — checklist-accuracy finding C7 of QA run #24786 (wave anchor #24579 round 13): it still said 'the forbidden seven' while steps[0] and acceptance[0] say fourteen, and QA run #24786 measured 14. At base 7697ddcf94 FORBIDDEN_PATTERNS (extract-hook-body.ts) holds 14 entries; the verify now names one hook per entry of that array, with no number, and notes that compile.ts lists at most 20 in the warning block. No clause changed", "ref": "#24850" } ] }, { @@ -2136,7 +2136,7 @@ "revision": 2, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 13, round 6's theme-B rule): the ref listed seven spec, formula and lint unit pins with no clause qualifier, though acceptance[1] and acceptance[2] need os validate and os build. None of the seven runs the CLI (read at base 7697ddcf94). The pins are acceptance[0]'s own subject, so `automated` stays, narrowed to it. No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }, @@ -2208,7 +2208,7 @@ "revision": 3, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding C8 of QA run #24786 (wave anchor #24579 round 13, round 6's theme-B rule): the five pins spawn the CLI over their own fixture stacks, never the showcase, so acceptance[1]'s 'on the showcase' needs the live run, which QA run #24786 drove. The ref now names acceptance[0] for all five and acceptance[2] for verify-delete-behaviors.test.ts's NEGATIVE CONTROL case (read at base 7697ddcf94). No clause changed", - "ref": "claude/issue-24579-v18-sweep-round13" + "ref": "#24850" } ] }