From 9395b8f90e0d5a018b452c56b8f5f63f5e796989 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:15:31 +0000 Subject: [PATCH 01/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20cli.?= =?UTF-8?q?json:=2018.0=20pre-release=20ceremony=20clause,=20dev-boot=20co?= =?UTF-8?q?nsole=20split,=20procedural=20negatives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/cli.json | 124 +++++++++++++++------- 1 file changed, 86 insertions(+), 38 deletions(-) diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 3fb79b4014..109b8dfda4 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": 6, + "revision": 7, "priority": "P0", "surface": "cli", "personas": ["operator (local shell)"], @@ -18,7 +18,7 @@ "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", - "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" + "for acceptance[7], the console clause, 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 acceptance[7] blocked(environment), not a FAIL of the clause" ] }, "steps": [ @@ -30,14 +30,15 @@ "with instance A still running, boot instance B on the same requested port and capture the '↪ server bound to port (requested )' line", "run once with NONE of --database/--fresh/env-db set and no config-declared default datasource, and record the printed Database line (must be the project-anchored UNIFIED default file:/.objectstack/data/objectstack.db); then run with `--fresh` and record the '🧪 Fresh OS_HOME' tempdir line (its DB is the SAME unified filename under the ephemeral home) and its deletion on exit", "legacy compat-read: in a scratch project whose .objectstack/data holds a legacy dev.db and NO objectstack.db, boot with nothing chosen and capture both the Database line (must resolve to the legacy file — never a fresh empty objectstack.db beside it) and the one loud notice line naming that file and how to converge on the unified name", - "capture `echo $?` after every terminated invocation, against the expected codes: Ctrl-C/SIGINT ends a plain `os dev` with its serve child's exit code — 0 on a clean shutdown (the parent follows `code ?? 0`, packages/cli/src/utils/dev-restart.ts#ServeRestartCoordinator); a `--fresh` run's own SIGINT handler deletes the tempdir and exits 130 (SIGTERM 143); the no-config case of steps[3] exits 1" + "capture `echo $?` after every terminated invocation, against the expected codes: Ctrl-C/SIGINT ends a plain `os dev` with its serve child's exit code — 0 on a clean shutdown (the parent follows `code ?? 0`, packages/cli/src/utils/dev-restart.ts#ServeRestartCoordinator); a `--fresh` run's own SIGINT handler deletes the tempdir and exits 130 (SIGTERM 143); the no-config case of steps[3] exits 1", + "config-declared default datasource tier, for acceptance[2] (derived from the resolver source, not yet replayed live): in your own scratch project (fixtures.requires[1]), declare a NAMED sqlite datasource with a relative config.filename, e.g. `{ name: 'qa_home', driver: 'sqlite', config: { filename: '.objectstack/data/qa_home.db' } }` (the shape of the showcase's showcase-external.datasource.ts without its external block), plus a datasourceMapping rule `{ default: true, datasource: 'qa_home' }`, with every object declaring sharingModel (variants[6]). Build it with `os build`: `os dev` compiles only when no artifact exists, and this tier reads the compiled dist/objectstack.json. Boot with NONE of --database, --fresh, OS_DATABASE_URL, DATABASE_URL or TURSO_DATABASE_URL set, and record the Database line: it must name file: plus that filename resolved against the project root, never .objectstack/data/objectstack.db (packages/runtime/src/resolve-project-database.ts#readConfigDeclaredDefault, the rung after the explicit URL and the environment). If the boot cannot be staged, record this variant blocked(fixture) with the reason" ], "acceptance": [ { - "clause": "the boot reaches health: /api/v1/health answers 200 and /api/v1/ready succeeds, with the console served at /_console when --ui is passed", + "clause": "the boot reaches health: /api/v1/health answers 200 and /api/v1/ready succeeds", "oracle": "api", - "verify": "the health poll flips to 200 within the probe budget; ready returns success; /_console/ serves the login page", - "evidence": "curl outputs + console screenshot" + "verify": "the health poll flips to 200 within the probe budget; ready returns success. The console is acceptance[7], so a run with no console build scores this clause in full", + "evidence": "curl outputs" }, { "clause": "--seed-admin provisions the FIXED well-known dev admin (admin@objectos.ai / admin123) loginable via the real auth endpoint, and the seed is idempotent — a later boot with a different --admin-password does NOT overwrite the existing account", @@ -48,7 +49,7 @@ { "clause": "DB selection honors the ONE shared resolution matrix (#6469 resolveProjectDatabaseUrl, which dev maps its flags onto via resolveDevDatabase and start/migrate resolve through too): with nothing chosen, dev defaults to the PERSISTENT project-anchored sqlite file — the unified /data/objectstack.db, i.e. .objectstack/data/objectstack.db under the project — never the historical serve default of :memory: that wipes work on restart", "oracle": "log", - "verify": "the printed Database key-value per variant matches the matrix, tier for tier (explicit -d url, including `-d :memory:` — SQLite's own in-memory database; env url; config-declared default datasource; legacy dev.db/standalone.db compat-read WITH its notice line; otherwise the unified objectstack.db default) — and dev carries no fallback filename of its own", + "verify": "the printed Database key-value per variant matches the matrix, tier for tier (explicit -d url, including `-d :memory:` — SQLite's own in-memory database; env url; config-declared default datasource, staged by steps[9]; legacy dev.db/standalone.db compat-read WITH its notice line; otherwise the unified objectstack.db default) — and dev carries no fallback filename of its own", "evidence": "the per-variant Database boot lines" }, { @@ -74,6 +75,12 @@ "oracle": "log", "verify": "echo $? prints 1 and stderr carries the remedy line", "evidence": "captured stderr + exit code" + }, + { + "clause": "with --ui passed, the console is served at /_console/: it serves the login page", + "oracle": "screenshot", + "verify": "after steps[1]'s health 200, open /_console/: it renders the console's login page, confirmed by a screenshot before any DOM read (RUNNER rule 1). It needs a BUILT Console (fixtures.requires[2]); on an unbuilt tree /_console/ answers 503 text/plain 'The ObjectStack Console is not built, so this server has no Console to serve at /_console/.', and this clause is recorded blocked(environment), never a FAIL", + "evidence": "console screenshot" } ], "negative": [ @@ -125,6 +132,12 @@ "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": "#24850" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "acceptance[0] narrowed to its server half, with the console half appended as acceptance[7], and steps[9] stages the config-declared default tier. Checklist-accuracy findings of QA run #24838 (18.0 sweep round 4 at 307ce4c4; QA run #24841 lists the console build among its fixture gaps). (1) acceptance[0] bundled /_console/ with the health and ready probes, so a run with no console build could never pass the server half that held (health 200, ready 200, /_console/ 503 ConsoleNotBuilt). The halves now score apart, and fixtures.requires[2] points at acceptance[7]. (2) acceptance[2]'s matrix names the config-declared default datasource tier, and no step staged it. steps[9] stages it the way resolve-project-database.ts readConfigDeclaredDefault reads it at base feb09db14a: the compiled artifact's datasourceMapping rule { default: true, datasource: NAME } and that datasource's config.filename, resolved against the project root. The step says it is derived from source and not yet replayed live. No other clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -133,7 +146,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": 7, + "revision": 8, "priority": "P1", "surface": "build", "personas": ["operator (local shell)"], @@ -150,12 +163,13 @@ "on the clean config run `os build; echo $?` and capture the summary (metadata stats, Artifact path/size)", "run `os build --json` and capture the success payload", "break the schema: give one field an impossible shape (e.g. delete its type) and run `os build; echo $?` — capture the located Zod error. Make the break on an object authored as a PLAIN LITERAL handed to defineStack: ObjectSchema.create() parses at import (data/object.zod.ts), so the same break inside a helper-authored object (ObjectSchema.create / Field.*) dies at 'Loading configuration…' with the raw Zod issue array (path [fields, body, type], no object name) and never reaches the located 'defineStack validation failed … objects.0.fields.body.type' block this clause scores", - "break an author-time rule: author a flow whose Approval node approver expression does not parse (the #4409 worked example that once built green while os lint rejected it) and run `os build; echo $?`", + "break an author-time rule: author a flow whose Approval node approver expression does not parse (the #4409 worked example that once built green while os lint rejected it) and run `os build; echo $?`. Keep the config's `requires: ['automation', 'triggers']`, which the scaffolded blank app and the showcase both declare: when the flow carries an auto-launch trigger (record_change, schedule, time_relative or api) and `requires` lacks either token, defineStack refuses the stack at load (packages/spec/src/stack.zod.ts#validateTriggerCapability) and the build never reaches the author-time rule this step scores", "re-run both failing builds with --json and capture the failure payloads", "restore the config and add one UNDECLARED authoring key (the #3786 advisory class) where it is still an advisory: object, field, report and enable shapes are CLOSED, so an unknown key ON a field is a hard refusal (exit 2 out of defineStack, exit 1 'Validation failed … unrecognized_keys' under strict:false) — that is clause 2's territory, not this one. The advisory survives only at a nested strip-mode site, e.g. inside a field's object-form visibleWhen ({ dialect: 'cel', source: …, qaUndeclared: 1 }), on a RAW field object of a plain-literal object: written inside ObjectSchema.create() the key is stripped by that helper's own parse, with no advisory under either defineStack mode (build exit 0, the key silently absent). Build it under defineStack(…, { strict: false }) and run `os build; echo $?` — the 'Undeclared authoring keys (1) — dropped at load; reported here, never refused' block prints and the build passes; under the default defineStack the same key draws defineStack's own one-line warning and exit 0, without the compile-side block", "the exit-slot pair: make the config throw at LOAD time and run BOTH `os build; echo $?` (human path — expect exit 2 out of compile.ts's human-path catch: its own ✗ line, then this.exit(2)) and `os build --json; echo $?` (expect 1 via emitJson) — the pair that separates the framework's error exit from the #4873 leak", "repeat one successful run and one failing run verbatim and compare the two exit codes each time — a code that MOVES between identical runs is the #4873 signature, and only a repeat can see it", - "confirm `os build` and `os compile` produce identical behavior on the same input (build is the documented alias)" + "confirm `os build` and `os compile` produce identical behavior on the same input (build is the documented alias)", + "scope, not a probe: the DEEP gate content (date-arithmetic formula errors, retired-key tombstones) is api-backend.formula-gates / api-backend.enforce-or-remove-authoring-gates; cite pinned passes there. This item owns only the build's OWN exit/error/output contract" ], "acceptance": [ { @@ -197,8 +211,7 @@ ], "negative": [ "exit 0 with no artifact written, or nonzero on the clean config, is a FAIL", - "an exit code that tracks a computed value — a duration, a count, a finding total — is the #4873 leak this item's exit clause exists for: a SUCCESSFUL run exiting nonzero, or any code that differs between two identical runs, FAILS. Oclif's fixed ExitError 2 on the human path (its catch's this.exit(2)) is NOT that leak and must not be filed as one (that reading is what collided with cli.flag-command-error-ux, reconciled in #7648)", - "the DEEP gate content (date-arithmetic formula errors, retired-key tombstones) is api-backend.formula-gates / api-backend.enforce-or-remove-authoring-gates — cite pinned passes there; this item owns only the build's OWN exit/error/output contract" + "an exit code that tracks a computed value — a duration, a count, a finding total — is the #4873 leak this item's exit clause exists for: a SUCCESSFUL run exiting nonzero, or any code that differs between two identical runs, FAILS. Oclif's fixed ExitError 2 on the human path (its catch's this.exit(2)) is NOT that leak and must not be filed as one (that reading is what collided with cli.flag-command-error-ux, reconciled in #7648)" ], "traps": ["stale-dist"], "source": [ @@ -215,7 +228,8 @@ { "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": "#24850" } + { "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" }, + { "revision": 8, "date": "2026-10-20", "change": "steps[3] names the trigger-capability requirement, and negative[2] moved to steps[9]. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The #4409 approver fixture needed requires: ['automation', 'triggers']. Read at base feb09db14a: stack.zod.ts validateTriggerCapability refuses an auto-launched flow at load when requires lacks either token, ahead of the author-time rules this step scores; the blank scaffold (create-objectstack's templates/blank) and the showcase both declare the pair, so the step says to keep it. (2) negative[2] (the deep gate content belongs to other items) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[9]. No clause changed", "ref": "#PENDING-24579R16" } ] }, { @@ -524,7 +538,7 @@ "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": 8, + "revision": 9, "priority": "P1", "surface": "cli", "personas": ["operator (local shell)", "suite author (writes qa/*.test.json)"], @@ -554,7 +568,8 @@ "variant sweep: stage one scenario per TestActionTypeSchema member (8) and run them in one suite; record each member's own verdict and the exact error text of every refusal — no member inferred from a sibling", "zero-match glob: `os test 'qa/nothing-matches-*.test.json' --url http://localhost:; echo $?` and record BOTH the message and the exit code", "glob discipline (#7363): `os test 'qa/**/*.test.json'` from the app dir — it must complete in seconds and find the same suite, never the multi-minute walk that preceded the lazy resolver", - "persona check: re-run the green suite with NO --token and record which scenario survives (health) and which fails (the data roundtrip, HTTP 401)" + "persona check: re-run the green suite with NO --token and record which scenario survives (health) and which fails (the data roundtrip, HTTP 401)", + "what counts as a run: satisfying this item by executing the unit pins in `source` instead of driving a booted app is not a run. Those pins cover the load boundary, the glob resolver and one assertion operator, and none of them proves a suite reaches a real server, which is why this item carries no `automated` entry" ], "acceptance": [ { @@ -610,8 +625,7 @@ "a zero-match glob prints `Found 0 test suites.` + `No test files found matching: ` and exits **0** — a green exit from a run that loaded no suite. Since #7848 that is a DECLARED posture (stated in `--help`, opt out with `--fail-on-empty`, which exits 1) rather than an accident, but the default is unchanged and so is the trap: a CI job wired to `os test` with a typo'd path still reports success forever unless it passes the flag. A run record that ticks this item without quoting a `Found N test suites.` line with N > 0 is the false positive this item exists to prevent", "a malformed suite that REACHES the runner — reported passed having executed nothing, or dying as a TypeError inside runSuite with no file named — is the #6247 regression", "a `contains` against a path the result does not carry reporting ✅ is the #7256 regression", - "the five record-shaped action types (`create_record`, `read_record`, `update_record`, `delete_record`, `query_records`) **round-trip** against a stock server, and an **HTTP 404** from any of them is now the regression this clause names. They answered 404 until #7848 — `HttpTestAdapter` built `${baseUrl}/api/data/:object` where the server serves `{apiPath}/data/:object` with `apiPath` = `/api/v1`, and `update_record` issued PUT where the route is PATCH. Both are fixed and the prefix is derived from `RestApiConfigSchema` + `CrudEndpointsConfigSchema` rather than written down, so this cannot regress by a literal drifting. Re-measured 2026-08-12 on showcase, one scenario per member with no shared setup: 7 of 8 execute and assert, `run_script` refuses by name", - "satisfying this item by executing the unit pins in `source` instead of driving a booted app is not a run: those pins cover the load boundary, the glob resolver and one assertion operator, and none of them proves a suite reaches a real server — which is why this item carries no `automated` entry" + "the five record-shaped action types (`create_record`, `read_record`, `update_record`, `delete_record`, `query_records`) **round-trip** against a stock server, and an **HTTP 404** from any of them is now the regression this clause names. They answered 404 until #7848 — `HttpTestAdapter` built `${baseUrl}/api/data/:object` where the server serves `{apiPath}/data/:object` with `apiPath` = `/api/v1`, and `update_record` issued PUT where the route is PATCH. Both are fixed and the prefix is derived from `RestApiConfigSchema` + `CrudEndpointsConfigSchema` rather than written down, so this cannot regress by a literal drifting. Re-measured 2026-08-12 on showcase, one scenario per member with no shared setup: 7 of 8 execute and assert, `run_script` refuses by name" ], "variants": [ "api_call — the only action type that reaches the real API surface (target is a path or absolute URL; `payload.method`/`payload.body` shape the request)", @@ -647,7 +661,8 @@ { "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": "#24850" } + { "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" }, + { "revision": 9, "date": "2026-10-20", "change": "negative[4] moved to steps[12]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a runner rule (executing the source pins instead of driving a booted app is not a run) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[12]. No clause changed", "ref": "#PENDING-24579R16" } ] }, { @@ -753,7 +768,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": 6, + "revision": 7, "priority": "P1", "surface": "cli", "personas": ["build-time author (no session)"], @@ -781,7 +796,7 @@ "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", "confirm the semantic-only entries behave as ADVISORIES: they are reported under ` manual change(s) require your judgment:` with their why/verify text, never silently 'fixed' (a semantic change has no mechanical rewrite by definition)", - "WRITE MODE (#9591): run `os migrate meta --help` and record the --write line. Then, on a SECOND fresh scratch copy carrying the same planted spellings, keep a byte copy of the tree and run `os migrate meta --from --write`; capture stdout + the exit code. Read the `Wrote of mechanical change(s) into file(s):` block (each site `: ()`), any ` mechanical change(s) left for you to apply by hand:` block (each with its `not written []: `), and the closing `Re-ran the chain over the written sources: …` line. Diff the tree against its byte copy: the changed files are exactly the files the block names, each hunk sits at a listed site, and every other byte (comments, formatting, key order) is unchanged. Then, over the WRITTEN tree: re-run `os migrate meta --from ` without --write (no written site is still listed under Applied; the semantic advisories are listed exactly as before), run `--write` a second time (it writes nothing: `--write: the chain made no mechanical change here, so no file was written.`), and run `--write --json` on a third fresh copy (exit 0; the payload's `write.status` is `written` and `write.written[]` names each site with its file and line)" + "WRITE MODE (#9591): run `os migrate meta --help` and record the --write line. Then, on a SECOND fresh scratch copy carrying the same planted spellings, keep a byte copy of the tree and run `os migrate meta --from --write`; capture stdout + the exit code. Read the `Wrote of mechanical change(s) into file(s):` block (each site `: ()`), any ` mechanical change(s) left for you to apply by hand:` block (each with its `not written []: `), and the closing `Re-ran the chain over the written sources: …` line. Diff the tree against its byte copy: the changed files are exactly the files the block names, plus, when the run owed the manifest's declared protocol range an edit (a tree declaring engines.protocol '^17'; the stock showcase declares '^18'), the config file that the separate 'Rewrote the declared protocol range, so the load admits protocol N:' block names with its line. That rewrite is not one of the chain's M changes, so the 'Wrote …' block never lists it, and `--json` reports it as write.range, apart from write.written[]. Each hunk sits at a listed site, and every other byte (comments, formatting, key order) is unchanged. Then, over the WRITTEN tree: re-run `os migrate meta --from ` without --write (no written site is still listed under Applied; the semantic advisories are listed exactly as before), run `--write` a second time (it writes nothing: `--write: the chain made no mechanical change here, so no file was written.`), and run `--write --json` on a third fresh copy (exit 0; the payload's `write.status` is `written` and `write.written[]` names each site with its file and line)" ], "acceptance": [ { @@ -829,7 +844,7 @@ { "clause": "--write rewrites the authored sources IN PLACE for exactly the mechanical changes it can prove, and for nothing else (#9591): each planted spelling traced to a literal is written at its site, each one it cannot trace is listed with a refusal kind and reason, no semantic advisory is written, every byte outside a written site is unchanged, and the write is held to a re-run of the chain over the written sources — so the written tree no longer carries the converted spelling, and a second --write has nothing to do", "oracle": "log", - "verify": "the WRITE MODE step: the `Wrote of mechanical change(s) into file(s):` block names a file and line for each traced planted spelling, N plus the by-hand count equals M (the Applied count of the dry run over the same input), and the run closes with `Re-ran the chain over the written sources: no mechanical change remains.` (or `only the change(s) left above remain.`) and exits 0; the diff against the pre-run byte copy touches only the named files, only at the named sites; the re-run without --write lists no written site under Applied and the same semantic advisories; the second --write prints `--write: the chain made no mechanical change here, so no file was written.`; `--write --json` exits 0 with `write.status` `written`. ⛔ A write the re-run disagrees with must come back `restored` with exit 1 and every file at its old bytes, never half-converted (meta.ts writeSources) — a reported `written` over a tree the re-run still converts is a FAIL", + "verify": "the WRITE MODE step: the `Wrote of mechanical change(s) into file(s):` block names a file and line for each traced planted spelling, N plus the by-hand count equals M (the Applied count of the dry run over the same input), and the run closes with `Re-ran the chain over the written sources: no mechanical change remains.` (or `only the change(s) left above remain.`), which ends ` The declared protocol range now admits protocol N.` when the run rewrote the range, and exits 0; the diff against the pre-run byte copy touches only the named files, only at the named sites: the 'Wrote …' block's, and the range block's one site when the run owed the range an edit (meta.ts printRangeOutcome); the re-run without --write lists no written site under Applied and the same semantic advisories; the second --write prints `--write: the chain made no mechanical change here, so no file was written.`; `--write --json` exits 0 with `write.status` `written`. ⛔ A write the re-run disagrees with must come back `restored` with exit 1 and every file at its old bytes, never half-converted (meta.ts writeSources) — a reported `written` over a tree the re-run still converts is a FAIL", "evidence": "the --write stdout and exit code, the before/after tree diff, the re-run stdout, the second --write line, and the --json write block" } ], @@ -837,7 +852,7 @@ "a retirement message prescribing `os migrate meta --from N` for a spelling the chain does not LIST — the prescription is then a dead end, and the author who follows it is worse off than one who was told nothing", "a listed edit whose declared successor ALSO does not parse (pointing the author at a second retired name)", "a run WITHOUT --write that mutates the authored sources is a FAIL of the untouched-sources clause — the rewrite is opt-in", - "a --write that changes any byte outside a listed site, writes a semantic advisory, drops a planted spelling from both its written and its by-hand list, or reports the write done while the re-run over the written tree still converts a written site, is a FAIL of the write clause", + "a --write that changes any byte outside a listed site (a site the 'Wrote N of M mechanical change(s) into K file(s)' block lists, or the declared protocol range its 'Rewrote the declared protocol range' block names), writes a semantic advisory, drops a planted spelling from both its written and its by-hand list, or reports the write done while the re-run over the written tree still converts a written site, is a FAIL of the write clause", "a below-floor --from that silently no-ops and exits 0", "a replay from the target major that reports changes, or two identical runs whose stdouts disagree", "`Migrated stack is schema-valid` printed while manual changes are still outstanding — the same dishonesty as reporting 'migrated' over a semantic change nobody applied" @@ -893,6 +908,12 @@ "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": "#24850" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "negative[3], steps[11] and acceptance[7].verify count the declared-protocol-range rewrite as a listed site. Checklist-accuracy finding n3 of QA run #24841 (18.0 sweep round 4 at 307ce4c4): `--write` also moved engines.protocol (^17 → ^18) in objectstack.config.ts, announced outside the 'Wrote N … into K file(s)' block, so 'the changed files are exactly the files the block names' read a correct write as a FAIL. Measured on main's CLI text at base feb09db14a (meta.ts, since #22252): the 'Wrote …' block lists only the chain's files and sites; printRangeOutcome prints a separate 'Rewrote the declared protocol range, so the load admits protocol N:' block naming the file and line; the closing 'Re-ran the chain …' line then adds ' The declared protocol range now admits protocol N.'; and --json reports it as write.range, apart from write.written[]. No clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1182,7 +1203,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": 9, + "revision": 10, "priority": "P1", "surface": "cli", "personas": ["operator (local shell, pre-repair audit)"], @@ -1206,7 +1227,8 @@ "run `os migrate duplicates --object ` and `--object ` — capture .filter in both payloads", "run `os migrate duplicates --database-url file:/tmp//dup.db` and confirm it reaches the same DB (the flag also honors OS_DATABASE_URL)", "negative (no_sql_seam): run `pnpm --filter @objectstack/cli exec vitest run src/commands/migrate/duplicates.null-seam.test.ts` and cite its pass — the seam double that answers no result set, driving the real command; then run `os migrate duplicates --database-url memory://qa; echo $?` and capture the boot_failed payload (the retired engine is refused at boot, naming the replacement)", - "stderr/stdout split: confirm report.json parses as ONE JSON document — the boot's own log lines must have gone to stderr (#6217)" + "stderr/stdout split: confirm report.json parses as ONE JSON document — the boot's own log lines must have gone to stderr (#6217)", + "flags, not a probe: there is NO --json flag. stdout is always the report (the payload IS the deliverable, #8928 point 3), so probing `--json` and reading the oclif Nonexistent-flag exit 2 as this command's contract is a runner error, not a finding" ], "acceptance": [ { @@ -1230,7 +1252,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. 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", + "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, preceded by '[Registry] Overwriting view: NAME' where the row lands on a key the registry already holds, as the seeded same-name pair does (QA run #24841 read 'Overwriting view: qa_dup_view'; packages/objectql/src/registry.ts#registerItem). The objectql registry prints both 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" }, { @@ -1261,7 +1283,6 @@ "negative": [ "any write to the inspected DB — a created sqlite file, a synced column, a mutated counter — is the FAIL the ruling's read-only boot exists for", "a within-partition repeat surfacing in .duplicates misdiagnoses a unique-index defect as tenancy damage", - "there is NO --json flag: stdout is always the report (the payload IS the deliverable, #8928 point 3) — probing `--json` and reading the oclif Nonexistent-flag exit 2 as this command's contract is a runner error, not a finding", "an empty .duplicates from a run whose .skipped is non-empty must not be summarized as 'no duplicates' without naming what was skipped" ], "traps": ["stale-dist", "absence-inference"], @@ -1305,6 +1326,12 @@ "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": "#24850" + }, + { + "revision": 10, + "date": "2026-10-20", + "change": "acceptance[3].verify names the Overwriting line, and negative[2] moved to steps[9]. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) For the seeded same-name pair the plan printed '[Registry] Overwriting view: qa_dup_view', where the verify named only 'Registered view'. Read at base feb09db14a in registry.ts registerItem: it prints 'Overwriting TYPE: KEY' when the key is already held, and 'Registered TYPE: KEY' at the end of every registration. (2) negative[2] (there is no --json flag, and reading its exit 2 as this command's contract is a runner error) stated a runner rule with no product expectation, so it could only score not-run and held the item at partial. Its text moved, unchanged in substance, to steps[9], and negative[3] is now negative[2]; nothing in the item cites either by index. The record's other finding, that planting the within-partition repeat needs the partitioned unique index dropped first, no longer holds: revision 9 (#24850) put it in fixtures.requires[1] (schema-drift.ts buildIndexName). No clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1313,7 +1340,7 @@ "title": "os datasource list-tables/introspect/validate: the federation door answers, the generated draft is cwd-jailed and compiles, and schema drift maps to ✗ diffs with exit 1", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "cli", "personas": ["operator (local shell) whose token HOLDS manage_platform_settings — all three commands address federation READ routes, which require it since #9901/#10255", "integration author (adopting a remote table), same capability"], @@ -1327,12 +1354,12 @@ "knownGaps": [ "the three CLI commands are unit-tested against the server's wrapped envelope since #10675 (packages/cli/src/commands/datasource/envelope-unwrap.test.ts). That suite drives each command over a stubbed fetch, so this item stays their only end-to-end coverage against a live server", "the #5594 carve-out applies to the fixture DB: .objectstack/data/showcase_external.db is a cwd-relative app-declared path that survives --fresh by documented design — do not file its persistence as a bug", - "acceptance[0]'s NARROWING half (--schema narrows; a non-matching schema prints 'No remote tables found.') cannot be observed on the stock fixture. SQLite tables carry no schema, and listRemoteTables applies ?schema= only to a table it can attribute to one (packages/services/service-datasource/src/external-datasource-service.ts: a table with no schema part is never filtered), so every --schema run lists all tables (QA run #21721, parked on #22017 row 24). Score that half from the pins in `automated`. The unpinned remainder, the CLI turning --schema into ?schema= (list-tables.ts), is observable live against a schema-bearing driver such as postgres, or with no driver at all: the request line a local echo server receives (steps[1])" + "acceptance[0]'s NARROWING half (--schema narrows; a non-matching schema prints 'No remote tables found.') cannot be observed on the stock fixture. SQLite tables carry no schema, and listRemoteTables applies ?schema= only to a table it can attribute to one (packages/services/service-datasource/src/external-datasource-service.ts: a table with no schema part is never filtered), so every --schema run lists all tables (QA run #21721, parked on #22017 row 24). On the stock fixture record that half blocked(fixture), with the pins in `automated` beside it as evidence of their own layers only (RUNNER rule 6). The unpinned remainder, the CLI turning --schema into ?schema= (list-tables.ts), is observable live against a schema-bearing driver such as postgres, or with no driver at all: the request line a local echo server receives (steps[1])" ] }, "steps": [ "boot showcase on a scratch port and mint the admin token (the cli.qa-suite-execution recipe)", - "`os datasource list-tables showcase_external --url http://localhost: --token ` — capture the table listing; repeat with `--schema ` and with a schema that matches nothing ('No remote tables found.'). On the stock SQLite fixture both runs list every table, since there is no schema to filter on: record that and score the narrowing from the pins. The forwarding half (the CLI turning --schema into ?schema=) needs no schema-bearing driver: re-run list-tables with --url pointed at a local echo server (any listener that prints the request line) and read GET /api/v1/datasources/showcase_external/external/tables?schema=S off it (QA run #24551 observed ?schema=mart)", + "`os datasource list-tables showcase_external --url http://localhost: --token ` — capture the table listing; repeat with `--schema ` and with a schema that matches nothing ('No remote tables found.'). On the stock SQLite fixture both runs list every table, since there is no schema to filter on: record that, and record the narrowing blocked(fixture) with the pins cited beside it (RUNNER rule 6). The forwarding half (the CLI turning --schema into ?schema=) needs no schema-bearing driver: re-run list-tables with --url pointed at a local echo server (any listener that prints the request line) and read GET /api/v1/datasources/showcase_external/external/tables?schema=S off it (QA run #24551 observed ?schema=mart)", "`os datasource introspect showcase_external --table --url … --token ` — draft printed to stdout; then re-run with `--out objects/qa_external.object.ts` and capture 'Wrote objects/qa_external.object.ts' plus any 'REVIEW: column …' warnings", "escape probes: re-run with `--out ../qa-escape.object.ts` and `--out /tmp/qa-escape.object.ts` — capture both refusals and `echo $?`, and confirm neither file exists afterwards", "wire the generated object into the scratch config and run `os build; echo $?`. The draft is named after the datasource's own prefix (introspecting showcase_external's customers table yields showcase_customers), and defineStack's namespace-prefix check refuses an object whose name lacks the package namespace — so give the scratch manifest namespace: 'showcase' (the draft's prefix), or rename the object to NAMESPACE_…, before reading the build verdict; a mismatched namespace scores the scratch wiring, not the generated draft", @@ -1343,7 +1370,7 @@ { "clause": "list-tables prints the remote tables with schema qualification and column counts (plus row estimates when the driver reports them) through GET /api/v1/datasources/:name/external/tables, and --schema narrows via the ?schema= query — this item pins the CLI's /external/tables FEDERATION door specifically; the admin twin GET /:name/remote-tables is a different mount owned by the integration-system area", "oracle": "log", - "verify": "the listing matches the fixture DB's actual tables (cross-check with sqlite3.tables on showcase_external.db); on a schema-bearing driver the schema filter narrows and the no-match run prints 'No remote tables found.' (on the stock SQLite fixture that half is scored from the pins in `automated` — fixtures.knownGaps) (list-tables.ts; route registered in packages/rest/src/external-datasource-routes.ts, ledgered at rest-route-ledger.ts)", + "verify": "the listing matches the fixture DB's actual tables (cross-check with sqlite3.tables on showcase_external.db); on a schema-bearing driver the schema filter narrows and the no-match run prints 'No remote tables found.' (on the stock SQLite fixture that half cannot be observed, fixtures.knownGaps: record it blocked(fixture), naming a schema-bearing driver, with the pins in `automated` beside it as evidence of their own layers only, never its pass (RUNNER rule 6, a pin scores only the layer it drives): the federation route's filter through the real Hono adapter, and the CLI's empty-list line over a stubbed fetch) (list-tables.ts; route registered in packages/rest/src/external-datasource-routes.ts, ledgered at rest-route-ledger.ts)", "evidence": "the listings + the sqlite3 cross-check" }, { @@ -1422,6 +1449,12 @@ "date": "2026-10-19", "change": "steps[1] and knownGaps[2] add the live reading of the unpinned forwarding half — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.datasource-introspect-codegen acceptance[0]; wave anchor #24579 §3): the knownGap said the CLI's flag-to-query forwarding is observable only against a schema-bearing driver, and the run proved it against a local echo server (GET …/external/tables?schema=mart). Re-derived at base 9a7838edf5: list-tables.ts builds `?schema=${encodeURIComponent(flags.schema)}` and sends it. No clause changed", "ref": "#24649" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "acceptance[0].verify, fixtures.knownGaps[2] and steps[1] no longer score the narrowing half from the pins. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, a log clause's half), read under RUNNER rule 6. acceptance[0] is a log clause, the CLI's printed listing. Its pins drive the federation route through the real Hono adapter (remote-tables-twin.equivalence.test.ts), the service (external-datasource-service.test.ts) and the CLI over a stubbed fetch (envelope-unwrap.test.ts), so on the stock SQLite fixture that half is recorded blocked(fixture) with the pins beside it. steps[1] and knownGaps[2] said the same and now say this. automated.ref is unchanged. Read at base feb09db14a. No clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1430,7 +1463,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": 5, + "revision": 6, "priority": "P1", "surface": "cli", "personas": ["build-time author (no session)"], @@ -1451,7 +1484,8 @@ "the inference matrix, one hook each on a clean config: ctx.api.object('x').find(…) · ctx.api.object('x').update(…) · const api = ctx.api; api.object('x').find(…) · ctx.crypto.randomUUID() · ctx.log.info(…) · a body whose ONLY crypto call is ctx.crypto.hash(…) · a body carrying the retired @capabilities first-line comment and no matching calls (#10917 — it must contribute nothing) · a hook supplying body.capabilities as data instead of a handler · an implicit-return arrow — `os build` then jq each hook's body.capabilities and isExpression handling", "free-identifier probe: a handler calling a module-scope helper function — `os build; echo $?` (expect green, handler bundled, no body) then `os build --strict-body; echo $?` (expect exit 1 naming the identifier)", "all-body-only run: remove every non-extractable handler and confirm the 'Skipping legacy runtime bundle (all N callables are body-only)' line and that no objectstack-runtime.*.mjs remains in dist/", - "re-run the strict-body failure with --json and capture the { success: false, error: 'strict-body: missing body', issues } payload" + "re-run the strict-body failure with --json and capture the { success: false, error: 'strict-body: missing body', issues } payload", + "scope, not a probe: this item does NOT own the runtime sandbox. Proving a token's enforcement belongs to records-forms' L2 sandbox coverage, and the build's generic exit contract to cli.build-own-contract" ], "acceptance": [ { @@ -1501,8 +1535,7 @@ "a forbidden body reaching the artifact AS body.source is the sandbox-bypass FAIL the whole allow-list exists for — worse than any warning question", "a crypto token inferred from ctx.crypto.hash is the #4391 regression: a build blessing a body that can only throw at runtime", "--strict-body exiting 0 while any callable lacks a body, or a bare 'could not parse' where a pattern's worded reason belongs", - "an alias (const api = ctx.api) escaping api.read/api.write inference would ship a body the sandbox then rejects at first call — the over-inclusive regex is deliberate, its narrowing is a regression", - "this item does NOT own the runtime sandbox: proving a token's enforcement belongs to records-forms' L2 sandbox coverage, and the build's generic exit contract to cli.build-own-contract" + "an alias (const api = ctx.api) escaping api.read/api.write inference would ship a body the sandbox then rejects at first call — the over-inclusive regex is deliberate, its narrowing is a regression" ], "traps": ["stale-dist", "absence-inference"], "source": [ @@ -1518,7 +1551,8 @@ { "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": "#24850" } + { "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" }, + { "revision": 6, "date": "2026-10-20", "change": "negative[4] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule (the runtime sandbox and the generic exit contract belong to other items) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]; fixtures.knownGaps[0] keeps the sandbox half too. No clause changed", "ref": "#PENDING-24579R16" } ] }, { @@ -1912,7 +1946,7 @@ "title": "`os migrate organization-ownership` upgrades a database 17.x or an 18.0 pre-release (18.0.0-next.*) created (18.x): a bare run writes the reviewed plan and changes nothing; `--apply --plan-file PLAN --backup-taken` runs every step — attribution through the moves, the declared webhook and sharing-rule settlements, the column drops and NOT NULL — and certifies the marker at this release's ownership shape generation; the served boot refuses an uncertified database, naming the command, and boots a certified one; `os migrate apply --allow-destructive` still withholds every organization_id drop the ceremony owns, and `os migrate comment-reactions --apply` stays pinned", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P0", "surface": "cli", "personas": [ @@ -1922,7 +1956,8 @@ "app": "showcase", "requires": [ "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", - "a COPY of a v17-shaped SQLite DB (platform-core.v18-boot-refusals names how to get one) — never the original" + "a COPY of a v17-shaped SQLite DB (platform-core.v18-boot-refusals names how to get one) — never the original", + "for acceptance[5] (the 18.0 pre-release upgrade), a COPY of a database an 18.0 pre-release created and served, never the original. Build it with that release's own CLI, never the run subject's: `os dev --seed-admin` over a fresh file DB in a scratch showcase copy, run from a linked worktree at the release's tag commit, each built in its own worktree and never by switching the checkout under test (npm's `next` dist-tag is `@objectstack/cli@18.0.0-next.5`; QA run #24838 used its tag commit 820fd5aef0, and 18.0.0-next.3's 2c9bf9430a). Then drive that release's HTTP doors so the rows the ceremony moves exist: a member invited and signed up and a second made by admin create-user, a password-reset request, an email-verification resend, an organization invitation, the global `mail` settings written twice, the declared webhook activated, and admin and member tasks. ⚠️ scripts/gen-upgrade-harness-fixture.mjs does not run against a published 18.0 pre-release, and the committed harness fixture was generated from main, not from a published build (QA run #24838), so neither stands in for this database. `rsync` is not installed here; copy with `tar` or `cp -a`. Where no such database can be built, record acceptance[5] blocked(fixture), never pass" ], "knownGaps": [ "a walled database (group or isolated posture) holding a CUSTOMIZED copy of a declared webhook or sharing rule blocks at its settlement step, by design: the declaration's switch is deployment-wide, so the ceremony does not switch it off for every organization on the operator's behalf — that arm is scored by the unit pins (organization-ownership-apply.generation-1.test.ts), not by a step here" @@ -1933,7 +1968,8 @@ "`--apply` without --plan-file; then `--apply --plan-file /tmp/RUN/plan.json --backup-taken --out /tmp/RUN/apply.json --json`; then read `SELECT verified_at, blocking, details FROM sys_migration WHERE id = 'adr-0131-organization-ownership'`", "boot the showcase (`os serve`) over the migrated copy, and over a second, unmigrated copy", "`os migrate apply --allow-destructive --yes --json` on a fresh unmigrated v17-shaped copy; checksum the DB before and after", - "`os migrate comment-reactions --apply` twice" + "`os migrate comment-reactions --apply` twice", + "18.0 pre-release leg, for acceptance[5]: on a COPY of the fixtures.requires[2] database, read every table's rows through SQLite (node:sqlite, the `-wal` included: no sqlite3 CLI here) and keep per-table counts; boot the run subject's `os serve` over it (OS_DATABASE_URL=file:COPY) and keep the output and exit code; run steps[0] and steps[1] on it (the bare plan, then the reviewed `--apply --plan-file PLAN --backup-taken`); boot `os serve` over it again; read the rows again. Then, on that boot: sign in as each user the pre-release made, with its 18.0 password and once with a wrong one; read the token out of a reset mail the pre-release sent (now a sys_identity_email row) and complete that reset; and PATCH one showcase_task" ], "acceptance": [ { @@ -1965,6 +2001,12 @@ "oracle": "build", "verify": "exit code + the JSON message and tables + checksums before/after", "evidence": "the excerpt and the checksum pair" + }, + { + "clause": "an 18.0 pre-release database (one an 18.0.0-next.* release created and served) takes the same path as a v17 one and keeps its data: the served boot refuses it, the reviewed apply completes, and the next boot serves it with its identity mail, its global setting audit, its pending mail jobs and its declared webhook carried over", + "oracle": "build", + "verify": "steps[5], each leg read at its own layer. Boot output: the first `os serve` exits 1 before any listening line, opening \"This database carries no organization-ownership marker\" and naming the tables still carrying organization_id (sys_flow_credential and sys_secret on 18.0.0-next.5); a database an 18.0.0-next.3 ceremony already certified at shape generation 0 is refused instead with \"… certified by an organization-ownership ceremony of shape generation 0, and this release requires generation 1 …\". The apply exits 0 with complete true and blocking 0, and the second boot reaches its serving line. Row reads, before the plan and after the second boot: every identity mail row (related_object sys_user, which is the reset and verification mail, and any magic-link mail) is in sys_identity_email under its original id, equal over the columns both tables hold, and gone from sys_email, while organization invitations stay in sys_email (packages/cli/src/utils/organization-ownership-apply.ts#SIBLING_MOVES); every sys_setting_audit row with scope 'global' is in sys_platform_setting_audit and gone from sys_setting_audit; every pending or running email.send.async or email.attachment.reclaim job naming a moved mail carries object sys_identity_email in its payload and metadata, while completed jobs and the organization-invitation job are unchanged; and no row disappears outside the apply report: each pre-existing table's count after equals its count before, less what the report's steps say they moved or deleted. The door's answer: each pre-release user signs in with its 18.0 password (200; a wrong one 401); the token read from the moved reset mail resets (200); and the one showcase_task PATCH writes exactly one sys_http_delivery row whose ref_id is declared:showcase_task_changed", + "evidence": "the refused boot's excerpt and exit code, the apply report's complete and blocking, the serving line; the before and after reads of sys_email, sys_identity_email, sys_setting_audit, sys_platform_setting_audit and the mail jobs, with per-table counts; the sign-in, reset and delivery answers" } ], "negative": [ @@ -2004,6 +2046,12 @@ "date": "2026-10-19", "change": "re-pointed to the opened ceremony (18.x steps 5 and 6, one landing by ruling record 6270035914 on the v18 epic): the registered command runs the ceremony again, the 18.0 refusal and its pin (organization-ownership.release-refusal.test.ts) are deleted, and the boot guard recognises a database by its birth marker and its ownership shape generation (ruling record 6269955919, ① A). Revision 1's arms are restated against it: the plan written, the reviewed apply certifying at the current generation, the served boot refusing an unmigrated copy and booting the migrated one; the drop-gate clause is kept; fixtures.knownGaps now records the one deliberate block (a walled customized copy). automated.ref drops the deleted refusal pin and gains the generation-1 steps' pins and the guard's", "ref": "#24681" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "acceptance[5], steps[5] and fixtures.requires[2] added. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4, the upgrade-ceremony pass at 307ce4c4): the item's fixture was only a v17-shaped database (npm @objectstack/cli@17.7.0), so the 18.0 pre-release path the ceremony opened was in no clause. The new clause's oracle is #24838's measured upgrade matrix (real 18.0.0-next.5 and next.3 databases refused, the ceremony applied, each booted with its identity mail, global audit, mail jobs and declared webhook carried over), and each leg is read at its own layer: the boot output, a row read, the door's answer. The fixture is a database the pre-release's own os dev created, plus HTTP writes, the source QA run #24838 found reliable; without one the clause is blocked(fixture). This item's title already names an 18.0 pre-release database, so the clause lives here, and platform-core.v18-boot-refusals points at it. Read at base feb09db14a (organization-ownership-apply.ts SIBLING_MOVES and the mail-job rewrite). No other clause changed", + "ref": "#PENDING-24579R16" } ] }, From d84e44cc94fc9701de26347cab667e3a8dc790b0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:16:19 +0000 Subject: [PATCH 02/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20plat?= =?UTF-8?q?form-core.json:=20ceremony=20cross-reference,=20boot-health=20c?= =?UTF-8?q?onsole=20split,=20seed-mode=20prefix,=20procedural=20negatives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/platform-core.json | 72 ++++++++++++++----- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 2965218612..9b6241533e 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -9,7 +9,7 @@ "title": "Showcase boots clean: health + ready 200, no degraded startup banners, console + app metadata served", "since": "v15", "status": "active", - "revision": 7, + "revision": 8, "priority": "P0", "surface": "mixed", "preconditions": [ @@ -44,16 +44,22 @@ "evidence": "the grepped log excerpt" }, { - "clause": "the console shell is served at /_console/ and the seeded app's merged metadata resolves", + "clause": "the seeded app's merged metadata resolves", "oracle": "api", - "verify": "GET /_console/ → 200 text/html; GET /api/v1/meta/app?id=showcase_app returns the merged app/nav metadata (dogfood skill §1 names this exact endpoint)", - "evidence": "response statuses + top-level keys" + "verify": "GET /api/v1/meta/app?id=showcase_app returns the merged app/nav metadata (dogfood skill §1 names this exact endpoint). The console shell is acceptance[5], so a run with no console build scores this clause in full", + "evidence": "the response status + top-level keys" }, { "clause": "the served nav matches the authored app: every grp_data object entry from src/ui/apps/index.ts appears in the meta/app response — nothing silently dropped at merge", "oracle": "api", "verify": "diff the nav object names in the response against examples/app-showcase/src/ui/apps/index.ts", "evidence": "the diff (empty)" + }, + { + "clause": "the console shell is served at /_console/", + "oracle": "api", + "verify": "GET /_console/ → 200 text/html (steps[4]). It needs a built Console: `pnpm objectui:build` at the framework repo root, which writes packages/console/dist (`pnpm build` never does). On an unbuilt tree the boot still reaches health, and /_console/ answers 503 text/plain 'The ObjectStack Console is not built, so this server has no Console to serve at /_console/.' with the build remedy (packages/cli/src/utils/console.ts#createConsoleNotBuiltPlugin): record this clause blocked(environment), never a FAIL", + "evidence": "the /_console/ status and content type" } ], "negative": [ @@ -96,6 +102,12 @@ "date": "2026-10-07", "change": "acceptance[2].verify quoted the per-flow banner line `⚠ flow '' declares a '' trigger but is NOT bound — disabled by deployment policy …`. The banner now prints one line per (trigger type, reason) class with the flows listed, and Boot diagnostics no longer repeats the automation plugin's per-flow line, so the quoted shape moved with it. The clause is unchanged", "ref": "#22073" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "acceptance[3] narrowed to its server half, with the console half appended as acceptance[5]. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4 at 307ce4c4): acceptance[3] bundled GET /_console/ with the app-metadata read, so a run with no console build (QA runs #24838 and #24786: /_console/ answered 503) could never pass it, although the metadata half held. The two halves now score apart, and acceptance[5] says what an unbuilt tree answers and that it is blocked(environment) (console.ts createConsoleNotBuiltPlugin, read at base feb09db14a). No other clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1843,7 +1855,7 @@ "title": "Marketplace install-local: the wiring matrix mounts the right arms per OS_CLOUD_URL, the air-gapped inline install works with NO control plane, reseed/purge touch exactly the sample rows, every mutating door is manage_metadata-gated both sides, and the listing projects installedBy/storageDir per principal", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1925,8 +1937,7 @@ "runtime-config advertising marketplace:true on a boot whose browse namespace 404s (or installLocal:true where the route is absent) is the #8356/#8388 declared-vs-derived gap re-opening", "purge deleting a row the manifest's seed datasets do not declare is a data-loss FAIL — 'user-created records are never touched' is the handler's own contract", "a 2xx for the plain member or anonymous on ANY mutating door is a FAIL of the #8976 gate — UI absence is not the gate, the server refusal is", - "purged sample rows reappearing after a restart is a FAIL of the sampleDataPurged rehydrate guard", - "a browse-leg upstream fetch failure on an egress-blocked runner filed as a product defect is a false finding — the mount is this kernel's contract, the upstream's reachability is not (score blocked(environment))" + "purged sample rows reappearing after a restart is a FAIL of the sampleDataPurged rehydrate guard" ], "traps": ["dispatcher-vs-hono-route", "absence-inference", "destructive-in-place", "wrong-persona", "auth-state-leak"], "source": [ @@ -1972,6 +1983,12 @@ "date": "2026-10-20", "change": "fixtures.requires[1], steps[2], steps[6]–[7] and acceptance[4].verify — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). The listing serves withSampleData, derived per request from the caller's rows (#21775), never sampleDataPurged, which lives in the ledger file (local-manifest-source.ts, marketplace-install-local-plugin.ts handleList at base 307ce4c4b4). The artifact step now says what a runner needs: the existing examples/app-crm/dist/objectstack.json the tree's build writes, rebuilt only when absent. No clause changed", "ref": "#24824" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "negative[5] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a browse-leg upstream failure on an egress-blocked runner is not a product defect; score blocked(environment)) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (on an egress-blocked or air-gapped runner the leg scores blocked(environment), never a product FAIL; what this item scores about browse is the MOUNT), so nothing was added. No acceptance clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -2090,7 +2107,7 @@ "title": "Runtime config boot read: GET /api/v1/runtime/config answers 200 ANONYMOUSLY with cloud URL, capability flags, branding and telemetry posture; the legacy /api/v1/studio/runtime-config alias is byte-identical (same handler); nothing credential-shaped is in the body", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": ["anonymous", "seeded admin (control only)"], @@ -2106,7 +2123,8 @@ "GET /api/v1/studio/runtime-config with the same (absent) auth — diff the two bodies byte for byte (the alias is mounted with the SAME handler instance; ledgered as its own row because it is its own wire path)", "scan the serialised body for credential-shaped content: no session token, no secret handle plaintext, no Authorization material, no sys_secret ciphertext; telemetry.errorReporting.dsn, when present, is a PUBLIC client key by design — record its presence, it is not a finding", "control: repeat the read WITH an admin session and confirm the payload is not silently enriched with anything the anonymous read withheld (the route is unauthenticated by construction — there is no per-principal projection to leak through)", - "repeat both reads on an OS_CLOUD_URL=off boot: still 200 anonymous, flags now reporting the offline truth (the flag/route agreement itself is scored by platform-core.marketplace-install-local-lifecycle clause 1 — here only that the route is served and anonymous on this boot shape too)" + "repeat both reads on an OS_CLOUD_URL=off boot: still 200 anonymous, flags now reporting the offline truth (the flag/route agreement itself is scored by platform-core.marketplace-install-local-lifecycle clause 1 — here only that the route is served and anonymous on this boot shape too)", + "filing, not a probe: flags asserted here that contradict the same boot's live route table belong to platform-core.marketplace-install-local-lifecycle's clause 1; do not double-file, cross-reference" ], "acceptance": [ { @@ -2137,8 +2155,7 @@ "negative": [ "a 401/403 on this route is a boot-breaking regression: the console reads it before any session exists — a client that must authenticate to learn where to authenticate is circular", "the alias drifting from the canonical body (or being dropped) breaks older Studio bundles silently — the ledger keeps them as separate rows precisely so a move is reported", - "session, secret-store, or credential material in this anonymous body is a security finding — capture and cross-file to access-security, do not tick past it", - "flags asserted here that contradict the same boot's live route table belong to the marketplace item's clause 1 — do not double-file, cross-reference" + "session, secret-store, or credential material in this anonymous body is a security finding — capture and cross-file to access-security, do not tick past it" ], "traps": ["dispatcher-vs-hono-route", "absence-inference", "cache-staleness"], "source": [ @@ -2160,6 +2177,12 @@ "date": "2026-10-19", "change": "steps[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.runtime-config-boot-read steps[1]; wave anchor #24579 §3): the branding block carries logoUrl, faviconUrl and brandColor only when they are configured — runtime-config-plugin.ts reads each from the plugin config or its env var and serialises an unset one as undefined, which JSON drops — so their absence on a stock boot is the contract, not a missing key. No acceptance clause changed", "ref": "#24632" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "negative[3] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a filing rule (a flag/route contradiction belongs to the marketplace item's clause 1; do not double-file) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -2703,7 +2726,7 @@ "title": "Seed mode matrix (insert/update/upsert/replace/ignore): each mode does exactly its verb through the ONE SeedLoaderService, no-op replays skip instead of churning, ignore reports skips — and `replace` is DECLARED delete-all-then-insert but IMPLEMENTED as bare insert (no caller clears the table): the posture is recorded, never ticked as scoped deletion", "since": "v15", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -2711,7 +2734,7 @@ "app": "showcase", "requires": [ "a runtime-created WRITABLE package (the metadata-authoring-roundtrip fixture) — `seed` is allowRuntimeCreate, so per-mode scratch seeds are authored as drafts (PUT /api/v1/meta/seed/ with an explicit `mode`) and applied by publish (applySeedBodies materializes published seed bodies through the SAME SeedLoaderService the boot path uses — packages/runtime/src/seed-loader.ts is a re-export shim of it). The seed BODY carries object, mode, records (and externalId when not the default) and NO `name` key — the URL names the seed, and SeedSchema is strict (packages/spec/src/data/seed.zod.ts#SeedSchema), so a body `name` answers 422 unrecognized_keys", - "a scratch target object in the same writable package (do NOT aim mode probes, least of all replace/insert replays, at seeded showcase objects — destructive-in-place)" + "a scratch target object in the same writable package (do NOT aim mode probes, least of all replace/insert replays, at seeded showcase objects — destructive-in-place: it contaminates platform-core.seed-integrity's baselines)" ], "knownGaps": [ "the publish response's seedApplied summary surfaces inserted/updated/errors but not per-row skip detail — the `ignore says so` clause therefore reads the skip from effect (counts + untouched values) plus the loader's summary where surfaced, not from a UI report", @@ -2720,7 +2743,7 @@ ] }, "steps": [ - "author the scratch object (2 fields + a natural-key `name`, and a declared `sharingModel` — a draft without one is accepted, but its publish is refused (publish-drafts answers outcome 'refused', failed[0] code INVALID_METADATA, rule security-owd-unset) and so is an active save (422 INVALID_METADATA, rule security-owd-unset)) into the writable package and publish; note `externalId` defaults to 'name' and `defaultMode` to 'upsert' (SeedLoaderRequestSchema)", + "author the scratch object (2 fields + a natural-key `name`, and a declared `sharingModel` — a draft without one is accepted, but its publish is refused (publish-drafts answers outcome 'refused', failed[0] code INVALID_METADATA, rule security-owd-unset) and so is an active save (422 INVALID_METADATA, rule security-owd-unset)) into the writable package and publish, named with that package's namespace prefix: NS_SHORT, where NS is the package manifest's namespace, which a package created without one derives from its id's last segment (com.objectstack.qa.seedmodes gives seedmodes; packages/spec/src/kernel/namespace-prefix.ts#deriveNamespaceFromPackageId). An unprefixed draft is accepted, and its publish is refused 422 NAMESPACE_PREFIX with the spelling to use (packages/spec/src/kernel/namespace-prefix.ts#validateObjectNamespacePrefix, ADR-0129 D2); note `externalId` defaults to 'name' and `defaultMode` to 'upsert' (SeedLoaderRequestSchema)", "UPSERT: publish a seed draft (mode upsert) with rows A,B → both insert; edit the draft changing B's value and adding C; re-publish → A skipped (no-op replay — only seed-declared fields compared, so runtime edits to OTHER columns never block the skip), B updated in place (same id), C inserted; verify by id stability + values", "UPDATE: seed draft (mode update) touching A (changed value) and a nonexistent D → A updates, D is SKIPPED (update never creates); row count unchanged", "IGNORE: seed draft (mode ignore) with A (changed value) and new E → A untouched (still the update-mode value; existing rows are never rewritten), E inserted; the skip is visible as A's unchanged value + count", @@ -2769,9 +2792,7 @@ ], "negative": [ "an upsert replay that bumps updated_at on identical rows is the churn isNoOpReplay exists to prevent (phantom edits in history, state-machine re-validation) — FAIL the skip clause", - "update creating a row, or ignore rewriting one, is a mode doing another mode's verb — FAIL", - "ticking the replace clause as 'deletion correctly scoped' is a FALSE PASS twice over: nothing is deleted at head, and the clause exists to record exactly that", - "aiming replace/insert replays at seeded showcase objects contaminates platform-core.seed-integrity's baselines — destructive-in-place; scratch objects only" + "update creating a row, or ignore rewriting one, is a mode doing another mode's verb — FAIL" ], "variants": [ "mode insert — no matching, always writes (duplicates on replay unless the object's own unique constraint refuses)", @@ -2813,6 +2834,12 @@ "date": "2026-10-20", "change": "knownGaps[2] is new — QA run #24779's observation on acceptance[3] (wave anchor #24579 round 12): after insert mode duplicated a natural key, upsert bound the newer duplicate and left the original unconverged. Read at base 307ce4c4b4 in seed-loader.ts loadExistingRecords, which keys an unordered find by externalId (last row wins). No clause changed", "ref": "#24824" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] names the namespace prefix; negative[2] and negative[3] removed. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The scratch object needs the package namespace prefix: QA run #24841's unprefixed qa_seedprobe draft saved 200, and its publish answered 422 NAMESPACE_PREFIX. Read at base feb09db14a: the publish doors judge objectNamespacePrefixViolation (protocol.ts, ADR-0129 D2), and a runtime-created package with no namespace takes deriveNamespaceFromPackageId. (2) negative[2] and negative[3] stated a recording rule and a fixture rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[2]'s text already stands, unchanged in substance, in steps[5] and acceptance[4] (do not tick 'replace scoped to its own object'; there is no deletion to scope). negative[3]'s stands in fixtures.requires[1], which now carries its reason as well (it contaminates platform-core.seed-integrity's baselines). No acceptance clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -3077,7 +3104,7 @@ "title": "A v18 server refuses to boot rather than serve wrongly: against a 17.x-shaped database (a physical organization_id on a non-tenant table, no verified ADR-0131 ceremony marker) it stops before listening with ORGANIZATION_OWNERSHIP_MIGRATION_REQUIRED, naming the tables and the ownership ceremony's plan and apply, and with security on but no identity objects it stops with AuthzIdentityObjectsMissingError", "since": "v18", "status": "active", - "revision": 5, + "revision": 6, "priority": "P0", "surface": "cli", "personas": [ @@ -3088,6 +3115,9 @@ "requires": [ "a SCRATCH copy of examples/app-showcase (or a scaffolded blank app) for every planted-metadata step — never edit the shared fixtures; a file DB under /tmp/RUN/ for every boot", "a v17-shaped SQLite file: a DB created by a 17.x showcase boot (npm `@objectstack/cli@17.7.0`) — or the guard pin's fixture shape planted into a scratch DB — copied, never the original" + ], + "knownGaps": [ + "an 18.0 pre-release database (18.0.0-next.*) is refused by the same guard, with the same absent-marker refusal (or, once a generation-0 ceremony certified it, the shape-generation refusal). Its path through refusal, ceremony and served boot, with its data carried over, is cli.organization-ownership-ceremony acceptance[5], scored there on that item's own fixture (its fixtures.requires[2]); this item's clauses judge the v17-shaped copy only" ] }, "steps": [ @@ -3167,6 +3197,12 @@ "date": "2026-10-20", "change": "title and acceptance[1] (clause, verify) re-worded for the open ownership ceremony (#24581). `os migrate organization-ownership` plans and applies again, and the boot guard decides on the ownership marker (a birth marker a fresh datastore writes, or the ceremony's verified one): a v17 database has none, so the refusal opens \"This database carries no organization-ownership marker\", names the tables still carrying organization_id, and names the ceremony's plan and apply (`--out PLAN`, then `--apply --plan-file PLAN --backup-taken`). Revision 4 asserted the 18.0 sentence and the absence of the command, so a run against the new text would have recorded FAIL on a correct product. It now asserts the marker sentence, the tables and the two steps, and that the refusal says nothing of staying on 17.x. The ⛔ on the code literal is kept. acceptance[0], acceptance[2], acceptance[3] and both steps are unchanged", "ref": "#24681" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "fixtures.knownGaps[0] added, a cross-reference. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4 at 307ce4c4): neither ceremony item had a clause for a real 18.0 pre-release database. The clause is cli.organization-ownership-ceremony acceptance[5], because that item's title names an 18.0 pre-release database and this item's names a 17.x-shaped one; this item points at it. Its scope does not cover fresh installs, so the order #24827 fixed (os migrate apply before the first os serve) is not added here. Read at base feb09db14a. No clause changed", + "ref": "#PENDING-24579R16" } ] }, From 8f15bbf6a9662dcdfab6a65fe79d5d9c103ba37c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:16:54 +0000 Subject: [PATCH 03/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20iden?= =?UTF-8?q?tity-auth.json:=20password-reset=20expiry=20leg=20without=20a?= =?UTF-8?q?=20clock=20wait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/identity-auth.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 36a5bf40d0..62e63978a1 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -988,7 +988,7 @@ "title": "Forgot-password: request → token captured at the dev mail transport → reset; old password refused, expired/reused token refused loudly", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": ["anonymous visitor (holds no session)", "the account owner (before and after reset)"], @@ -1008,7 +1008,7 @@ "complete the reset: POST /api/v1/auth/reset-password with the token + a new password; capture the response", "sign in with the NEW password (2xx) and confirm the OLD password is now refused", "reuse the SAME token a second time (POST /api/v1/auth/reset-password again) and capture the loud refusal — a consumed token must not reset again", - "request a fresh reset, let the token expire (or use a tampered/garbage token) and confirm the reset is refused with a named error, never a silent success", + "request a fresh reset and expire its token without a clock wait. Read the token from the new sys_identity_email row (steps[1]); then, in the run's own file DB, move that token's verification row into the past with node:sqlite (no sqlite3 CLI in this container): `UPDATE sys_verification SET expires_at = 'PAST' WHERE identifier = 'reset-password:TOKEN'`, where PAST is one hour before now in the form Date#toISOString writes, and the statement reports one changed row. better-auth keys a reset row reset-password:TOKEN, stored plain, and expires_at is a datetime column stored as canonical UTC text (packages/plugins/plugin-auth/src/auth-schema-config.ts#AUTH_VERIFICATION_CONFIG). Then POST /api/v1/auth/reset-password with that token and confirm it is refused with a named error (better-auth's consume treats a row past expiresAt as invalid: 400 INVALID_TOKEN, as QA runs #24838 and #24841 measured), that the current password still signs in, and that the attempted one does not. Separately, a tampered/garbage token: confirm the same refusal, never a silent success", "negative: request-password-reset for an unknown email should not leak whether the account exists (anti-enumeration) — record the response shape", "confirm password-reuse prevention if configured: resetting to the OLD password is refused where password_history_count > 0 (previous_password_hashes ring, ADR-0069 D1)" ], @@ -1070,6 +1070,12 @@ "date": "2026-10-20", "change": "fixtures.requires[0], knownGaps[0], steps[1] and acceptance[0] (verify, evidence) read the reset mail from sys_identity_email — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12): sys_email held 0 rows and the mail was in sys_identity_email. Governing text ADR-0131 D7 (identity mail is deployment-level); read at base 307ce4c4b4 in auth-manager.ts (the auth.password_reset send passes identityMail: true) and email-service.ts (emailLogObjectOf). Not loosened: the clause still requires a real mail carrying a working tokened link", "ref": "#24824" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[5]'s expiry leg runs without a clock wait. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4; QA run #24838 ran the same leg): the step said 'let the token expire', and a reset token lives an hour (resetPasswordTokenExpiresIn || 3600, better-auth 1.7.3 routes/password.mjs). It now writes the token's sys_verification.expires_at into the past, the simulation both records measured (400 INVALID_TOKEN, the password unchanged). Read at base feb09db14a: AUTH_VERIFICATION_CONFIG maps expiresAt to expires_at; no verification.storeIdentifier is set, so the identifier is stored plain; consumeVerificationValue treats a row past expiresAt as invalid. The record's other finding (requires and steps still naming sys_email) no longer holds: revision 4 (#24824) moved them to sys_identity_email, and the sys_email mentions left say 'never sys_email'. No clause changed", + "ref": "#PENDING-24579R16" } ] }, From adf07fcc4f74d1dac863ad8954adfbc69bafc3c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:17:30 +0000 Subject: [PATCH 04/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20auto?= =?UTF-8?q?mation.json:=20procedural=20negatives,=20subflow=20refusal=20ar?= =?UTF-8?q?ms=20read=20per=20RUNNER=20rule=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/automation.json | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index ed9932c410..6f4b5c0339 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1550,7 +1550,7 @@ "title": "Disabling a packaged flow writes a durable sys_metadata_activation row that survives a cold restart with the trigger left unbound; re-enable UPDATES the row and restores firing", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P0", "surface": "api", "personas": [ @@ -1612,7 +1612,7 @@ "a toggle answering 2xx with NO sys_metadata_activation row written is a FAIL on this boot — the engine has a legitimate ledger-less degraded mode, but it WARNS 'IN PROCESS ONLY … will NOT survive a restart' (engine.ts) and the showcase composition attaches the ledger, so silence plus no row means the durable write was skipped where it was promised", "a restart that re-arms the flow (enabled or bound true, or a run row from the post-restart create) is a FAIL against the whole point of ADR-0126 §7.2 — it is the retired flowEnabled map's behaviour reappearing", "a re-enable that DELETES the row is a FAIL of the row contract even though firing resumes — absence-means-active makes deletion look equivalent right up until something needs the recorded choice", - "a run that scored the ledger clause from the FLOW_DISABLED code alone (without the message) has verified nothing this item asserts — the code is shared with the status dimension by design" + "a ledger-disabled flow's trigger answered 409 FLOW_DISABLED with the status dimension's bare sentence (\"Flow 'showcase_urgent_task_alert' is disabled\", with no 'packaged-metadata activation ledger (sys_metadata_activation' phrasing) is a FAIL: the refusal then reports the administrator's ledger choice as an authoring status, and the code alone cannot tell the two apart, because ADR-0126 §7.2 shares it between them by design" ], "traps": [ "first-boot-cold-start", @@ -1649,6 +1649,12 @@ "date": "2026-10-16", "change": "clause 3's log corroboration reads an INFO line that a default os dev boot never prints (#23878 automation slice finding 5): the CLI's default log level is warn. Step 6 now boots the second server with --log-level info, and the verify says the line is INFO. Re-measured on main c91af8ed6: after toggling showcase_urgent_task_alert off and cold-restarting at OS_LOG_LEVEL=info over the same file DB, the log carries INFO '[Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'.' and /_status reports enabled:false, bound:false; re-enable answers 200", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "negative[3] rewritten as the product outcome it guarded. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a run scoring the ledger clause from the code alone verified nothing) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). The outcome it guards is the refusal naming the ledger, so the FAIL is now a ledger-disabled flow answered with the status dimension's bare 'Flow NAME is disabled'. Read at base feb09db14a in engine.ts describeDisabledFlow: the ledger arm adds the 'packaged-metadata activation ledger (sys_metadata_activation' sentence, and the status arm returns the bare one. acceptance[1].verify keeps its ⛔ on the code alone. No acceptance clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1657,7 +1663,7 @@ "title": "Disabling a packaged flow that packaged callers invoke as a subflow is refused 409 DELETE_RESTRICTED naming every caller; the refused attempt writes no ledger row; enabling the subflow itself is never guarded", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1669,7 +1675,7 @@ "the three stock packaged caller→child pairs (examples/app-showcase/src/automation/flows/index.ts): showcase_task_done_notify_owner → showcase_notify_owner via a subflow node · showcase_project_closure → showcase_closure_signoff via subflow · showcase_release_signoff → showcase_one_task_signoff via a MAP node — the map pair is load-bearing, because a scan that read only `subflow` nodes would pass the first two and miss it" ], "knownGaps": [ - "two engine-pinned arms have no stock HTTP specimen and are scored from the unit pins, not driven: a NON-packaged caller must not guard (a tenant's own flow cannot hold a packaged one hostage) and a flow calling ITSELF must not guard its own disable — both pinned engine-level in packages/services/service-automation/src/flow-activation-ledger.test.ts; driving them over HTTP needs a scratch-package tenant caller nothing stock provides", + "two arms have no stock HTTP specimen: a NON-packaged caller must not guard (a tenant's own flow cannot hold a packaged one hostage), and a flow calling ITSELF must not guard its own disable. Both are pinned engine-level in packages/services/service-automation/src/flow-activation-ledger.test.ts, over a fake store: evidence of the engine layer only, never a pass for the toggle door's answer (RUNNER rule 6, a pin scores only the layer it drives). Driving them over HTTP needs a scratch-package tenant caller nothing stock provides, so a run without one records their door legs blocked(fixture), with the pin beside them", "there is NO HTTP-level pin of this refusal anywhere (the pins above are engine unit tests against a fake store) — which is exactly why the wire shape (does the thrown {code:'DELETE_RESTRICTED', status:409} survive the dispatcher's error mapping intact?) is this item's job and not a formality" ] }, @@ -1680,7 +1686,8 @@ "after each refusal: GET /api/v1/data/sys_metadata_activation and assert NO row exists for the refused flow; then prove it is still ARMED — trigger a run through its caller (for the notify pair: create a showcase_task with its required project and status 'in_review', then PATCH it to status='done' — task_status_flow allows done only from in_review) and confirm the child's subflow step executed", "remedy sequence (the refusal's own instruction): POST /api/v1/automation/showcase_task_done_notify_owner/toggle {\"enabled\": false} (the CALLER — it has no packaged callers of its own, so this lands), then RETRY POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": false} and record what it answers", "enable arm, child FIRST: POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": true} while its caller is still off (confirm it is never refused), then toggle showcase_task_done_notify_owner back ON, then repeat the child's enable with the caller armed (a no-op enable). ⚠️ Enabling the CALLER while its packaged subflow is still disabled is refused by design since 679f95e (#20711) — 409 RESOURCE_CONFLICT 'cannot be enabled while 1 packaged subflow it calls is disabled … Enable it first' — so the caller-first order is not a fail of this item", - "teardown: re-enable anything left disabled (or discard the isolated DB)" + "teardown: re-enable anything left disabled (or discard the isolated DB)", + "scope, not a probe: the UI half (the caller names rendered verbatim in a role=alert on the Setup packaged-automation page) is deliberately NOT scored here; it is automation.setup-packaged-automation-board's refusal clause" ], "acceptance": [ { @@ -1716,8 +1723,7 @@ ], "negative": [ "a 2xx on step 2 or 3 (the disable landing despite packaged callers) is a FAIL of §7.3 itself — the vendor caller would then break mid-run at its subflow node with the late 'subflow … failed: Flow … is disabled' this guard exists to prevent", - "a refusal naming only the FIRST caller when several exist is a FAIL — the message must name every packaged caller (pinned engine-level, flow-activation-ledger.test.ts); with stock fixtures each child has one caller, so this arm rides the pin unless a scratch second caller is authored", - "the UI half — the caller names rendered verbatim in a role=alert on the Setup packaged-automation page — is deliberately NOT scored here; it is automation.setup-packaged-automation-board's refusal clause" + "a refusal naming only the FIRST caller when several exist is a FAIL — the message must name every packaged caller (pinned engine-level, flow-activation-ledger.test.ts); with stock fixtures each child has one caller, so the door leg needs a scratch second caller and is recorded blocked(fixture) without one; the engine pin is evidence beside that verdict, never its pass (RUNNER rule 6)" ], "traps": [ "dispatcher-vs-hono-route", @@ -1742,7 +1748,8 @@ { "revision": 2, "date": "2026-10-02", "change": "retired clause 5's expected-fail note and re-ordered step 6. The remedy sequence now holds (36d043b #20724, 0d9349f #20759): on 17.6.0 the caller's disable then the child's retry both answer 200 and the ledger reads active=false. Step 6's caller-first enable is refused by design since 679f95e (#20711, 409 RESOURCE_CONFLICT naming the disabled subflow), so the step enables the child first and clause 4 is scoped to a child's enable. Measured by the 17.6.0 release-verification run", "ref": "#21330" }, { "revision": 3, "date": "2026-10-16", "change": "the 'still armed' probe patched a showcase_task to done without saying how to get there (#23878 automation slice finding 2, re-measured on main c91af8ed6): showcase_task requires `project` (a create without it answers 400 VALIDATION_FAILED 'Project is required'), and task_status_flow allows status→done only from in_review (a PATCH from the default backlog answers 400 invalid_transition). A create with project and status 'in_review' answers 201 (create is not transition-checked) and the PATCH to done answers 200, firing the record-change flows. Step 4 now creates the task with project and status 'in_review' first; on that path showcase_task_done_notify_owner ran and completed", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "acceptance[0].clause quotes the remedy the engine writes — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-subflow-disable-refusal acceptance[0]; wave anchor #24579 §3): the remedy names the calling flow, so a substring match on the generic quote fails although the meaning is identical. Re-derived at base 9a7838edf5 (packages/services/service-automation/src/engine.ts, the subflow-caller refusal: 'disable the calling flow 'NAME' first' … ' — or leave this one armed.')", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 707, 795, 816, 1322 and 1339 of the showcase flows index.ts in the comma-range spelling (authored in e0abc386a4 as the list 678 to 1339). At that commit those lines were the names and child flowName values of the three stock caller flows; at this base all of them have moved. source[4] now anchors the three callers, index.ts#TaskDoneNotifyOwnerFlow, #ProjectClosureFlow and #ReleaseSignoffFlow, and names each child in prose. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" } + { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 707, 795, 816, 1322 and 1339 of the showcase flows index.ts in the comma-range spelling (authored in e0abc386a4 as the list 678 to 1339). At that commit those lines were the names and child flowName values of the three stock caller flows; at this base all of them have moved. source[4] now anchors the three callers, index.ts#TaskDoneNotifyOwnerFlow, #ProjectClosureFlow and #ReleaseSignoffFlow, and names each child in prose. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, + { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#PENDING-24579R16" } ] }, { From de6e0395ccf8c09dd41b0006852f6e55264fdfab Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:18:05 +0000 Subject: [PATCH 05/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20acce?= =?UTF-8?q?ss-security.json:=20procedural=20negatives?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index fc32be39bd..4c8c3573d8 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": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1465,8 +1465,7 @@ ], "negative": [ "a Studio panel that greys the save while a direct meta PUT widens is a FAIL — the server gate is the authority (ADR-0124 D1 both-sides)", - "a stock-deploy packaged-object widening PUT that answers 2xx (or leaves the object widened) is a FAIL", - "do NOT tick external-principal read/write enforcement: an external principal reaches explicitly shared rows only (ADR-0095) — this item is the save/authoring gate only" + "a stock-deploy packaged-object widening PUT that answers 2xx (or leaves the object widened) is a FAIL" ], "variants": [ "the retired externalSharingModel (the object schema's tombstone, all object writes, draft included)", @@ -1494,7 +1493,8 @@ { "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": "#24850" } + { "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" }, + { "revision": 7, "date": "2026-10-20", "change": "negative[2] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule ('do NOT tick external-principal read/write enforcement … this item is the save/authoring gate only') with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (this item tests the save gate, NOT external-principal enforcement; do not tick an external-principal read/write result; an external principal reaches explicitly shared rows only, ADR-0095), so nothing was added. No acceptance clause changed", "ref": "#PENDING-24579R16" } ] }, { @@ -2677,7 +2677,7 @@ "title": "Write-door parity on a packaged flow: PUT/DELETE /automation/:name must refuse the same packaged artifact that PUT /meta/flow/:name refuses (ADR-0126 §2 locked base)", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -2699,7 +2699,8 @@ "probe leg 1: PUT /api/v1/automation/showcase_urgent_task_alert with the same trivially modified definition — capture status and, if 2xx, GET the flow back to prove the live registration mutated", "probe leg 2: DELETE /api/v1/automation/showcase_urgent_task_alert — capture status and, if 2xx, confirm GET /api/v1/automation/showcase_urgent_task_alert now 404s (the shipped flow is gone from the live engine)", "RESTORE, only if a probe mutated anything: diff GET /api/v1/automation/showcase_urgent_task_alert against the stored capture; if it differs (or 404s), restore with a cold restart over the same database (the boot flow pull re-registers the packaged body — a restore PUT is itself refused 403 once the door is locked) and diff again — byte-identical", - "verify the flow still fires: trigger its record-change mutation once and confirm a run appears (the restore must revive the trigger binding, not just the definition read)" + "verify the flow still fires: trigger its record-change mutation once and confirm a run appears (the restore must revive the trigger binding, not just the definition read)", + "scope, not a probe: the toggle door is NOT this item. POST /:name/toggle is the sanctioned activation switch (access-security.activation-write-operator-gate); this item is about DEFINITION writes and deletion of the locked base (ADR-0126 §2/§6: sole definition ledger, never silent override), so the toggle is neither driven nor scored here" ], "acceptance": [ { @@ -2729,8 +2730,7 @@ ], "negative": [ "a 403 on the /automation probes coming from a MISSING manage_metadata capability is the wrong tier and proves nothing about the lock — this item's persona holds the capability precisely so any refusal observed is the package lock; quote the refusal message to prove which gate answered", - "do NOT read clause 1's /meta refusal as covering the /automation door — two doors onto one artifact are the item; scoring one door twice is the mistake", - "the toggle door is NOT this item: POST /:name/toggle is the sanctioned activation switch (access-security.activation-write-operator-gate); this item is about DEFINITION writes and deletion of the locked base (ADR-0126 §2/§6 — sole definition ledger, never silent override)" + "a 2xx from PUT or DELETE /api/v1/automation/showcase_urgent_task_alert on the artifact the /meta door refused (acceptance[0]) is a FAIL of door parity, whatever the /meta trace shows: each /automation door is scored from its own trace (steps[2] and steps[3]), because two doors onto one artifact are the item" ], "traps": [ "destructive-in-place", @@ -2751,7 +2751,8 @@ "change": "new — #12438 measured that PUT/DELETE /automation/:name re-register/unregister a PACKAGED flow on manage_metadata alone while PUT /meta/flow/:name refuses the same artifact: the ADR-0126 §2 lock is unimplemented at the /automation door. Authored as a door-parity item with the parity promise as the acceptance and the present-day 200 recorded as an expected fail (tracked centrally in FOLLOW-UPS), plus a mandatory restore step because the probe mutates a live engine registration", "ref": "#12438" }, - { "revision": 2, "date": "2026-10-02", "change": "retired the expected-fail framing: 4b45afae (#20817) locked the /automation write doors against the packaged base, and the 17.6.0 release-verification run measured PUT and DELETE /automation/showcase_urgent_task_alert answering 403 NOT_OVERRIDABLE with the /meta door's lock message, the flow unchanged and still firing. Title, knownGaps and clauses 2-3 now state the parity as holding; the restore step is conditional (a restore PUT is itself refused), and the knownGap names the permanent pin packaged-flow-write-door-parity.dogfood.test.ts", "ref": "#21330" } + { "revision": 2, "date": "2026-10-02", "change": "retired the expected-fail framing: 4b45afae (#20817) locked the /automation write doors against the packaged base, and the 17.6.0 release-verification run measured PUT and DELETE /automation/showcase_urgent_task_alert answering 403 NOT_OVERRIDABLE with the /meta door's lock message, the flow unchanged and still firing. Title, knownGaps and clauses 2-3 now state the parity as holding; the restore step is conditional (a restore PUT is itself refused), and the knownGap names the permanent pin packaged-flow-write-door-parity.dogfood.test.ts", "ref": "#21330" }, + { "revision": 3, "date": "2026-10-20", "change": "negative[1] rewritten as the product outcome it guarded, and negative[2] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): both stated a runner rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[1]'s 'do not read the /meta refusal as covering the /automation door' guarded one: an /automation write door answering 2xx on the artifact /meta refused, which is now the FAIL. negative[2] (the toggle door is not this item) guards none, so its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#PENDING-24579R16" } ] }, { From 9c989e6a9a19b193177f2d41e8a9cc2759d2bfb7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:18:41 +0000 Subject: [PATCH 06/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20sear?= =?UTF-8?q?ch.json:=20procedural=20negative?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/search.json | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/qa/platform-checklist/areas/search.json b/docs/qa/platform-checklist/areas/search.json index 4c7b09102b..8e9bb8b85e 100644 --- a/docs/qa/platform-checklist/areas/search.json +++ b/docs/qa/platform-checklist/areas/search.json @@ -211,7 +211,7 @@ "title": "Search honors RLS both ways: a restricted member gets no hits — and no total leakage — from rows they cannot see; the entitled persona finds the same rows", "since": "v15", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": ["seeded admin (sees all invoices)", "signed-up member bound to the contributor position via the qa-contributor-bound-member recipe (invoice_own_rows RLS: owner == current_user.email) — ⛔ NOT one of the seeded demo personas: Mei holds no position (PERMISSION_DENIED, object gate) and Ada holds auditor with viewAllRecords (all 12). See the recipe."], @@ -235,7 +235,8 @@ "as admin, run the identical search — INV-1003 must return (the entitled side of the same gate)", "as the member, create their own invoice (POST /api/v1/data/showcase_invoice, name 'INV-QA-RLS') and search for it — their own rows must remain findable", "as the member, run a broad paged search { search: 'INV', limit: 2 } and record records/total/hasMore across pages", - "compare the member's paged totals against their full visible row set" + "compare the member's paged totals against their full visible row set", + "persona discipline, not a probe: running the deny side as admin proves nothing (wrong-persona trap); the guard check must run as the non-privileged member" ], "acceptance": [ { @@ -264,8 +265,7 @@ } ], "negative": [ - "any hit (or any total inflation) for the restricted member from an invisible row is an RLS bypass — P1 FAIL, reproduce twice and file", - "running the deny side as admin proves nothing (wrong-persona trap): the guard check must run as the non-privileged member" + "any hit (or any total inflation) for the restricted member from an invisible row is an RLS bypass — P1 FAIL, reproduce twice and file" ], "traps": ["wrong-persona", "seed-data-thin"], "source": [ @@ -301,6 +301,12 @@ "date": "2026-10-15", "change": "clause 2's mechanism re-authored to the behaviour on main, and its source anchor re-pointed. #22790 (PR #22857, 55382dc02) deleted protocol.ts's `countable` exception and its page-local estimate: a paged search now runs engine.count with the query's search and searchFields, through the same expander find runs and the same read middleware, so `total` is the counted number of matches the caller can read. The clause said the protocol never counts a searched list, and revision 3 bounded the reconciliation to skip <= the visible count because the estimate had no page to count past it; both were true of the deleted arm and are false now, so the verify now asks for one total on every page that equals the enumerable matches, at any skip. The source anchor on protocol.ts named countable and no longer resolved (check:platform-checklist ABSENT SYMBOL); it now names findData, which holds the counting arm, plus the door pin that measures a member's searched total. The security property is unchanged: no response may reveal a row the caller cannot read. Clauses 1, 3 and 4, the steps, the personas and the fixtures are unchanged", "ref": "#23593" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "negative[1] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a runner rule (the deny side run as admin proves nothing) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). The outcome it guards, a hit or a total for the member from a row it cannot see, is already negative[0]. Its text moved, unchanged in substance, to steps[6], and traps keeps wrong-persona. No acceptance clause changed", + "ref": "#PENDING-24579R16" } ] }, From 822eb138e591ab2332f86976334d293aafb8da43 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:19:18 +0000 Subject: [PATCH 07/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20api-?= =?UTF-8?q?backend.json:=20MCP=20clause=20no=20longer=20falls=20back=20to?= =?UTF-8?q?=20the=20unit=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/api-backend.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 701426e897..c792849c80 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1565,7 +1565,7 @@ "title": "A disabled packaged action is refused 409 ACTION_DISABLED at BOTH dispatch doors — after the capability gate (a 403 is never an oracle for the switch), before the param contract and the record load, durable across restart", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P0", "surface": "api", "personas": [ @@ -1581,7 +1581,7 @@ "showcase_portfolio_snapshot (ui/actions/index.ts — object-less/global, and the ONLY stock action with ai.exposed: true): the live MCP-door probe; its activation flip spells the object segment `global` (/actions/_activation/global/showcase_portfolio_snapshot)" ], "knownGaps": [ - "The MCP door cannot be driven live with showcase_mark_done: the bridge's AI-exposure gate (actionAiExposureError, fail-closed on ai.exposed !== true) sits AHEAD of the activation consult, so a non-exposed action answers the exposure refusal regardless of its switch. showcase_portfolio_snapshot is the only stock ai.exposed action. The MCP transport IS drivable live on a stock boot: the boot banner prints `MCP: http://localhost:/api/v1/mcp`, and QA run #21056 drove POST /api/v1/mcp tools/call run_action against showcase_portfolio_snapshot (disabled → isError carrying code ACTION_DISABLED, status 409; re-enabled → ok). Score the MCP clause live; fall back to the unit pin (action-activation-dispatch.test.ts 'door 2') only where the endpoint is not mounted, and record which oracle the verdict rests on.", + "The MCP door cannot be driven live with showcase_mark_done: the bridge's AI-exposure gate (actionAiExposureError, fail-closed on ai.exposed !== true) sits AHEAD of the activation consult, so a non-exposed action answers the exposure refusal regardless of its switch. showcase_portfolio_snapshot is the only stock ai.exposed action. The MCP transport IS drivable live on a stock boot: the boot banner prints `MCP: http://localhost:/api/v1/mcp`, and QA run #21056 drove POST /api/v1/mcp tools/call run_action against showcase_portfolio_snapshot (disabled → isError carrying code ACTION_DISABLED, status 409; re-enabled → ok). Score the MCP clause live. Where the endpoint is not mounted, record acceptance[4] blocked(environment), with the unit pin cited beside it: action-activation-dispatch.test.ts 'door 2' calls invokeBusinessAction in-process over a stubbed engine and metadata service, with no MCP transport and no HTTP, so it is evidence of that layer only, never the clause's pass (RUNNER rule 6, a pin scores only the layer it drives).", "The record-load half of the ordering proof has NO contrasting refusal: the REST door's record load is best-effort (try/catch in domains/actions.ts — a missing record passes an empty record for new-record actions), so a nonexistent recordId never 404s on its own, enabled or disabled. The record leg is proven by code order plus the absence of any read/side effect, never by expecting a 404 from the enabled control." ] }, @@ -1694,6 +1694,12 @@ "date": "2026-10-19", "change": "steps[5] names the MCP argument — checklist-accuracy finding of the 18.0 QA sweep (QA run #24556, api-backend.packaged-action-disabled-dispatch step 6; wave anchor #24579 §3). Re-derived at base 9a7838edf5 (packages/mcp/src/mcp-http-tools.ts run_action input: actionName, objectName, recordId). No clause changed", "ref": "#24649" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "fixtures.knownGaps[0] no longer falls back to the unit pin for the MCP clause. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, the MCP clause's unit fallback), read under RUNNER rule 6. acceptance[4]'s oracle is the MCP tool's answer. The 'door 2' case of packages/runtime/src/domains/action-activation-dispatch.test.ts calls invokeBusinessAction in-process over a stubbed engine and metadata service (read at base feb09db14a), so where the endpoint is not mounted the clause is blocked(environment), with the pin beside it. No clause changed", + "ref": "#PENDING-24579R16" } ] }, From a221be71867ee7cdd9827c2b745b7df9983cf16a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:19:52 +0000 Subject: [PATCH 08/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20inte?= =?UTF-8?q?gration-system.json:=20connector=20wire=20shapes=20and=20dataso?= =?UTF-8?q?urce-admin=20ref=20read=20per=20RUNNER=20rule=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/integration-system.json | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index adbfb8c048..01425f248a 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1190,7 +1190,7 @@ "title": "The /api/v1/datasources admin lifecycle: all 11 routes behind ONE manage_platform_settings floor (drivers catalog included), runtime create with provenance+health, draft test probe, credential migration into the store, secret never echoes (hasSecret only), bad drafts 400 DATASOURCE_ADMIN_ERROR, unwired federation degrades 503 naming external-datasource", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1291,7 +1291,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/services/service-datasource/src/__tests__/admin-routes.test.ts + packages/services/service-datasource/src/__tests__/envelope.conformance.test.ts — pins route behavior + envelope; the LIVE-mount half is not pinned, drive os dev for it" + "ref": "packages/services/service-datasource/src/__tests__/admin-routes.test.ts + packages/services/service-datasource/src/__tests__/envelope.conformance.test.ts. Both mount the real route registrar on the real HonoHttpServer adapter over STUBBED datasource-admin and external-datasource services and a fake auth service: the route layer, and nothing below it runs. They score whole: acceptance[7] (its oracle is test: admin-routes' #4225 cases, each federation route's 503 naming external-datasource while datasource-admin still serves, and envelope's 'the external-datasource service is not wired'), negative[4] and negative[5] (attributions the route layer owns: the 503's service, and each route's refusal code, admin-routes' #4249 cases on both services). They hold route-layer halves only, evidence beside the live answer and never its pass (RUNNER rule 6): acceptance[0]'s admin half (the static catalog with its configSchema, mounted with no service), acceptance[2]'s inline secret split out of the draft, acceptance[4]'s 201 envelope, acceptance[6]'s service refusal answered 400 DATASOURCE_ADMIN_ERROR in the declared envelope, and acceptance[8]'s 404 RESOURCE_NOT_FOUND. Neither drives the capability floor (acceptance[1], and acceptance[0]'s anonymous 401), the real service, the secret binder or the store (acceptance[3], acceptance[5], and the persistence and health halves): drive os dev for every api clause" }, "source": [ "packages/services/service-datasource/src/admin-routes.ts#requireDatasourceAdmin (the eleven routes; the uniform floor docblock + requireDatasourceAdmin + DATASOURCE_ADMIN_CAPABILITY #9391/#9593; splitSecret keeps the secret out of the persisted draft; resolve()/badRequest() per-service attribution #4225/#4249; getDatasource credential-stripped + hasSecret; POST /test draft probe; POST /:name/migrate-credential)", @@ -1332,6 +1332,12 @@ "date": "2026-10-20", "change": "automated.ref respelled — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: a ref that misses its own pin), round 15 of wave anchor #24579. The second pin was spelled '(+ __tests__/envelope.conformance.test.ts)', a partial path in a parenthesis, so the run's ref parser missed it and ran it separately (24/24). It is now a full repo path after ' + ', as the other multi-pin refs spell it. The ref's scope is unchanged. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "automated.ref narrowed per clause. PR #24891's carried finding (round 15 respelled this ref and left its scope, 'pins route behavior + envelope', unnarrowed), read under RUNNER rule 6 and the theme-B rule. Read at base feb09db14a: both pins mount registerDatasourceAdminRoutes on the real HonoHttpServer adapter over stubbed services and a fake auth service, so they score acceptance[7] (test oracle), negative[4] and negative[5] (attributions the route layer owns) whole, and hold only route-layer halves of acceptance[0], [2], [4], [6] and [8]. The capability floor has its own pin (admin-routes-auth-guard.test.ts), which this ref does not name. No clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -1828,7 +1834,7 @@ "title": "Declarative connector static auth kinds (none/api-key/basic/bearer) resolve credentialRef at materialization and land on the wire in exactly their header/query shape; an unresolvable credentialRef is boot-fatal with the env-var prescription; oauth2 has NO authorable spelling (EE tier)", "since": "v15.1", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "fixtures": { @@ -1839,7 +1845,7 @@ "env vars set for each probe's credentialRef before boot (the open tier resolves credentialRef from environment variables — plugin.ts)" ], "knownGaps": [ - "NO shipped connector declares a secret-bearing auth kind — all three showcase instances are auth:{type:'none'} on purpose (examples/app-showcase/src/system/connectors/index.ts#StatusApiConnector, and StatusOpenApiConnector and DevToolsMcpConnector beside it, each declare auth: { type: 'none' }; StatusApiConnector's own comment says a real upstream would use bearer+credentialRef). The per-kind instances are therefore scratch-authored in a writable package against the echo upstream; without the echo upstream the wire-shape clauses are blocked(fixture) and fall back to the unit pins (rest-connector.test.ts drives applyAuth through a stubbed fetch)" + "NO shipped connector declares a secret-bearing auth kind — all three showcase instances are auth:{type:'none'} on purpose (examples/app-showcase/src/system/connectors/index.ts#StatusApiConnector, and StatusOpenApiConnector and DevToolsMcpConnector beside it, each declare auth: { type: 'none' }; StatusApiConnector's own comment says a real upstream would use bearer+credentialRef). The per-kind instances are therefore scratch-authored in a writable package against the echo upstream; without the echo upstream the wire-shape clauses are blocked(fixture), with the unit pins cited beside them as evidence of their own layer only (rest-connector.test.ts drives applyAuth through a stubbed fetch), never their pass (RUNNER rule 6)" ] }, "variants": [ @@ -1864,7 +1870,7 @@ { "clause": "each static kind lands on the wire in exactly its shape, one verdict per variant, none inferred: bearer → Authorization: Bearer ; basic → Authorization: Basic base64(username:password); api-key header form → X-API-Key (or the authored headerName); api-key query form → the key in the named query param AND no key header; none → no auth artifact", "oracle": "network", - "verify": "the echo upstream's captured headers/query per instance against applyAuth (packages/connectors/connector-rest/src/rest-connector.ts); unit fallback per shape in rest-connector.test.ts", + "verify": "the echo upstream's captured headers/query per instance against applyAuth (packages/connectors/connector-rest/src/rest-connector.ts). rest-connector.test.ts (applyAuth through a stubbed fetch) is evidence of the request the connector builds, never of what an upstream received: without the echo upstream the clause is blocked(fixture) with that pin beside it, never scored pass from it (RUNNER rule 6, a pin scores only the layer it drives)", "evidence": "the five echoed requests, keyed by kind" }, { @@ -1924,6 +1930,12 @@ "date": "2026-10-20", "change": "checklist-accuracy findings of QA run #24615 (18.0 sweep round 2, ba4afcc2; wave anchor #24579 §3). steps[1] offered 'direct engine dispatch', which has no HTTP door; it names the runtime-flow vehicle #24615 used (POST /api/v1/automation, then POST /automation/NAME/trigger). fixtures.knownGaps[0] and source[5] cited the showcase connectors file by comma-separated line numbers, the rot class the line-citation limb exists for; both now anchor the declaring symbol. No clause changed", "ref": "#24717" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "acceptance[0].verify and fixtures.knownGaps[0] no longer fall back to the unit pin for the wire shapes. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, wire-shape clauses), read under RUNNER rule 6. acceptance[0] is a network clause: what the echo upstream received. packages/connectors/connector-rest/src/rest-connector.test.ts drives applyAuth through a stubbed fetch (read at base feb09db14a), so without the echo upstream the clause is blocked(fixture), with the pin beside it. No clause changed", + "ref": "#PENDING-24579R16" } ] }, From 0d028842319412be9c6c3fa5c2f926e152bd3d38 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:20:28 +0000 Subject: [PATCH 09/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20appr?= =?UTF-8?q?ovals.json:=20action-token=20legs=20scored=20at=20their=20own?= =?UTF-8?q?=20layer,=20hosted-door=20pin=20named?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../platform-checklist/areas/approvals.json | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 879dd9f199..5e5d24bce7 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1148,7 +1148,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": 6, + "revision": 7, "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"], @@ -1159,9 +1159,9 @@ "the pinned tests in automated.ref, run with the plugin's own vitest (`pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2 FILE`, never through a bare `--`). They mint with issueActionTokens in-process and hold the raw token in memory, which is the only place a live token exists now: sys_approval_token stores SHA-256 hashes, and since PR #24620 no persisted row (a notification payload, an audit row, a log line) holds a raw token or a URL built from one — issueActionTokens' caller contract (approval-service.ts)" ], "knownGaps": [ - "NO STOCK DOOR MINTS AN ACTION TOKEN since PR #24620 (65a8eeb7d4, 2026-10-19). remind() no longer calls issueActionTokens or attaches a link (no channel ever read payload.actions, so no recipient received one), issueActionTokens has no production caller, and no REST route reaches it. So every leg that needs a live raw token — the per-approver mint, the confirm page, the redemption, the replay, the not_pending / not_approver pages, the expiry and the bearer probe — is blocked(fixture) on a live boot and is scored from the pins in automated.ref. QA run #24542's capture (the raw links read out of sys_notification.payload.actions after a remind) no longer exists: the absence of a link there is now this item's at-rest leg, not a fixture failure. ⛔ Never plant a token row or patch remind() to get a live link", + "NO STOCK DOOR MINTS AN ACTION TOKEN since PR #24620 (65a8eeb7d4, 2026-10-19). remind() no longer calls issueActionTokens or attaches a link (no channel ever read payload.actions, so no recipient received one), issueActionTokens has no production caller, and no REST route reaches it. So every leg that needs a live raw token — the per-approver mint, the confirm page, the redemption, the replay, the not_pending / not_approver pages, the expiry and the bearer probe — is blocked(fixture) on a live boot. Each such leg is scored as its clause's verify names it, at that clause's own oracle layer (RUNNER rule 6, a pin scores only the layer it drives): the test-oracle clauses acceptance[0]–[3] (the mint, the confirm page, the redemption, the replay) from the pins in automated.ref; acceptance[4]'s expired and consumed pages from action-page-member.integration.test.ts, which renders them through the plugin's page member and mount on a started kernel over a real Hono app; and acceptance[4]'s not_pending / not_approver pages and acceptance[5]'s bearer probe, which no pin drives at their door, stay blocked(fixture) with the lower pins beside them. QA run #24542's capture (the raw links read out of sys_notification.payload.actions after a remind) no longer exists: the absence of a link there is now this item's at-rest leg, not a fixture failure. ⛔ Never plant a token row or patch remind() to get a live link", "the EMAIL-RENDERING leg (the Approve/Reject links arriving as buttons in an actual message) has NO stock producer at all: a reminder names no channel, so messaging materializes it in the in-app inbox only, no approval.reminder email template exists, and since PR #24620 the payload carries no link — the dev log mail transport the forgot-password item uses (EMAIL_TRANSPORT_PROVIDERS 'log' → LogTransport, identity-auth.self-service-password-reset fixtures) would capture nothing to render. Record the leg blocked(fixture), never a defect", - "the EXPIRED leg has no live door on this build: ACTION_TOKEN_TTL_MS is 72h (approval-service.ts), issueActionTokens' ttlMs override is not REST-reachable, the service clock is a constructor option the plugin never threads (approvals-plugin.ts constructs ApprovalService without one), and since PR #22633 (#22616) sys_approval_token declares apiEnabled: false + apiMethods: [], so every data-API verb answers 404 OBJECT_API_DISABLED to every caller, the administrator included — the old recipe (backdate expires_at over the data API) is gone, and update was never in the object's whitelist even before. Score the expired shape from the pinned unit test instead: approval-service.test.ts 'redeem: dead tokens — invalid, expired, decided request, reassigned slot' mints with ttlMs: 1 under the fake clock and pins reason 'expired' (RESULT_COPY.expired in action-link-pages.ts is the page that reason renders); a live run records this one leg as blocked(fixture), never as a defect" + "the EXPIRED leg has no live door on this build: ACTION_TOKEN_TTL_MS is 72h (approval-service.ts), issueActionTokens' ttlMs override is not REST-reachable, the service clock is a constructor option the plugin never threads (approvals-plugin.ts constructs ApprovalService without one), and since PR #22633 (#22616) sys_approval_token declares apiEnabled: false + apiMethods: [], so every data-API verb answers 404 OBJECT_API_DISABLED to every caller, the administrator included — the old recipe (backdate expires_at over the data API) is gone, and update was never in the object's whitelist even before. The expired shape is pinned below the live door instead: approval-service.test.ts 'redeem: dead tokens — invalid, expired, decided request, reassigned slot' mints with ttlMs: 1 under the fake clock and pins reason 'expired' (the service layer; RESULT_COPY.expired in action-link-pages.ts is the page that reason renders), and action-page-member.integration.test.ts renders the expired page through the page member (knownGaps[0]). A live run records this one leg as blocked(fixture), never as a defect, with those pins beside it" ] }, "steps": [ @@ -1238,7 +1238,8 @@ { "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": "#24850" } + { "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" }, + { "revision": 7, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and knownGaps[2] score each live-token leg at its own clause's layer. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: acceptance[0]–[3] are test-oracle clauses and their pins are their oracle; acceptance[4] is a network clause, and action-page-member.integration.test.ts renders its invalid, expired and consumed pages through the plugin's page member and mount on a started kernel (driver-sql over in-memory SQLite, a real Hono app), while its not_pending / not_approver pages and acceptance[5]'s bearer probe have no pin at their door. knownGaps[0] said every such leg is scored from the pins, and knownGaps[2] scored the expired shape from the service test; both now say this, as acceptance[4].verify already did. No clause changed", "ref": "#PENDING-24579R16" } ] }, { @@ -1602,7 +1603,7 @@ "title": "Approvals on v18: the email action door /approvals/act works through the dispatcher exactly as the self-hosted mount (GET renders, POST redeems, a PUT neither door claims an ordinary 404 miss with no Allow header, a dead link a 200 page, a policy-held session not 403'd before the token), and an approval over an unstaffed position opens on the org administrators as ordinary approvers", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -1613,7 +1614,7 @@ "app": "showcase", "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", - "a live action token for the GET-renders / POST-redeems legs — which no stock door mints since PR #24620 (65a8eeb7d4): a stock boot sends no approval email, and a reminder now carries no link (approvals.email-action-token-door knownGaps[0]), so those two legs are scored from the pins in automated.ref. The PUT and dead-link legs need no live token: any non-token string (token=garbage) drives them", + "a live action token for the GET-renders / POST-redeems legs — which no stock door mints since PR #24620 (65a8eeb7d4): a stock boot sends no approval email, and a reminder now carries no link (approvals.email-action-token-door knownGaps[0]), so those two legs are scored at the layer their pins drive, never lower (RUNNER rule 6): the hosted dispatcher door's GET-renders and POST-redeems by packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts (the real createHonoApp catch-all hands the request to the real HttpDispatcher and the real ApprovalService member over in-memory SQLite, with a token minted in-process: that door's answer), and the self-hosted mount's by action-page-member.integration.test.ts (the plugin's own mount on a real Hono app over a started kernel). approvals-act.test.ts drives the real dispatcher over a stub member, so it is evidence of the forwarding only. The PUT and dead-link legs need no live token: any non-token string (token=garbage) drives them", "an approval process whose approver is a position nobody holds (scratch process, default onEmptyApprovers)" ] }, @@ -1631,8 +1632,8 @@ { "clause": "GET renders the action page, POST redeems, a dead token → a 200 page (not an error envelope), and PUT — a method neither wire door claims — is that door's ordinary miss: 404 with no Allow header, never reaching the member (ENDPOINT_NOT_FOUND on the self-hosted `os dev` boot, whose raw-app mount registers only GET and POST; ROUTE_NOT_FOUND on the hosted dispatcher door, whose `/approvals/act` domain claims only GET, HEAD and POST). 405 with Allow: GET, POST is the in-process ApprovalService.handleActionPage member's answer only, which no wire request reaches with a PUT", "oracle": "api", - "verify": "status + headers per verb; for the PUT also error.code and the absence of an Allow header. The PUT and the dead-link legs run live with token=garbage; the GET-renders and POST-redeems halves need a live token no stock door mints since PR #24620, so they are scored from packages/runtime/src/domains/approvals-act.test.ts (the dispatcher forwards GET and POST to the member, the form body unread) and packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (the member renders and redeems). The member's 405 is not a wire leg: it is pinned in-process by action-page-member.integration.test.ts, and the dispatcher's 404 miss by approvals-act.test.ts (both in automated.ref)", - "evidence": "the live PUT line (its error.code and its headers) and the dead-link page, plus the two pins' summary lines for the live-token halves" + "verify": "status + headers per verb; for the PUT also error.code and the absence of an Allow header. The PUT and the dead-link legs run live with token=garbage; the GET-renders and POST-redeems halves need a live token no stock door mints since PR #24620, so they are scored at the layer their pins drive (fixtures.requires[1]): the hosted door's from packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts ('GET renders the confirm page for a live token; a form POST redeems it — the body crossed the catch-all unread'), and the self-hosted mount's from packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts (the member renders and redeems, and its parity cases hold the mount to the member's bytes). packages/runtime/src/domains/approvals-act.test.ts (the real dispatcher forwards GET and POST to a stub member, the form body unread) is evidence of the forwarding only. The member's 405 is not a wire leg: it is pinned in-process by action-page-member.integration.test.ts, and the dispatcher's 404 miss by approvals-act.test.ts (both in automated.ref)", + "evidence": "the live PUT line (its error.code and its headers) and the dead-link page, plus the pins' summary lines for the live-token halves" }, { "clause": "the unstaffed request appears in each org admin's pending list and the decision is recorded via_override:false", @@ -1643,7 +1644,7 @@ ], "automated": { "kind": "dogfood", - "ref": "packages/runtime/src/domains/approvals-act.test.ts + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts + packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts + packages/qa/dogfood/test/approval-admin-rescue-slate.dogfood.test.ts" + "ref": "packages/runtime/src/domains/approvals-act.test.ts + packages/plugins/plugin-approvals/src/action-page-member.integration.test.ts + packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts + packages/plugins/plugin-approvals/src/admin-rescue-named-slate.integration.test.ts + packages/qa/dogfood/test/approval-admin-rescue-slate.dogfood.test.ts" }, "source": [ "packages/runtime/src/domains/approvals.ts", @@ -1669,6 +1670,12 @@ "date": "2026-10-19", "change": "fixtures.requires[1], steps[0] and acceptance[1] (verify, evidence) follow PR #24620 (65a8eeb7d4) — wave anchor #24579 §1, round 2's out-of-scope note (report 6274548573) that #24620 refutes QA run #24542's reading (\"no mail harness needed — the raw links sit in sys_notification.payload.actions after a remind\"); QA run #24558 recorded that a stock boot sends no approval email. No stock door mints an action token now, so the GET-renders / POST-redeems legs are scored from the pins already in automated.ref, and the PUT and dead-link legs run live with a non-token string. Re-derived at base 9a7838edf5: issueActionTokens has no production caller. No clause changed", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "fixtures.requires[1] and acceptance[1] (verify, evidence) score the live-token legs at the layer their pins drive, and automated.ref gains the hosted door's pin. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: approvals-act.test.ts drives the real HttpDispatcher over a stub member (the forwarding); action-page-member.integration.test.ts drives the member and the plugin's mount on a started kernel over a real Hono app; packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts drives the hosted door end to end (createHonoApp's catch-all, the HttpDispatcher, the real ApprovalService member over in-memory SQLite), and its case 'GET renders the confirm page for a live token; a form POST redeems it' is the hosted door's GET-renders and POST-redeems at that door's layer, which the ref missed. No clause changed", + "ref": "#PENDING-24579R16" } ] } From c513f6b812d61c9cdb761182b8879212179820fe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:21:04 +0000 Subject: [PATCH 10/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20reco?= =?UTF-8?q?rds-forms.json:=20unique=20clause=20states=20its=20oracle,=20re?= =?UTF-8?q?cord-history=20spec=20object?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/records-forms.json | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 4e9977e87f..ef229dbd7d 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -66,7 +66,7 @@ "title": "Create → read → update → delete a record through the console UI", "since": "v15", "status": "active", - "revision": 11, + "revision": 12, "priority": "P0", "surface": "browser", "personas": [ @@ -125,7 +125,7 @@ { "clause": "record History reflects the create and the update with display values (option labels, localized dates, real actor), not raw audit payloads or phantom value→null rows", "oracle": "dom", - "verify": "after a screenshot confirms the History tab rendered, read its entries — same contract objectui e2e/live/record-history-display.spec.ts pins on showcase Project", + "verify": "after a screenshot confirms the History tab rendered, read its entries — same contract objectui e2e/live/record-history-display.spec.ts pins on showcase_account, the showcase's enable.trackHistory object (its option labels, the localized Customer Since date and the resolved owner; objectui blob 7ebc45106cff at the console pin 616df6598b42)", "evidence": "screenshot + entries" }, { @@ -231,6 +231,12 @@ "date": "2026-10-19", "change": "steps[2], acceptance[0].verify, acceptance[6].verify, negative[2] and fixtures.provisioning.why corrected from QA run #24572 (18.0 sweep, 5b14b7f3) and the P0 run #24556, re-read at objectstack f89d29e382. (1) The New form submits select defaults the step never touches (sales_region:'amer', default:true in account.object.ts), so the payload diff expects that key. (2) acceptance[6]'s ownership half is unobservable when the admin clones its own row; the provisioned contributor member cloning a seeded showcase_field_zoo row (allowCreate there, permission-sets.ts) is a non-owner cloner, and the same clone shows the autonumber and formula re-derived. (3) negative[2] named `status`, which defaults on showcase_account; the required field with no default there is `name`. The round-2 Tier 1 note that this item's automated.ref over-claims does not hold and is not taken: the ref already scopes itself to 'acceptance[6]'s READONLY half ONLY … Every other clause still needs the run'. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" + }, + { + "revision": 12, + "date": "2026-10-20", + "change": "acceptance[4].verify names the object the objectui spec drives. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said the spec pins the History contract 'on showcase Project'. Read at objectstack's .objectui-sha pin 616df6598b42 (objectui blob 7ebc45106cff): e2e/live/record-history-display.spec.ts creates and patches a showcase_account, the showcase's enable.trackHistory object, and asserts its option labels, the localized signed_on date and the resolved owner. The live verdict at the pin is not measured here. No clause changed", + "ref": "#PENDING-24579R16" } ] }, @@ -4198,7 +4204,7 @@ "title": "Field-level `unique` on the write path: colliding writes refused 409 UNIQUE_VIOLATION on both driver families, NULL-distinct, organization-scoped by default, whole-batch refusal, autonumber resync converges", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -4251,7 +4257,7 @@ "evidence": "the refusal + full before/after row reads" }, { - "clause": "the scope matrix matches ADR-0120 D1/D3 on BOTH families: bare `unique: true` ≡ 'organization' (per-tenant where a tenant column exists; NULL-organization rows fold to ONE bucket), 'global' is platform-wide even with a tenant column — and the SQL family materializes the same rule via uniqueIndexesFromFields (driver-sql/src/schema-drift.ts), answering the same 409 UNIQUE_VIOLATION over REST", + "clause": "the scope matrix matches ADR-0120 D1/D3 on BOTH families: bare `unique: true` ≡ 'organization' (per-tenant where a tenant column exists; NULL-organization rows fold to ONE bucket), 'global' is platform-wide even with a tenant column — and the SQL family materializes the same rule via uniqueIndexesFromFields (driver-sql/src/schema-drift.ts), its refusal satisfying isUniqueViolationError (packages/types/src/unique-violation.ts#isUniqueViolationError), the predicate the platform's 409 UNIQUE_VIOLATION envelope is built on (packages/objectql/src/duplicate-record-error.ts#envelopeUniqueViolation): measured at SqlDriver through the pins' createAsApi mapping, never over a REST door (the REST answers are acceptance[0]–[3]'s, on the stock SQLite boot)", "oracle": "test", "verify": "memory-unique-constraint.test.ts (scope legs) + sql-driver-8577-tenant-scoped-declared-unique.test.ts green; live two-org probe only if two organizations are provisioned (knownGaps)", "evidence": "test output (+ live scope probe where run)" @@ -4274,7 +4280,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/drivers/driver-memory/src/memory-unique-constraint.test.ts — acceptance[4]'s memory-family legs (the \"[#13197] scoping matches driver-sql's ADR-0120 D1/D3 rule\" describe), acceptance[5] ('the refusal is recognisable to the ENGINE, so the autonumber resync converges'), negative[2] ('updateMany refuses BEFORE mutating anything — no half-applied batch') and negative[3]'s memory-family half, at the memory driver; + packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts + packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts — acceptance[4]'s SQL-family legs and negative[3]'s SQL-family half, at SqlDriver over better-sqlite3. NO pin drives a REST door: the SQL pins map each driver refusal through a test-local createAsApi helper to the { status: 409, code: 'UNIQUE_VIOLATION' } envelope rest-server.ts derives, so acceptance[4]'s 'same 409 UNIQUE_VIOLATION over REST' wording rests on that mapping, and every pin is driver-level evidence for acceptance[0]–[3], never their pass: those clauses score the REST answer on the stock SQLite boot" + "ref": "packages/drivers/driver-memory/src/memory-unique-constraint.test.ts — acceptance[4]'s memory-family legs (the \"[#13197] scoping matches driver-sql's ADR-0120 D1/D3 rule\" describe), acceptance[5] ('the refusal is recognisable to the ENGINE, so the autonumber resync converges'), negative[2] ('updateMany refuses BEFORE mutating anything — no half-applied batch') and negative[3]'s memory-family half, at the memory driver; + packages/drivers/driver-sql/src/sql-driver-8577-tenant-scoped-declared-unique.test.ts + packages/drivers/driver-sql/src/sql-driver-declared-index-organization-respelling.test.ts — acceptance[4]'s SQL-family legs and negative[3]'s SQL-family half, at SqlDriver over better-sqlite3. NO pin drives a REST door: the SQL pins map each driver refusal through a test-local createAsApi helper to the { status: 409, code: 'UNIQUE_VIOLATION' } envelope rest-server.ts derives, so acceptance[4]'s SQL-family leg is that mapping, as its clause says, and every pin is driver-level evidence for acceptance[0]–[3], never their pass: those clauses score the REST answer on the stock SQLite boot" }, "source": [ "packages/drivers/driver-memory/src/memory-unique-constraint.ts#scope (#13197/#13249, commit 56c093c — single judgment point; scope table read off driver-sql, arm for arm)", @@ -4314,6 +4320,12 @@ "date": "2026-10-20", "change": "automated.ref narrowed per clause; acceptance[1].verify, acceptance[2].verify, acceptance[3].verify and steps[4] rewritten — checklist-accuracy findings of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4), round 15 of wave anchor #24579. (1) The ref said the two SQL pins assert '409 UNIQUE_VIOLATION over real REST'. They drive SqlDriver over better-sqlite3 and map its refusal to that envelope through a test-local createAsApi helper, so no REST door is driven. (2) The sweep-wide rule-6 contradiction #24840 names at acceptance[1]–[3]: those clauses score the REST answer, and their verifies and steps[4] told the runner to score them from the memory-driver pin ('score from the pin'). By RUNNER rule 6 (a pin scores only the layer it drives) the pins are now cited as driver-level evidence only, and an updateMany door the boot does not expose is recorded blocked(fixture). The ref names the clauses each pin scores: acceptance[4] (test oracle) and [5], and negative[2] and [3]. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "acceptance[4].clause states what its oracle measures, and automated.ref's sentence quoting the old wording follows it. PR #24891's carried finding (round 15 left the clause text, 'the same 409 UNIQUE_VIOLATION over REST', under a test oracle). Read at base feb09db14a: the SQL pins drive SqlDriver over better-sqlite3 and map each refusal through a test-local createAsApi helper with isUniqueViolationError (@objectstack/types), the predicate envelopeUniqueViolation (objectql duplicate-record-error.ts) builds the 409 on; no pin drives a REST door. acceptance[0]–[3] drive the REST door live, so no knownGaps line is owed. The scope matrix the clause asserts is unchanged", + "ref": "#PENDING-24579R16" } ] }, From ff9e3ea76fbc0617b44e2c2e87d02cc29a9f6117 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:21:39 +0000 Subject: [PATCH 11/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20stud?= =?UTF-8?q?io-authoring.json:=20first-run=20F1=20testids=20at=20the=20cons?= =?UTF-8?q?ole=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ledger text only; see the round-16 PR body for the per-finding table. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/studio-authoring.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index d79190d441..a93f569620 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -9,7 +9,7 @@ "title": "The first-run authoring loop closes: package → object → record → app → publish → end-user, zero code, zero restarts", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P0", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai)", "end user (same account driving the published app)"], @@ -39,7 +39,7 @@ { "clause": "package create round-trips and the package switcher lists the new writable package without a page reload", "oracle": "network", - "verify": "the create POST succeeds and the switcher shows the package in the same session (objectui e2e/live/studio-object-designer.spec.ts test F1 is meant to pin it, but ⚠️ at the pinned objectui (.objectui-sha) it targets testids package-id-input / package-name-input that no source file declares, so it cannot be cited as a pass here; drive the clause by hand — and note the new-package wizard's own state, objectui#11327)", + "verify": "the create POST succeeds and the switcher shows the package in the same session. objectui e2e/live/studio-object-designer.spec.ts test F1 pins it at the pinned objectui (.objectui-sha 616df6598b42, blob 58a7f1cbf024): it creates a package through the switcher's New package dialog by the testids studio-pkg-chip, package-form-id-input and package-form-submit, all declared in that tree's app-shell (StudioDesignSurface.tsx, PackageFormDialog.tsx), and asserts that the surface moved into the new package and that the switcher lists it by name with no reload. From this checkout it is not runnable (RUNNER, the objectui-ref fact): drive the clause by hand, or run F1 in an objectui checkout at the pinned revision and cite its output naming the revision. Note the new-package wizard's own state, objectui#11327", "evidence": "the create request/response + switcher screenshot" }, { @@ -88,7 +88,8 @@ { "revision": 1, "date": "2026-08-07", "change": "new area item: the full first-run authoring loop, grounded step-by-step in the 2026-07 Studio dogfood audit ('The loop closes'), with the audit's open UX findings carried as knownGaps instead of asserted", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-08-18", "change": "corrected the ?id= spelling in step 8 and clause 4, which said in an item whose step 2 creates the package com.example.repairs — inviting the package-id spelling that answers with an empty items array (#9334's class). Found by grounding rather than from a report: this item was not on the #9334 list. ⚠️ The card's other instruction for this item — drop any inherited 'Studio is not installed' assumption (#9453 CF-4) — needed NO edit: the item never carried one. It has no blocked marker, no such knownGap, and its steps drive the App Builder directly; the assumption lived in the wave briefing and the run records, not here", "ref": "#9386" }, { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" }, - { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" } + { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" }, + { "revision": 5, "date": "2026-10-20", "change": "acceptance[0].verify follows F1's testids. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said F1 targets package-id-input / package-name-input, which no source declares. Read at the console pin 616df6598b42 (objectui blob 58a7f1cbf024): F1 targets studio-pkg-chip, package-form-id-input and package-form-submit, each declared in that tree's app-shell source, and the two old testids appear nowhere in it. So F1 is citable when run at the pinned revision; the live verdict there is not measured here. No clause changed", "ref": "#PENDING-24579R16" } ] }, { From 8b2b24577275912a6827081107434e497029a30e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:22:44 +0000 Subject: [PATCH 12/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20RUNN?= =?UTF-8?q?ER.md:=20rsync=20is=20absent=20from=20the=20runner=20containers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One environment fact, from QA run #24841. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/RUNNER.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index 5606c63035..e89b947b2b 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -341,6 +341,8 @@ contradicts it, and correct it here when it does. the real port). This is "zero hits needs a positive control" applied to one tool: a negative from a command that never ran is not evidence. +- **`rsync` is not installed in these containers — copy a scratch app with `tar` or `cp -a`.** QA run #24841 met it. + - **A SIGTERM to the `pnpm` wrapper does not stop the server under it — start a backgrounded boot with `setsid` and stop its process group.** The canonical boot (in the `--no-ui` entry below) is `pnpm -C … exec objectstack dev …`: pnpm spawns `os dev`, which From e208b693b566be71af235032e2f7a996c38c7347 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:38:59 +0000 Subject: [PATCH 13/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20inte?= =?UTF-8?q?gration-system.json:=20datasource-admin=20bind=20arm=20read=20p?= =?UTF-8?q?er=20RUNNER=20rule=206?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The item keeps one revision bump and one history row for the round; ledger text only. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- docs/qa/platform-checklist/areas/integration-system.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 01425f248a..541968d443 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1217,7 +1217,7 @@ "GET /api/v1/datasources; confirm qa_ds_probe appears with origin:'runtime' and its health in `status` ('ok' | 'error' | 'blocked' | 'unvalidated', with `statusReason` beside 'error' and 'blocked' — packages/services/service-datasource/src/contracts/datasource-admin-service.ts#DatasourceSummary)", "GET /api/v1/datasources/qa_ds_probe; inspect the body for config + a hasSecret flag and confirm the cleartext secret value is ABSENT", "POST /api/v1/datasources with a bad draft (invalid config shape / missing required); capture status + code", - "POST /api/v1/datasources/qa_ds_probe/migrate-credential (#8155): the fresh, cleanly-created row has nothing to re-home, so capture the 200 whose result reports the plan outcome (the wire answers { result: { name, status } }: 'already-bound' for a row created with a `secret`, 'nothing-to-migrate' for one holding no credential, 'refused' with reason + remedy — packages/services/service-datasource/src/contracts/datasource-admin-service.ts#CredentialMigrationResult) rather than a 400; for the live 'bind' arm a stored row still carrying an inline cleartext credential must be PLANTED directly (the current create path splits `secret` out, so no parse-produced row has one — same fixture caveat as datasource-credential-refusal-matrix's legacy-alias clause); the recipe QA run #24615 proved: stop the server, insert a sys_metadata row of type 'datasource' and origin 'runtime' whose config carries the password inline and no external.credentialsRef, and restart. The detail then reads hasSecret false and redactedConfigKeys ['password'], migrate-credential answers status 'migrated' with migratedKey 'password', and a re-run answers 'already-bound'. Only a run that cannot write such a row scores that arm from the unit pin and records blocked(fixture)", + "POST /api/v1/datasources/qa_ds_probe/migrate-credential (#8155): the fresh, cleanly-created row has nothing to re-home, so capture the 200 whose result reports the plan outcome (the wire answers { result: { name, status } }: 'already-bound' for a row created with a `secret`, 'nothing-to-migrate' for one holding no credential, 'refused' with reason + remedy — packages/services/service-datasource/src/contracts/datasource-admin-service.ts#CredentialMigrationResult) rather than a 400; for the live 'bind' arm a stored row still carrying an inline cleartext credential must be PLANTED directly (the current create path splits `secret` out, so no parse-produced row has one — same fixture caveat as datasource-credential-refusal-matrix's legacy-alias clause); the recipe QA run #24615 proved: stop the server, insert a sys_metadata row of type 'datasource' and origin 'runtime' whose config carries the password inline and no external.credentialsRef, and restart. The detail then reads hasSecret false and redactedConfigKeys ['password'], migrate-credential answers status 'migrated' with migratedKey 'password', and a re-run answers 'already-bound'. Only a run that cannot write such a row records that arm blocked(fixture), with the unit pin beside it (RUNNER rule 6)", "GET /api/v1/datasources/qa_ds_probe/remote-tables (an external-datasource-served route): on a stock os dev boot federation IS wired (serve.ts) so expect a WIRED answer (2xx, or a 400 EXTERNAL_DATASOURCE_ERROR from the introspector — not a 503) and score the 503 clause via its unit pin; only a boot that genuinely lacks the service (dynamic import failed / custom host without the plugin) shows the 503 — capture which service the answer names either way", "GET /api/v1/datasources/does-not-exist; capture the 404" ], @@ -1243,8 +1243,8 @@ { "clause": "POST /:name/migrate-credential re-homes a stored inline cleartext credential into the secret store and drops the inline key — afterwards the read shows hasSecret with the cleartext absent — while a row it cannot re-home safely answers 200 with the plan outcome (action 'refuse' carries operator-facing reason + remedy; action 'none' reports already-bound / nothing-to-migrate), NOT a 400: the datasource is intact and the operator asked a question (#8155). Per-datasource and operator-initiated by construction — no batch spelling exists", "oracle": "api", - "verify": "live: the 'none' outcome on the clean qa_ds_probe + (if a legacy row was planted) the bind outcome and its post-migration redacted read; otherwise the unit pin packages/services/service-datasource/src/__tests__/datasource-credential-migration.test.ts (plan outcomes: bind / drop-inline / none / refuse — datasource-credential-migration.ts; route: admin-routes.ts)", - "evidence": "the migrate responses + the post-migration detail read (or the unit-test output for the bind arm)" + "verify": "live: the 'none' outcome on the clean qa_ds_probe + (if a legacy row was planted) the bind outcome and its post-migration redacted read; otherwise record the bind arm blocked(fixture), with the unit pin packages/services/service-datasource/src/__tests__/datasource-credential-migration.test.ts beside it (plan outcomes: bind / drop-inline / none / refuse — datasource-credential-migration.ts; route: admin-routes.ts): it drives the re-homing planner over row objects, so it is evidence of the planner only, never the door's pass (RUNNER rule 6, a pin scores only the layer it drives)", + "evidence": "the migrate responses + the post-migration detail read (or, for a bind arm recorded blocked(fixture), the unit-test output beside it)" }, { "clause": "a runtime create lands with provenance + health: POST /api/v1/datasources creates a sqlite-file datasource (201) and the subsequent list shows it with origin:'runtime' and its health `status`", @@ -1336,7 +1336,7 @@ { "revision": 6, "date": "2026-10-20", - "change": "automated.ref narrowed per clause. PR #24891's carried finding (round 15 respelled this ref and left its scope, 'pins route behavior + envelope', unnarrowed), read under RUNNER rule 6 and the theme-B rule. Read at base feb09db14a: both pins mount registerDatasourceAdminRoutes on the real HonoHttpServer adapter over stubbed services and a fake auth service, so they score acceptance[7] (test oracle), negative[4] and negative[5] (attributions the route layer owns) whole, and hold only route-layer halves of acceptance[0], [2], [4], [6] and [8]. The capability floor has its own pin (admin-routes-auth-guard.test.ts), which this ref does not name. No clause changed", + "change": "automated.ref narrowed per clause, and the credential-migration bind arm is no longer scored from its unit pin. PR #24891's carried finding (round 15 respelled this ref and left its scope, 'pins route behavior + envelope', unnarrowed), read under RUNNER rule 6 and the theme-B rule. Read at base feb09db14a: both ref pins mount registerDatasourceAdminRoutes on the real HonoHttpServer adapter over stubbed services and a fake auth service, so they score acceptance[7] (test oracle), negative[4] and negative[5] (attributions the route layer owns) whole, and hold only route-layer halves of acceptance[0], [2], [4], [6] and [8]. The capability floor has its own pin (admin-routes-auth-guard.test.ts), which this ref does not name. acceptance[3] (verify, evidence) and steps[8] scored the bind arm from datasource-credential-migration.test.ts, which drives the re-homing planner over row objects, under an api clause; that arm is now blocked(fixture) where the row cannot be planted, with the pin beside it, as the narrowed ref implies. No clause changed", "ref": "#PENDING-24579R16" } ] From 752eac40feaf23b3a623efdba70706da1bcf92d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 18:50:40 +0000 Subject: [PATCH 14/14] =?UTF-8?q?docs(qa):=20round=2016=20=E2=80=94=20hist?= =?UTF-8?q?ory=20rows=20name=20this=20PR=20instead=20of=20the=20placeholde?= =?UTF-8?q?r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 27 new history rows were committed with the placeholder ref; this swaps in the PR's number and changes nothing else. Claude-Session: https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp Co-authored-by: Claude --- .../areas/access-security.json | 4 ++-- .../qa/platform-checklist/areas/api-backend.json | 2 +- docs/qa/platform-checklist/areas/approvals.json | 4 ++-- docs/qa/platform-checklist/areas/automation.json | 4 ++-- docs/qa/platform-checklist/areas/cli.json | 16 ++++++++-------- .../platform-checklist/areas/identity-auth.json | 2 +- .../areas/integration-system.json | 4 ++-- .../platform-checklist/areas/platform-core.json | 10 +++++----- .../platform-checklist/areas/records-forms.json | 4 ++-- docs/qa/platform-checklist/areas/search.json | 2 +- .../areas/studio-authoring.json | 2 +- 11 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 4c8c3573d8..b433d2cb75 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -1494,7 +1494,7 @@ { "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": "#24850" }, - { "revision": 7, "date": "2026-10-20", "change": "negative[2] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule ('do NOT tick external-principal read/write enforcement … this item is the save/authoring gate only') with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (this item tests the save gate, NOT external-principal enforcement; do not tick an external-principal read/write result; an external principal reaches explicitly shared rows only, ADR-0095), so nothing was added. No acceptance clause changed", "ref": "#PENDING-24579R16" } + { "revision": 7, "date": "2026-10-20", "change": "negative[2] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule ('do NOT tick external-principal read/write enforcement … this item is the save/authoring gate only') with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (this item tests the save gate, NOT external-principal enforcement; do not tick an external-principal read/write result; an external principal reaches explicitly shared rows only, ADR-0095), so nothing was added. No acceptance clause changed", "ref": "#24963" } ] }, { @@ -2752,7 +2752,7 @@ "ref": "#12438" }, { "revision": 2, "date": "2026-10-02", "change": "retired the expected-fail framing: 4b45afae (#20817) locked the /automation write doors against the packaged base, and the 17.6.0 release-verification run measured PUT and DELETE /automation/showcase_urgent_task_alert answering 403 NOT_OVERRIDABLE with the /meta door's lock message, the flow unchanged and still firing. Title, knownGaps and clauses 2-3 now state the parity as holding; the restore step is conditional (a restore PUT is itself refused), and the knownGap names the permanent pin packaged-flow-write-door-parity.dogfood.test.ts", "ref": "#21330" }, - { "revision": 3, "date": "2026-10-20", "change": "negative[1] rewritten as the product outcome it guarded, and negative[2] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): both stated a runner rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[1]'s 'do not read the /meta refusal as covering the /automation door' guarded one: an /automation write door answering 2xx on the artifact /meta refused, which is now the FAIL. negative[2] (the toggle door is not this item) guards none, so its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#PENDING-24579R16" } + { "revision": 3, "date": "2026-10-20", "change": "negative[1] rewritten as the product outcome it guarded, and negative[2] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): both stated a runner rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[1]'s 'do not read the /meta refusal as covering the /automation door' guarded one: an /automation write door answering 2xx on the artifact /meta refused, which is now the FAIL. negative[2] (the toggle door is not this item) guards none, so its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#24963" } ] }, { diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index c792849c80..951daedf2a 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1699,7 +1699,7 @@ "revision": 5, "date": "2026-10-20", "change": "fixtures.knownGaps[0] no longer falls back to the unit pin for the MCP clause. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, the MCP clause's unit fallback), read under RUNNER rule 6. acceptance[4]'s oracle is the MCP tool's answer. The 'door 2' case of packages/runtime/src/domains/action-activation-dispatch.test.ts calls invokeBusinessAction in-process over a stubbed engine and metadata service (read at base feb09db14a), so where the endpoint is not mounted the clause is blocked(environment), with the pin beside it. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 5e5d24bce7..9bb16bf8aa 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1239,7 +1239,7 @@ { "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": "#24850" }, - { "revision": 7, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and knownGaps[2] score each live-token leg at its own clause's layer. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: acceptance[0]–[3] are test-oracle clauses and their pins are their oracle; acceptance[4] is a network clause, and action-page-member.integration.test.ts renders its invalid, expired and consumed pages through the plugin's page member and mount on a started kernel (driver-sql over in-memory SQLite, a real Hono app), while its not_pending / not_approver pages and acceptance[5]'s bearer probe have no pin at their door. knownGaps[0] said every such leg is scored from the pins, and knownGaps[2] scored the expired shape from the service test; both now say this, as acceptance[4].verify already did. No clause changed", "ref": "#PENDING-24579R16" } + { "revision": 7, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and knownGaps[2] score each live-token leg at its own clause's layer. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: acceptance[0]–[3] are test-oracle clauses and their pins are their oracle; acceptance[4] is a network clause, and action-page-member.integration.test.ts renders its invalid, expired and consumed pages through the plugin's page member and mount on a started kernel (driver-sql over in-memory SQLite, a real Hono app), while its not_pending / not_approver pages and acceptance[5]'s bearer probe have no pin at their door. knownGaps[0] said every such leg is scored from the pins, and knownGaps[2] scored the expired shape from the service test; both now say this, as acceptance[4].verify already did. No clause changed", "ref": "#24963" } ] }, { @@ -1675,7 +1675,7 @@ "revision": 4, "date": "2026-10-20", "change": "fixtures.requires[1] and acceptance[1] (verify, evidence) score the live-token legs at the layer their pins drive, and automated.ref gains the hosted door's pin. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: approvals-act.test.ts drives the real HttpDispatcher over a stub member (the forwarding); action-page-member.integration.test.ts drives the member and the plugin's mount on a started kernel over a real Hono app; packages/qa/http-conformance/src/hono-approvals-act.conformance.test.ts drives the hosted door end to end (createHonoApp's catch-all, the HttpDispatcher, the real ApprovalService member over in-memory SQLite), and its case 'GET renders the confirm page for a live token; a form POST redeems it' is the hosted door's GET-renders and POST-redeems at that door's layer, which the ref missed. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] } diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 6f4b5c0339..f19a18fb53 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1654,7 +1654,7 @@ "revision": 4, "date": "2026-10-20", "change": "negative[3] rewritten as the product outcome it guarded. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a run scoring the ledger clause from the code alone verified nothing) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). The outcome it guards is the refusal naming the ledger, so the FAIL is now a ledger-disabled flow answered with the status dimension's bare 'Flow NAME is disabled'. Read at base feb09db14a in engine.ts describeDisabledFlow: the ledger arm adds the 'packaged-metadata activation ledger (sys_metadata_activation' sentence, and the status arm returns the bare one. acceptance[1].verify keeps its ⛔ on the code alone. No acceptance clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1749,7 +1749,7 @@ { "revision": 3, "date": "2026-10-16", "change": "the 'still armed' probe patched a showcase_task to done without saying how to get there (#23878 automation slice finding 2, re-measured on main c91af8ed6): showcase_task requires `project` (a create without it answers 400 VALIDATION_FAILED 'Project is required'), and task_status_flow allows status→done only from in_review (a PATCH from the default backlog answers 400 invalid_transition). A create with project and status 'in_review' answers 201 (create is not transition-checked) and the PATCH to done answers 200, firing the record-change flows. Step 4 now creates the task with project and status 'in_review' first; on that path showcase_task_done_notify_owner ran and completed", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "acceptance[0].clause quotes the remedy the engine writes — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-subflow-disable-refusal acceptance[0]; wave anchor #24579 §3): the remedy names the calling flow, so a substring match on the generic quote fails although the meaning is identical. Re-derived at base 9a7838edf5 (packages/services/service-automation/src/engine.ts, the subflow-caller refusal: 'disable the calling flow 'NAME' first' … ' — or leave this one armed.')", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 707, 795, 816, 1322 and 1339 of the showcase flows index.ts in the comma-range spelling (authored in e0abc386a4 as the list 678 to 1339). At that commit those lines were the names and child flowName values of the three stock caller flows; at this base all of them have moved. source[4] now anchors the three callers, index.ts#TaskDoneNotifyOwnerFlow, #ProjectClosureFlow and #ReleaseSignoffFlow, and names each child in prose. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#PENDING-24579R16" } + { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#24963" } ] }, { diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 109b8dfda4..6611e3e6db 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -137,7 +137,7 @@ "revision": 7, "date": "2026-10-20", "change": "acceptance[0] narrowed to its server half, with the console half appended as acceptance[7], and steps[9] stages the config-declared default tier. Checklist-accuracy findings of QA run #24838 (18.0 sweep round 4 at 307ce4c4; QA run #24841 lists the console build among its fixture gaps). (1) acceptance[0] bundled /_console/ with the health and ready probes, so a run with no console build could never pass the server half that held (health 200, ready 200, /_console/ 503 ConsoleNotBuilt). The halves now score apart, and fixtures.requires[2] points at acceptance[7]. (2) acceptance[2]'s matrix names the config-declared default datasource tier, and no step staged it. steps[9] stages it the way resolve-project-database.ts readConfigDeclaredDefault reads it at base feb09db14a: the compiled artifact's datasourceMapping rule { default: true, datasource: NAME } and that datasource's config.filename, resolved against the project root. The step says it is derived from source and not yet replayed live. No other clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -229,7 +229,7 @@ { "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": "#24850" }, - { "revision": 8, "date": "2026-10-20", "change": "steps[3] names the trigger-capability requirement, and negative[2] moved to steps[9]. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The #4409 approver fixture needed requires: ['automation', 'triggers']. Read at base feb09db14a: stack.zod.ts validateTriggerCapability refuses an auto-launched flow at load when requires lacks either token, ahead of the author-time rules this step scores; the blank scaffold (create-objectstack's templates/blank) and the showcase both declare the pair, so the step says to keep it. (2) negative[2] (the deep gate content belongs to other items) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[9]. No clause changed", "ref": "#PENDING-24579R16" } + { "revision": 8, "date": "2026-10-20", "change": "steps[3] names the trigger-capability requirement, and negative[2] moved to steps[9]. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The #4409 approver fixture needed requires: ['automation', 'triggers']. Read at base feb09db14a: stack.zod.ts validateTriggerCapability refuses an auto-launched flow at load when requires lacks either token, ahead of the author-time rules this step scores; the blank scaffold (create-objectstack's templates/blank) and the showcase both declare the pair, so the step says to keep it. (2) negative[2] (the deep gate content belongs to other items) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[9]. No clause changed", "ref": "#24963" } ] }, { @@ -662,7 +662,7 @@ { "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": "#24850" }, - { "revision": 9, "date": "2026-10-20", "change": "negative[4] moved to steps[12]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a runner rule (executing the source pins instead of driving a booted app is not a run) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[12]. No clause changed", "ref": "#PENDING-24579R16" } + { "revision": 9, "date": "2026-10-20", "change": "negative[4] moved to steps[12]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a runner rule (executing the source pins instead of driving a booted app is not a run) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[12]. No clause changed", "ref": "#24963" } ] }, { @@ -913,7 +913,7 @@ "revision": 7, "date": "2026-10-20", "change": "negative[3], steps[11] and acceptance[7].verify count the declared-protocol-range rewrite as a listed site. Checklist-accuracy finding n3 of QA run #24841 (18.0 sweep round 4 at 307ce4c4): `--write` also moved engines.protocol (^17 → ^18) in objectstack.config.ts, announced outside the 'Wrote N … into K file(s)' block, so 'the changed files are exactly the files the block names' read a correct write as a FAIL. Measured on main's CLI text at base feb09db14a (meta.ts, since #22252): the 'Wrote …' block lists only the chain's files and sites; printRangeOutcome prints a separate 'Rewrote the declared protocol range, so the load admits protocol N:' block naming the file and line; the closing 'Re-ran the chain …' line then adds ' The declared protocol range now admits protocol N.'; and --json reports it as write.range, apart from write.written[]. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1331,7 +1331,7 @@ "revision": 10, "date": "2026-10-20", "change": "acceptance[3].verify names the Overwriting line, and negative[2] moved to steps[9]. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) For the seeded same-name pair the plan printed '[Registry] Overwriting view: qa_dup_view', where the verify named only 'Registered view'. Read at base feb09db14a in registry.ts registerItem: it prints 'Overwriting TYPE: KEY' when the key is already held, and 'Registered TYPE: KEY' at the end of every registration. (2) negative[2] (there is no --json flag, and reading its exit 2 as this command's contract is a runner error) stated a runner rule with no product expectation, so it could only score not-run and held the item at partial. Its text moved, unchanged in substance, to steps[9], and negative[3] is now negative[2]; nothing in the item cites either by index. The record's other finding, that planting the within-partition repeat needs the partitioned unique index dropped first, no longer holds: revision 9 (#24850) put it in fixtures.requires[1] (schema-drift.ts buildIndexName). No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1454,7 +1454,7 @@ "revision": 7, "date": "2026-10-20", "change": "acceptance[0].verify, fixtures.knownGaps[2] and steps[1] no longer score the narrowing half from the pins. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, a log clause's half), read under RUNNER rule 6. acceptance[0] is a log clause, the CLI's printed listing. Its pins drive the federation route through the real Hono adapter (remote-tables-twin.equivalence.test.ts), the service (external-datasource-service.test.ts) and the CLI over a stubbed fetch (envelope-unwrap.test.ts), so on the stock SQLite fixture that half is recorded blocked(fixture) with the pins beside it. steps[1] and knownGaps[2] said the same and now say this. automated.ref is unchanged. Read at base feb09db14a. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1552,7 +1552,7 @@ { "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": "#24850" }, - { "revision": 6, "date": "2026-10-20", "change": "negative[4] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule (the runtime sandbox and the generic exit contract belong to other items) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]; fixtures.knownGaps[0] keeps the sandbox half too. No clause changed", "ref": "#PENDING-24579R16" } + { "revision": 6, "date": "2026-10-20", "change": "negative[4] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a scope rule (the runtime sandbox and the generic exit contract belong to other items) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]; fixtures.knownGaps[0] keeps the sandbox half too. No clause changed", "ref": "#24963" } ] }, { @@ -2051,7 +2051,7 @@ "revision": 4, "date": "2026-10-20", "change": "acceptance[5], steps[5] and fixtures.requires[2] added. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4, the upgrade-ceremony pass at 307ce4c4): the item's fixture was only a v17-shaped database (npm @objectstack/cli@17.7.0), so the 18.0 pre-release path the ceremony opened was in no clause. The new clause's oracle is #24838's measured upgrade matrix (real 18.0.0-next.5 and next.3 databases refused, the ceremony applied, each booted with its identity mail, global audit, mail jobs and declared webhook carried over), and each leg is read at its own layer: the boot output, a row read, the door's answer. The fixture is a database the pre-release's own os dev created, plus HTTP writes, the source QA run #24838 found reliable; without one the clause is blocked(fixture). This item's title already names an 18.0 pre-release database, so the clause lives here, and platform-core.v18-boot-refusals points at it. Read at base feb09db14a (organization-ownership-apply.ts SIBLING_MOVES and the mail-job rewrite). No other clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/identity-auth.json b/docs/qa/platform-checklist/areas/identity-auth.json index 62e63978a1..77e9b2d219 100644 --- a/docs/qa/platform-checklist/areas/identity-auth.json +++ b/docs/qa/platform-checklist/areas/identity-auth.json @@ -1075,7 +1075,7 @@ "revision": 5, "date": "2026-10-20", "change": "steps[5]'s expiry leg runs without a clock wait. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4; QA run #24838 ran the same leg): the step said 'let the token expire', and a reset token lives an hour (resetPasswordTokenExpiresIn || 3600, better-auth 1.7.3 routes/password.mjs). It now writes the token's sys_verification.expires_at into the past, the simulation both records measured (400 INVALID_TOKEN, the password unchanged). Read at base feb09db14a: AUTH_VERIFICATION_CONFIG maps expiresAt to expires_at; no verification.storeIdentifier is set, so the identifier is stored plain; consumeVerificationValue treats a row past expiresAt as invalid. The record's other finding (requires and steps still naming sys_email) no longer holds: revision 4 (#24824) moved them to sys_identity_email, and the sys_email mentions left say 'never sys_email'. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 541968d443..7f88657f62 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1337,7 +1337,7 @@ "revision": 6, "date": "2026-10-20", "change": "automated.ref narrowed per clause, and the credential-migration bind arm is no longer scored from its unit pin. PR #24891's carried finding (round 15 respelled this ref and left its scope, 'pins route behavior + envelope', unnarrowed), read under RUNNER rule 6 and the theme-B rule. Read at base feb09db14a: both ref pins mount registerDatasourceAdminRoutes on the real HonoHttpServer adapter over stubbed services and a fake auth service, so they score acceptance[7] (test oracle), negative[4] and negative[5] (attributions the route layer owns) whole, and hold only route-layer halves of acceptance[0], [2], [4], [6] and [8]. The capability floor has its own pin (admin-routes-auth-guard.test.ts), which this ref does not name. acceptance[3] (verify, evidence) and steps[8] scored the bind arm from datasource-credential-migration.test.ts, which drives the re-homing planner over row objects, under an api clause; that arm is now blocked(fixture) where the row cannot be planted, with the pin beside it, as the narrowed ref implies. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1935,7 +1935,7 @@ "revision": 3, "date": "2026-10-20", "change": "acceptance[0].verify and fixtures.knownGaps[0] no longer fall back to the unit pin for the wire shapes. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, wire-shape clauses), read under RUNNER rule 6. acceptance[0] is a network clause: what the echo upstream received. packages/connectors/connector-rest/src/rest-connector.test.ts drives applyAuth through a stubbed fetch (read at base feb09db14a), so without the echo upstream the clause is blocked(fixture), with the pin beside it. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 9b6241533e..433083be70 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -107,7 +107,7 @@ "revision": 8, "date": "2026-10-20", "change": "acceptance[3] narrowed to its server half, with the console half appended as acceptance[5]. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4 at 307ce4c4): acceptance[3] bundled GET /_console/ with the app-metadata read, so a run with no console build (QA runs #24838 and #24786: /_console/ answered 503) could never pass it, although the metadata half held. The two halves now score apart, and acceptance[5] says what an unbuilt tree answers and that it is blocked(environment) (console.ts createConsoleNotBuiltPlugin, read at base feb09db14a). No other clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -1988,7 +1988,7 @@ "revision": 7, "date": "2026-10-20", "change": "negative[5] removed. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a browse-leg upstream failure on an egress-blocked runner is not a product defect; score blocked(environment)) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text already stands, unchanged in substance, in fixtures.knownGaps[0] (on an egress-blocked or air-gapped runner the leg scores blocked(environment), never a product FAIL; what this item scores about browse is the MOUNT), so nothing was added. No acceptance clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -2182,7 +2182,7 @@ "revision": 3, "date": "2026-10-20", "change": "negative[3] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a filing rule (a flag/route contradiction belongs to the marketplace item's clause 1; do not double-file) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). Its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -2839,7 +2839,7 @@ "revision": 4, "date": "2026-10-20", "change": "steps[0] names the namespace prefix; negative[2] and negative[3] removed. Checklist-accuracy findings of QA run #24841 (18.0 sweep round 4 at 307ce4c4). (1) The scratch object needs the package namespace prefix: QA run #24841's unprefixed qa_seedprobe draft saved 200, and its publish answered 422 NAMESPACE_PREFIX. Read at base feb09db14a: the publish doors judge objectNamespacePrefixViolation (protocol.ts, ADR-0129 D2), and a runtime-created package with no namespace takes deriveNamespaceFromPackageId. (2) negative[2] and negative[3] stated a recording rule and a fixture rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[2]'s text already stands, unchanged in substance, in steps[5] and acceptance[4] (do not tick 'replace scoped to its own object'; there is no deletion to scope). negative[3]'s stands in fixtures.requires[1], which now carries its reason as well (it contaminates platform-core.seed-integrity's baselines). No acceptance clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -3202,7 +3202,7 @@ "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] added, a cross-reference. Checklist-accuracy finding of QA run #24838 (18.0 sweep round 4 at 307ce4c4): neither ceremony item had a clause for a real 18.0 pre-release database. The clause is cli.organization-ownership-ceremony acceptance[5], because that item's title names an 18.0 pre-release database and this item's names a 17.x-shaped one; this item points at it. Its scope does not cover fresh installs, so the order #24827 fixed (os migrate apply before the first os serve) is not added here. Read at base feb09db14a. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index ef229dbd7d..a88c818871 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -236,7 +236,7 @@ "revision": 12, "date": "2026-10-20", "change": "acceptance[4].verify names the object the objectui spec drives. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said the spec pins the History contract 'on showcase Project'. Read at objectstack's .objectui-sha pin 616df6598b42 (objectui blob 7ebc45106cff): e2e/live/record-history-display.spec.ts creates and patches a showcase_account, the showcase's enable.trackHistory object, and asserts its option labels, the localized signed_on date and the resolved owner. The live verdict at the pin is not measured here. No clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, @@ -4325,7 +4325,7 @@ "revision": 6, "date": "2026-10-20", "change": "acceptance[4].clause states what its oracle measures, and automated.ref's sentence quoting the old wording follows it. PR #24891's carried finding (round 15 left the clause text, 'the same 409 UNIQUE_VIOLATION over REST', under a test oracle). Read at base feb09db14a: the SQL pins drive SqlDriver over better-sqlite3 and map each refusal through a test-local createAsApi helper with isUniqueViolationError (@objectstack/types), the predicate envelopeUniqueViolation (objectql duplicate-record-error.ts) builds the 409 on; no pin drives a REST door. acceptance[0]–[3] drive the REST door live, so no knownGaps line is owed. The scope matrix the clause asserts is unchanged", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/search.json b/docs/qa/platform-checklist/areas/search.json index 8e9bb8b85e..44de794aac 100644 --- a/docs/qa/platform-checklist/areas/search.json +++ b/docs/qa/platform-checklist/areas/search.json @@ -306,7 +306,7 @@ "revision": 8, "date": "2026-10-20", "change": "negative[1] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a runner rule (the deny side run as admin proves nothing) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). The outcome it guards, a hit or a total for the member from a row it cannot see, is already negative[0]. Its text moved, unchanged in substance, to steps[6], and traps keeps wrong-persona. No acceptance clause changed", - "ref": "#PENDING-24579R16" + "ref": "#24963" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index a93f569620..c2a192472a 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -89,7 +89,7 @@ { "revision": 2, "date": "2026-08-18", "change": "corrected the ?id= spelling in step 8 and clause 4, which said in an item whose step 2 creates the package com.example.repairs — inviting the package-id spelling that answers with an empty items array (#9334's class). Found by grounding rather than from a report: this item was not on the #9334 list. ⚠️ The card's other instruction for this item — drop any inherited 'Studio is not installed' assumption (#9453 CF-4) — needed NO edit: the item never carried one. It has no blocked marker, no such knownGap, and its steps drive the App Builder directly; the assumption lived in the wave briefing and the run records, not here", "ref": "#9386" }, { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" }, { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" }, - { "revision": 5, "date": "2026-10-20", "change": "acceptance[0].verify follows F1's testids. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said F1 targets package-id-input / package-name-input, which no source declares. Read at the console pin 616df6598b42 (objectui blob 58a7f1cbf024): F1 targets studio-pkg-chip, package-form-id-input and package-form-submit, each declared in that tree's app-shell source, and the two old testids appear nowhere in it. So F1 is citable when run at the pinned revision; the live verdict there is not measured here. No clause changed", "ref": "#PENDING-24579R16" } + { "revision": 5, "date": "2026-10-20", "change": "acceptance[0].verify follows F1's testids. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said F1 targets package-id-input / package-name-input, which no source declares. Read at the console pin 616df6598b42 (objectui blob 58a7f1cbf024): F1 targets studio-pkg-chip, package-form-id-input and package-form-submit, each declared in that tree's app-shell source, and the two old testids appear nowhere in it. So F1 is citable when run at the pinned revision; the live verdict there is not measured here. No clause changed", "ref": "#24963" } ] }, {