diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index 81551777fbf..ca58ce76528 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -612,7 +612,7 @@ card filed against it*. Recorded, not acted on — the channel question is #1173 |---|---|---|---|---| | E1 | **`metadata.endpoints.items` gates four routes, three of which its declared meaning does not cover** — its `describe()` says "GET /meta/:type — List items of type", and it also gates `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and the **`POST {prefix}/_migrate-stored` write door**. An operator switching off a listing read silently disarms a migration door and the cross-type spec-validation sweep. `endpoints.item` is milder but the same shape: it also takes `{prefix}/book/:name/tree`. | `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` (four `endpoints.items` gates, four `endpoints.item` gates) vs `packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema` (one route named per switch) | api-backend.rest-metadata-config-contract (a clause requires the run to ENUMERATE each switch's real radius) | design/docs — filed as #15542, **RULED and closed**: every `endpoints.*` switch now gates exactly the face its name states. The whole-store family (`/diagnostics`, `/_drafts`, `POST {prefix}/_migrate-stored`) moved to a new key `maintenance`; `items` is down to its one declared mount; `item` gained the per-item `PUT`/`DELETE` and the history family it never gated (the converse mismatch, filed as #15854 and landed in the same PR); all four `describe()` strings now enumerate their mounts. ⛔ The checklist clause is **kept**, not retired — the run still ENUMERATES each switch's real radius from a route-table diff, and `areas/api-backend.json` revision 2 carries the new expected sets. | | E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code — through the one real door, `createRestApiPlugin({ api })`. ⚠️ **This row used to name a second door, `createHonoServerPlugin({ restConfig })`; no such function exists** (definition probe: zero across the tree, against a positive control that finds `createRestApiPlugin` at `packages/rest/src/rest-api-plugin.ts#createRestApiPlugin`). `HonoServerPlugin` is a class whose `restConfig` option has one reader taking `api.basePath` for the SPA fallback, and it never constructs a REST server. Corrected 2026-09-08 with #15543. A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this, and the correction is about WHERE, not whether. The card and the ruling cite `batch.maxBatchSize`'s docblock as calling the cap *"deployment policy"* and attribute it to `packages/spec/src/api/rest-server.zod.ts`; the phrase **does not occur in that file** (control: `maxBatchSize` occurs there 3 times, so the zero is a real zero). It **does exist**, verbatim, in the REST server: *"The cap is deployment policy — `RestServerConfig.batch.maxBatchSize` (1..1000, default 200)"* at `packages/rest/src/rest-server.ts#enforceBatchSize`. ⇒ **The ruling misattributed the sentence to the wrong file; it did not invent it.** That carrier is real, is the same defect class, and is **owed to a successor card in `domain:rest`** — this PR does not touch `packages/rest`. Inside `packages/spec/src/api/rest-server.zod.ts` the population is: `metadata.maskObjectFields`'s `false` *"opts this server out"* (the ruling's one correctly-placed citation), plus a carrier the card missed entirely, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not forward either. Both are repaired here. | -| E3 | **The MOUNT half of every sub-config switch is unpinned.** `packages/rest/src/rest-sub-config-parse-not-cast.test.ts` pins what a switch normalizes to, and `rest-batch-size-cap.test.ts` pins the cap's effect; nothing asserts that a `false` switch removes its route from the table `getRoutes()` returns. The declared-not-enforced direction — a switch that normalizes correctly and gates nothing — is exactly what no current test would catch. ⚠️ The card said **nine** switches; re-measured on `cc5b3dd0c27` the mount-gating population is **nineteen** — the twelve sub-config switches the card enumerates (its own list adds to twelve, not nine) plus the seven `api.enable*` gates in `registerRoutes`, which are the same seam and were equally unpinned. | the two test files above; the gates live in `registerCrudEndpoints` / `registerBatchEndpoints` / `registerMetadataEndpointsInner` / `registerRoutes` | the three config items (the mount clauses, each with the gap named in `knownGaps`) | test gap — filed as #15544, **closed by `packages/rest/src/rest-config-mount-table.pin.test.ts`**: all nineteen gates pinned as a set difference against the all-true baseline, each with its presence twin (**twenty** since #15542 added `metadata.endpoints.maintenance`; the pin's §0 count moves deliberately with each switch added or retired). ⚠️ The three config items' `knownGaps` still say the harness is the only observation — stale in the good direction, refresh pending (`areas/api-backend.json` was held by another branch when this landed); `rest-metadata-config-contract`'s `automated.ref` now names this pin, the other two are still owed. | +| E3 | **The MOUNT half of every sub-config switch is unpinned.** `packages/rest/src/rest-sub-config-parse-not-cast.test.ts` pins what a switch normalizes to, and `rest-batch-size-cap.test.ts` pins the cap's effect; nothing asserts that a `false` switch removes its route from the table `getRoutes()` returns. The declared-not-enforced direction — a switch that normalizes correctly and gates nothing — is exactly what no current test would catch. ⚠️ The card said **nine** switches; re-measured on `cc5b3dd0c27` the mount-gating population is **nineteen** — the twelve sub-config switches the card enumerates (its own list adds to twelve, not nine) plus the seven `api.enable*` gates in `registerRoutes`, which are the same seam and were equally unpinned. | the two test files above; the gates live in `registerCrudEndpoints` / `registerBatchEndpoints` / `registerMetadataEndpointsInner` / `registerRoutes` | the three config items (the mount clauses, each with the gap named in `knownGaps`) | test gap — filed as #15544, **closed by `packages/rest/src/rest-config-mount-table.pin.test.ts`**: all nineteen gates pinned as a set difference against the all-true baseline, each with its presence twin (**twenty** since #15542 added `metadata.endpoints.maintenance`; the pin's §0 count moves deliberately with each switch added or retired). ⚠️ This row used to end that the three config items' `knownGaps` still said the harness was the only observation (refresh pending, `areas/api-backend.json` held by another branch) and that only `rest-metadata-config-contract`'s `automated.ref` named this pin, the other two still owed; **paid 2026-10-19 with #24649** (`cc95f5e63e`) — `rest-crud-config-contract` and `rest-batch-config-contract`, revision 3 each, name this pin in `automated.ref` and say in `knownGaps[1]` that the mount half is pinned, so all three config items cite it and nothing in this row is owed. | ### 10c. Checked and CLEAN (so the next sweep does not re-derive) diff --git a/docs/qa/platform-checklist/RUNNER.md b/docs/qa/platform-checklist/RUNNER.md index 3f49b5e40c3..ada05953dd4 100644 --- a/docs/qa/platform-checklist/RUNNER.md +++ b/docs/qa/platform-checklist/RUNNER.md @@ -170,20 +170,25 @@ contradicts it, and correct it here when it does. are quoting whenever you cite either. - **Playwright needs an explicit `executablePath` on these containers.** - `@playwright/test` 1.62.1 resolves chromium build **1234**; only **1194** is installed. - **Check:** `grep -A2 '"name": "chromium"' node_modules/playwright-core/browsers.json` + `@playwright/test` 1.63.0 (the `^1.63.0` `examples/app-showcase/package.json` declares, the + only version in the store) resolves chromium build **1243**; only **1194** is installed. + Re-measured at `bfc1539fe0`: this entry read 1.62.1 and build 1234 until the tree moved + to 1.63.0; `playwright-core` sits only in the pnpm store (there is no root + `node_modules/playwright-core`), so the check reads it there. + **Check:** + `grep -A2 '"name": "chromium"' node_modules/.pnpm/playwright-core@*/node_modules/playwright-core/browsers.json` against `ls $PLAYWRIGHT_BROWSERS_PATH` (`/opt/pw-browsers` here, holding `chromium-1194` / `chromium_headless_shell-1194` only). The stock `examples/app-showcase/playwright.config.ts` sets no `executablePath`, so every test dies at browser launch — the error names the **headless-shell** variant it wanted - (`Executable doesn't exist at .../chromium_headless_shell-1234/...`), which is the + (`Executable doesn't exist at .../chromium_headless_shell-1243/...`), which is the signature to recognise. Pass `launchOptions.executablePath=/opt/pw-browsers/chromium` (for the smoke, `OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium`, which the config reads since #21060) and the same specs pass (verified: Playwright launches through it, reporting Chromium 141.0.7390.37, and drives a real page). ⚠️ Use that **alias**, not the versioned `chromium-1194/chrome-linux/chrome` beneath it: `/opt/pw-browsers/chromium` is a symlink maintained by the image build, so it still resolves after the image moves to - 1234, while the versioned literal stops existing at exactly that moment — and a dead + 1243, while the versioned literal stops existing at exactly that moment — and a dead path copied out of this section is the `absence-inference` trap one level up. **The discriminator is uniformity:** a launch/environment failure takes down the whole run at once (`showcase-smoke.spec.ts` generates one test per @@ -333,6 +338,22 @@ contradicts it, and correct it here when it does. with `{"email": "EMAIL", "role": "member"}`; a pending, unexpired `sys_invitation` row for that address is what admits the following `sign-up/email` (`packages/plugins/plugin-auth/src/auth-manager.ts`, `hasPendingInvitationFor`). + ⚠️ **The sign-up does not apply the invitation's role.** It binds the new user to the + Default Organization as `member` whatever role was invited, and leaves the invitation + `pending` (`reconcileMembership` in + `packages/plugins/plugin-auth/src/reconcile-membership.ts` writes `role: 'member'`). For + any other role (`admin`, `owner`), the persona — signed in as itself — then sends + `POST /api/v1/auth/organization/accept-invitation` with `{"invitationId": "ID"}` (the + `id` the invite-member call answered, or the row its own + `GET /api/v1/auth/organization/list-user-invitations` lists). Acceptance writes the + invitation's role onto that membership and the invitation reads `accepted` + (`adoptExistingMembership` in `packages/plugins/plugin-auth/src/adopt-membership.ts`, + which never lowers a grade; pinned by `accept-invitation-adopt-membership.test.ts` + beside it, 6 passed at `bfc1539fe0`). The invitee's email need not be verified: the + deployment declares `requireEmailVerificationOnInvitation: false`. QA run #24616 met it: + an `admin` invitation signed up as `member`, and the persona held `org_admin` only after + the accept. **Check:** read the persona's `sys_member` row (`role`) before scoring a + role-gated clause, never the invitation you sent. 2. **Admin create.** `POST /api/v1/auth/admin/create-user`, which answers `mustChangePassword: true` by default (`packages/plugins/plugin-auth/src/admin-user-endpoints.ts`, `runAdminCreateUser`); @@ -431,6 +452,112 @@ contradicts it, and correct it here when it does. strength of the `automated` field alone: an unrun pin is a claim, not evidence — which is the same "ticking on a label" failure clause-grained acceptance exists to prevent. +- **18.0 boots the `single` tenancy posture only: a walled posture is refused before + anything is served, so a clause that needs a walled boot has no live leg on 18.0.** + `OS_TENANCY_POSTURE=group` or `isolated` — or the legacy `OS_MULTI_ORG_ENABLED=true`, + which `resolveTenancyPosture()` (`packages/types/src/env.ts`) reads as `isolated` — makes + `os serve` print `✖ FATAL: tenancy posture 'isolated' is not supported in 18.0 + (NOT_IMPLEMENTED).` and exit **1** before a config is read, a plugin is mounted or the + HTTP server starts (`walledPostureRefusal()` in `packages/cli/src/commands/serve.ts`, + ruling record 6259698357 on the v18 epic; pinned by + `packages/cli/src/commands/serve-walled-posture-refusal.test.ts`). `os dev` and `os start` + boot through `serve`, and nothing skips it: `OS_ALLOW_DEGRADED_TENANCY` answers a + different refusal (an absent multi-org runtime), which this one now runs ahead of. The + refusal promises the walled postures for "an 18.x release" and names none. Kernel-level + boots (`@objectstack/verify`'s `bootStack`, the dogfood wall suites) never pass through + `serve` and still boot walled, which is why a walled clause is proven by its pins. + Measured at `bfc1539fe0`: `isolated`, `group` and `OS_MULTI_ORG_ENABLED=true` each exit 1 + with that FATAL on `os serve` and on `os start`, `single` passes the gate, and the pin + reports 5 passed. **So:** score a walled clause from the pins its item names, and record + its live leg `skipped` with this entry cited — "not scored on a stock 18.0 boot" — as the + clauses of the withdrawn organization-ownership command are. ⛔ Never `blocked(fixture)`: + no fixture provisions a posture the release refuses. The 18.0 sweep's records met it and + scored the live leg both `blocked(environment)` (QA runs #24557, #24559) and + `blocked(fixture)` (QA run #24558); this entry is the one word. Each item that carries + such a leg names it in its own `fixtures.knownGaps`. + +- **A stock 18.0 boot holds exactly one organization, and no door mints a second — so an + org-switch clause has no live leg on a stock boot either.** + `POST /api/v1/auth/organization/create` answers **`403 ORGANIZATION_CREATION_DISABLED`** + wherever the effective posture enforces no organization wall (`beforeCreateOrganization` + in `packages/plugins/plugin-auth/src/auth-manager.ts`, the same derivation `/auth/config` + publishes as `features.multiOrgEnabled: false`; pinned by + `packages/plugins/plugin-auth/src/org-create-posture-gate.test.ts`), which on 18.0 is + every boot, by the entry above. `sys_organization` offers no create over the data API + (`apiMethods: ['get', 'list', 'update']`, + `packages/platform-objects/src/identity/sys-organization.object.ts`), and 18.0 serves + fresh installs only (the same refusal's text; a 17.x database is refused at boot, + `platform-core.v18-boot-refusals`). `POST /api/v1/auth/organization/set-active` therefore + has the Default Organization, or null, to switch between. **So:** a clause that needs a + second organization — an org switch mid-flow, organization A against B — is not scored on + a stock 18.0 boot: record its live leg `skipped` with this entry cited, and score what its + pins prove. A run that stages the second organization itself (QA run #24560 wrote one with + a system write) scores a fixture of its own making, not a stock boot, and its record says + so. QA runs #24560 and #24558 met the 403; the pin reports 25 passed at `bfc1539fe0`. + +- **One container builds one tree at a time: never run `pnpm build` and + `pnpm objectui:build` together, and give the heavy steps a concurrency the shell's memory + can hold.** `pnpm objectui:build` (`scripts/build-console.sh`) runs `turbo run build` in + THIS checkout as well — `@objectstack/spec` when its dist is absent or incomplete, + `@objectstack/client`, and every `@objectstack/*` package the objectui pin overrides, + declarations included — so a `pnpm build` beside it writes the same `packages/spec/dist` + from a second turbo process nothing serialises, and the spec's DTS pass races (the 18.0 + sweep, #24579 §6). The shell's cgroup is capped near 13.4 GiB (`memory.limit_in_bytes` + read `14343688192` on the cgroup `/proc/self/cgroup` names, at `bfc1539fe0`), and the + console's dependency build is `turbo run build --filter=@object-ui/console^...` at turbo's + default concurrency unless overridden. The 18.0 sweep's runner held both under that cap + only at `--concurrency=2`, and the Tier 1 vitest pins only at `--maxWorkers=1`, and was + killed with exit **137** otherwise (#24579 §6; QA run #24598's environment fingerprint + records the settings that held; the OOM itself is not re-run here). **Fix:** + `pnpm build --concurrency=2` first; then + `OBJECTUI_DEPS_BUILD_CMD='pnpm exec turbo run build --concurrency=2 --filter=@object-ui/console^...' pnpm objectui:build` + (the override the script's header documents); vitest one package at a time with + `--maxWorkers=1`. ⛔ An exit 137 is the container's memory limit, not a failing test or a + broken build — re-run narrower before reading anything into it. + +- **`OS_TEST_TIERS` narrows `packages/cli`'s vitest population, and only that package's — + a cli pin named under the wrong setting runs no test at all.** `scripts/nightly-tiers.mjs` + is the one reader of the switch: unset (or `queue`) collects every test file EXCEPT the + nightly tiers, `*.e2e.test.*` and `*.live.test.*`; `nightly` collects exactly those + (`testFilesOnDisk()` in `packages/cli/vitest-tiers.ts`). `packages/cli` owns every tier + file in the tree (`node scripts/nightly-tiers.mjs --packages`: 1 package, 83 files, at + `bfc1539fe0`), so the switch moves nothing anywhere else. The 18.0 sweep met both + directions: a `*.e2e.test.ts` pin (`serve-organization-record-page.e2e.test.ts`) collects + nothing without `OS_TEST_TIERS=nightly` (QA runs #24556 / #24558), and with it set, 28 of + the 29 cli files the Tier 1 rerun named ran 0 tests (QA run #24598, #24579 round 2). + **Fix:** run a cli `*.e2e.test.*` pin in an invocation of its own with + `OS_TEST_TIERS=nightly`, and every other cli pin without it. ⛔ A named file that collects + no test is not a pass, and here it is not a stale ref either — re-run it under the other + setting before fixing the item. + +- **`scripts/qa/qa-rollup.mjs` has two blind spots a runner meets at close-out.** (1) Only a + BARE area name classifies as an area: `classifySelector('platform-core')` answers `area`, + while `area:platform-core` — the selector spelling `checklist-select.mjs` takes — answers + `other`, so a record titled with it rolls up outside its area (16 records of 2026-10-16, + #23926 among them, do). Title an area run with the bare name, `QA run · platform-core (…)`, + as this sweep's records do. (2) Behind this container's egress proxy the roll-up dies on + its second page: it follows GitHub's `Link: rel="next"`, which names + `https://api.github.com/repositories//issues?…`, the proxy refuses numeric-id paths + **403** ("Numeric-ID repository paths … are not supported through this proxy"), and + `fetchRunRecords` throws on any non-OK page — measured at `bfc1539fe0` with 130 `qa-run` + records, so page 1 (100) is all it reads. The same page asked for by number on the named + path, `repos/objectstack-ai/objectstack/issues?labels=qa-run&state=all&per_page=100&page=2`, + answers 200 (#24579 §6). ⛔ That 403 says nothing about access to the repository: run the + roll-up where the proxy is not in the path, or read the pages by number. + +- **A map view draws its markers under a "Map failed to load" banner here, because the + default base style is unreachable — an environment fact, not a product defect.** The + console's map uses `https://demotiles.maplibre.org/style.json` unless the view declares a + style (objectui `packages/plugin-map/src/ObjectMap.tsx`, `DEFAULT_MAP_STYLE`, the same at + the pin `4016370a` QA run #24616 ran and at the pin `12dc7b47c340` #24660 moved it to), and + this container's egress proxy refuses that host + (`curl https://demotiles.maplibre.org/style.json` → `CONNECT tunnel failed, response 403`, + at `bfc1539fe0`). The map's `error` event then renders an amber `role="alert"` banner, + "Map failed to load (…). Markers below are still listed in the search box", over a canvas + that still carries the markers (QA run #24616, `records-forms.view-type-gallery`). + **So:** score the markers and the record list. ⛔ Do not file the missing tiles, and do + not read the banner as the map failing. + ### Trap vocabulary (`traps` field) | trap | what it fakes | counter | diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 7fe8ece5b92..9da397c523a 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -2347,7 +2347,7 @@ "title": "A session with no active organization is a LEGAL state with fail-closed semantics: tenant-scoped reads resolve to nothing, tenant-scoped writes are refused loudly naming the missing org, and nothing is ever NULL-stamped", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P0", "surface": "api", "personas": [ @@ -2363,7 +2363,8 @@ ], "knownGaps": [ "The state is timing-derived on the signup path (better-auth defers the membership write past the signup transaction, ADR-0093), so reproducing it by racing signup is flaky. The DURABLE reproduction is the removed-member path, or a multi-organization deployment whose reconciler binds nobody (ADR-0093 D1 `no-target-org`). Record WHICH producer the run used — a clause proven only on a racy producer is weaker evidence and must say so.", - "No stock showcase fixture is known to configure `invite-only` or SSO-JIT-pending; those two producers are blocked(fixture) unless the run configures them." + "No stock showcase fixture is known to configure `invite-only` or SSO-JIT-pending; those two producers are blocked(fixture) unless the run configures them.", + "18.0 adds a reason no fixture can answer: every clause needs a tenancy-walled deployment (fixtures.requires[0]), and os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal); see RUNNER.md's environment-facts entry '18.0 boots the single tenancy posture only'. So on 18.0 the item cannot run live on any CLI boot, whatever is provisioned — not even with the @objectstack/organizations runtime revision 2's history names, which is open code since ADR-0132 and would load only after that refusal. blocked (fixture, #9334) stays the marker; on 18.0 the item's only evidence is its pin (automated.ref, which boots the posture in-process). The walled postures return in a later 18.x release" ] }, "blocked": { "by": "fixture", "ref": "#9334" }, @@ -2454,7 +2455,13 @@ "change": "new — the ADR-0123 tenant-semantics wall had ZERO checklist coverage. Neither ratchet could see it: no new metadata kind (it is new behavior across the already-mapped permission/object kinds) and no new enum member. Authored P0 because the ADR records three independently measured production-shaped defects in three subsystems for one state, two of which are silent (a row invisible to its own creator; audit rows invisible to every RLS reader) and therefore invisible to any test that only asserts 'no error'", "ref": "#9299" }, - { "revision": 2, "date": "2026-08-18", "change": "added the two markers this item was missing. blocked {by: fixture, ref: #9334} — three independent rounds (#9334, #9417, and the wave sweep) each re-derived the same dead end: the stock deployment is tenancy: single, so ADR-0095 Layer 0 is inert (tenant-layer.ts postureEnforcesWall returns null) and booting OS_TENANCY_POSTURE=isolated is correctly refused because @objectstack/organizations is not declared; without the marker the selector keeps listing it runnable and a fourth round pays for it again. automated.ref — no-active-organization-write-refusal.test.ts pins every clause at unit level and was already green, which RUNNER rule 6's converse says should have been recorded here", "ref": "#9386" } + { "revision": 2, "date": "2026-08-18", "change": "added the two markers this item was missing. blocked {by: fixture, ref: #9334} — three independent rounds (#9334, #9417, and the wave sweep) each re-derived the same dead end: the stock deployment is tenancy: single, so ADR-0095 Layer 0 is inert (tenant-layer.ts postureEnforcesWall returns null) and booting OS_TENANCY_POSTURE=isolated is correctly refused because @objectstack/organizations is not declared; without the marker the selector keeps listing it runnable and a fourth round pays for it again. automated.ref — no-active-organization-write-refusal.test.ts pins every clause at unit level and was already green, which RUNNER rule 6's converse says should have been recorded here", "ref": "#9386" }, + { + "revision": 3, + "date": "2026-10-19", + "change": "fixtures.knownGaps[2] added — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2; the item stays blocked and hidden from the selector, #24579 §0). Re-derived at base bfc1539fe0: serve.ts walledPostureRefusal refuses group and isolated before a config is read, ahead of the multi-org runtime load revision 2 cites as the refusal. No clause or marker changed", + "ref": "#24687" + } ] }, { @@ -2462,7 +2469,7 @@ "title": "Activation-ledger write authority (ADR-0126 §5): the manage_metadata tier refuses first on every posture, and the shared operator gate — INERT on stock `single` — demands the platform_admin POSITION in walled postures, at both the flow and action doors", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -2480,7 +2487,7 @@ "a runtime-authored scratch permission set with system_permissions ['manage_metadata'] granted to a fresh member via a sys_user_permission_set row (same runtime-authoring pattern access-security.fls-mask-and-strip's read half uses) — no stock showcase set grants manage_metadata" ], "knownGaps": [ - "The group/isolated legs (clauses 5) have NO stock fixture: enabling a walled posture requires the enterprise @objectstack/organizations runtime (packages/spec/src/security/tenancy-posture.ts 'Open code, entitled activation', ADR-0105 D12), no example app sets a posture, and booting OS_TENANCY_POSTURE=isolated without it is correctly REFUSED (ADR-0093 D5) — the same dead end three rounds re-derived on access-security.no-active-org-session-semantics (#9334). Those clauses are blocked(fixture); the unit pins in automated.ref are their standing oracle. Never fake a posture to tick them.", + "The group/isolated legs (clause 5, acceptance[4]; steps[8]; variants[1]–[2]) have no live leg on 18.0: os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal); see RUNNER.md's environment-facts entry '18.0 boots the single tenancy posture only'. That refusal runs ahead of the multi-org runtime load this gap used to name, and the runtime itself is open code since ADR-0132, so neither an 'enterprise runtime' fixture nor ADR-0093 D5's absent-runtime refusal is what a runner meets now, and no fixture unblocks the legs. Clause 5 is scored from the unit pins in automated.ref (oracle test), which drive the posture x identity matrix in-process; its live HTTP leg is not scored on a stock 18.0 boot. The walled postures return in a later 18.x release. Never fake a posture to tick them.", "⚠️ On the STOCK `single` posture the §5 operator gate is INERT by design (activation-gate.ts — postureEnforcesWall('single') is false, so the gate returns before the position is ever read). The refusal an unentitled caller gets on stock showcase is the manage_metadata AUTHORING TIER, not the operator gate — score clause 1/2 against the tier and never against §5, or the run records the wrong gate as proven." ] }, @@ -2493,7 +2500,7 @@ "author the scratch manage_metadata permission set, grant it to a fresh member, and prove server-side that the member's positions do NOT include platform_admin (session row / GET /auth/get-session — never the grant gesture)", "as that member: POST /api/v1/automation/showcase_urgent_task_alert/toggle {\"enabled\": false} — expect 200; read the ledger row back (metadata_type 'flow', active false, and NO organization_id key on the row — the ledger carries no tenant column at all, #15024 / ADR-0131 D7); then re-enable ({\"enabled\": true}) to restore the fixture", "as the same member: POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {\"enabled\": false} — expect 200 + the metadata_type 'action' row; re-enable to restore", - "walled-posture legs: BLOCKED on stock fixtures (see knownGaps) — the group/isolated matrix (org admin with manage_metadata refused naming the posture and the per-artifact remedy; platform_admin POSITION admitted; no row on refusal) is pinned at unit level by automated.ref" + "walled-posture legs: not run live on 18.0, which refuses a walled boot (knownGaps[0]) — the group/isolated matrix (org admin with manage_metadata refused naming the posture and the per-artifact remedy; platform_admin POSITION admitted; no row on refusal) is pinned at unit level by automated.ref; run those pins for clause 5" ], "acceptance": [ { @@ -2521,9 +2528,9 @@ "evidence": "the position proof + both 200 traces + the ledger rows" }, { - "clause": "walled postures (`group`/`isolated`) — BLOCKED on stock fixtures (knownGaps, #9334 precedent): an org admin holding manage_metadata is refused 403 PERMISSION_DENIED with a message naming the POSTURE and the per-artifact remedy — flows recommend the clone path, actions deliberately do NOT (operator + sibling authoring instead) — no row is written on refusal, and a caller carrying the platform_admin POSITION (not a capability) is admitted", + "clause": "walled postures (`group`/`isolated`) — pin-scored, since 18.0 refuses the walled boot a live probe needs (knownGaps[0]): an org admin holding manage_metadata is refused 403 PERMISSION_DENIED with a message naming the POSTURE and the per-artifact remedy — flows recommend the clone path, actions deliberately do NOT (operator + sibling authoring instead) — no row is written on refusal, and a caller carrying the platform_admin POSITION (not a capability) is admitted", "oracle": "test", - "verify": "stands on the unit pins until an enterprise-posture fixture exists: packages/runtime/src/domains/automation-activation-posture-gate.test.ts (#12157) and action-activation-posture-gate.test.ts (#12160) drive the posture x identity matrix against both doors and assert toggleFlow/the write was never entered on refusal; the refusal sentence and remedies are activation-gate.ts,155-163. Do not tick this clause from an HTTP probe on a faked posture", + "verify": "stands on the unit pins, since 18.0 refuses the walled boot a live probe needs (knownGaps[0]): packages/runtime/src/domains/automation-activation-posture-gate.test.ts (#12157) and action-activation-posture-gate.test.ts (#12160) drive the posture x identity matrix against both doors and assert toggleFlow/the write was never entered on refusal; the refusal sentence and remedies are activation-gate.ts,155-163. Do not tick this clause from an HTTP probe on a faked posture", "evidence": "the two test runs' output, cited by test name" }, { @@ -2541,8 +2548,8 @@ ], "variants": [ "single (stock — gate inert, tier decides)", - "group (blocked: enterprise posture fixture)", - "isolated (blocked: enterprise posture fixture)" + "group (pin-only on 18.0: the walled boot is refused)", + "isolated (pin-only on 18.0: the walled boot is refused)" ], "enumSource": { "file": "packages/spec/src/security/tenancy-posture.ts", @@ -2579,6 +2586,12 @@ "date": "2026-10-16", "change": "step 1 and the fixture line name the posture oracle (#23878 checklist-accuracy finding): no API exposes the tenancy posture, and the proof the run used is the boot banner line 'Tenancy: single', which re-measured on main c91af8ed6 prints at any log level, with the info-level \"[security] tenancy posture 'single' — Layer 0 is inert\" line as corroboration. Re-run on that boot: the flow door and the action door both answer the plain member 403 PERMISSION_DENIED with the manage_metadata tier sentences, and the invalid body is still 403", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "fixtures.knownGaps[0], steps[8], acceptance[4] (clause, verify) and variants[1]–[2] re-grounded — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2). The walled legs were blocked(fixture) on an enterprise-runtime gap; re-derived at base bfc1539fe0 that is not what 18.0 does: serve.ts walledPostureRefusal refuses group and isolated before a config is read, ahead of the multi-org runtime load, and that runtime is open code since ADR-0132. Clause 5 keeps its unit pins as its oracle and its wording; no clause deleted", + "ref": "#24687" } ] }, @@ -3342,7 +3355,7 @@ "title": "Under a walled posture (group / isolated) environment metadata and the security catalog belong to the platform operator: an org owner holding manage_metadata is refused 403 on every /meta write verb, every /packages write, install-local of a catalog-carrying package, _migrate-stored and POST /automation — and the single posture is unchanged", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P0", "surface": "api", "personas": [ @@ -3356,10 +3369,13 @@ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", "on the walled boot: a second organization whose owner is granted manage_metadata but not the platform_admin position" + ], + "knownGaps": [ + "Every live leg here needs the walled boot fixtures.requires[1] names and the second organization fixtures.requires[2] names, and a stock 18.0 boot has neither: os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal), and a stock boot holds one organization (see RUNNER.md's environment-facts entries '18.0 boots the single tenancy posture only' and 'A stock 18.0 boot holds exactly one organization'). So on a stock 18.0 boot acceptance[1], acceptance[2] and negative[0] are not scored: record them skipped with those entries cited, never blocked(fixture) and never fail. steps[3]'s single-posture admit is only the control for the walled refusal and proves nothing alone. The item's 18.0 evidence is acceptance[0]: its pins boot the walled posture at kernel level, where serve's refusal never runs. The clauses stay: the walled postures return in a later 18.x release" ] }, "steps": [ - "walled boot, as the org owner: PUT /api/v1/meta/object/qa_walled_probe (a minimal object body); PUT /api/v1/meta/permission/showcase_member_access (any body); POST /api/v1/meta/view/SOME_VIEW/publish; DELETE /api/v1/packages/com.example.showcase", + "walled boot, as the org owner: PUT /api/v1/meta/object/qa_walled_probe (a minimal object body that declares sharingModel, e.g. 'private': the admitted repeats in steps[2] and steps[3] reach the save door's authoring rules, which refuse an object with no authored sharingModel 422 security-owd-unset (packages/lint/src/validate-security-posture.ts#SECURITY_OWD_UNSET), a 422 that reads as the admission failing); PUT /api/v1/meta/permission/showcase_member_access (any body); POST /api/v1/meta/view/SOME_VIEW/publish; DELETE /api/v1/packages/com.example.showcase", "walled boot, as the org owner: POST /api/v1/security/permission-sets/SOME_SET/clone", "walled boot, as the operator: repeat the object PUT and confirm it is admitted (then delete the probe)", "single-posture boot, as admin: the same object PUT is admitted (control)" @@ -3406,6 +3422,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.knownGaps added and steps[0]'s object body made to declare sharingModel — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2; QA runs #24556, #24558 and #24559 scored acceptance[1], acceptance[2] and negative[0] blocked on the boot's NOT_IMPLEMENTED FATAL), and round 2's report on the same wave anchor (a minimal object body is refused 422 security-owd-unset). Re-derived at base bfc1539fe0: serve.ts walledPostureRefusal refuses the walled boot before a config is read; meta-item-write-routes.ts asks the walled refusal ahead of saveMetaItem, so the org owner's refusal is unaffected, while the operator's and the single-posture admin's repeats reach the authoring rules, where validate-security-posture.ts refuses an object with no sharingModel at error tier. No clause deleted", + "ref": "#24687" } ] }, @@ -3650,7 +3672,7 @@ "title": "Grant-table write contract (sys_user_permission_set / sys_user_position): the retired permission_set_id column is refused, a duplicate grant is 409, an inverted validity window is 400, granted_by is stamped with the caller, and (walled) a grant to a non-member is refused", "since": "v18", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -3662,6 +3684,9 @@ "requires": [ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "the member's user id and a showcase permission-set NAME" + ], + "knownGaps": [ + "acceptance[4] and steps[4] are written for a walled boot holding a second organization A, and a stock 18.0 boot has neither: os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal), and a stock boot holds one organization (see RUNNER.md's environment-facts entries '18.0 boots the single tenancy posture only' and 'A stock 18.0 boot holds exactly one organization'). As written, acceptance[4] is not scored on a stock 18.0 boot: record it skipped with those entries cited, never blocked(fixture) and never fail. Its refusal is pinned through acceptance[0] (grant-holder-membership-refusal.test.ts in automated.ref). ⚠️ The refusal itself does not read the posture — that pin's case 'the predicate is the same on every posture' refuses an organization-scoped row naming a non-member under single too (packages/plugins/plugin-security/src/grant-holder-membership-refusal.ts#registerGrantHolderMembershipRefusal) — so the leg is walled by its wording, not by its mechanism. A single-posture rig with a grantee who holds no membership in the writer's organization is not measured here" ] }, "steps": [ @@ -3724,6 +3749,12 @@ "date": "2026-10-19", "change": "acceptance[2] (clause, verify) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.grant-table-write-contract acceptance[2]; wave anchor #24579 §3): the clause required constraint.rule validity_window_order on the refusal, which is unobservable — a plain rule violation carries field, code and message and no constraint object (packages/objectql/src/validation/rule-validator.ts); only an unevaluable predicate attaches one. The clause now asserts what the rule's verdict carries and what the item's own pin sys-user-permission-set-window-order.test.ts asserts (valid_until · rule_violation, the authored message, no unevaluable reason). The walled leg acceptance[4] is §2's and is unchanged", "ref": "#24632" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "fixtures.knownGaps added — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2; QA run #24559 scored acceptance[4] pass on its pin with the live leg blocked(environment) on the boot's NOT_IMPLEMENTED FATAL; rounds 2 and 3 named the leg). Re-derived at base bfc1539fe0: serve.ts walledPostureRefusal refuses the walled boot; grant-holder-membership-refusal.ts's predicate is posture-independent, which the gap records without re-pointing the clause. No clause deleted", + "ref": "#24687" } ] }, @@ -3732,7 +3763,7 @@ "title": "Write guards tightened on v18: a walled update cannot null out organization_id, a detail write naming an unreadable header is reference_not_found, a validation rule reading an unreadable header answers like a missing one, and a controlled_by_parent child's attachments / comments / explain follow the master's edit grant", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3745,6 +3776,9 @@ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "a showcase controlled_by_parent detail object with a master the member can read but not edit (or a scratch pair in the scratch app)", "for the organization leg: the walled boot" + ], + "knownGaps": [ + "acceptance[1] and steps[0] (the organization leg, on fixtures.requires[2]'s walled boot) need a walled posture: the Layer 0 write wall refuses an update that empties organization_id only under one, and its pin's control says the single posture is unchanged (tenant-wall-organization-removal.test.ts in automated.ref). 18.0 refuses the walled boot: os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal); see RUNNER.md's environment-facts entry '18.0 boots the single tenancy posture only'. On a stock 18.0 boot acceptance[1] is not scored: record it skipped with that entry cited, never blocked(fixture) and never fail; acceptance[0]'s pin carries the leg. acceptance[2] and acceptance[3] are not walled legs and run as written" ] }, "steps": [ @@ -3801,6 +3835,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.knownGaps added — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2; QA run #24559 scored acceptance[1] pass on its pin with the live leg blocked(environment) on the boot's NOT_IMPLEMENTED FATAL). Re-derived at base bfc1539fe0: serve.ts walledPostureRefusal refuses the walled boot, and tenant-wall-organization-removal.test.ts pins the emptied-organization refusal as a walled-posture rule with the single posture unchanged. No clause deleted", + "ref": "#24687" } ] }, diff --git a/docs/qa/platform-checklist/areas/attachments-storage.json b/docs/qa/platform-checklist/areas/attachments-storage.json index 5beb78f9ccf..3d1de7b3e6d 100644 --- a/docs/qa/platform-checklist/areas/attachments-storage.json +++ b/docs/qa/platform-checklist/areas/attachments-storage.json @@ -1086,7 +1086,7 @@ "title": "Chunked upload integrity on v18: complete with a missing chunk is 409 with details.missingChunks and the session stays in_progress, a re-sent chunk counts once, parallel chunks all record, and an org switch mid-upload answers 409 on chunk and complete, and on progress only once the session's expiry stamp is due", "since": "v18", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -1098,7 +1098,7 @@ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)" ], "knownGaps": [ - "acceptance[0] names the org-switch guard among what the pins prove, but none of the three automated.ref files drives a CHUNKED door after an organization switch, so the door-level 409 of acceptance[3] (chunk PUT, chunked complete, a progress read whose expiry stamp is due) is unpinned. The guard is the doors' shared requireStartingOrganization (packages/services/service-storage/src/storage-routes.ts, asking packages/services/service-storage/src/metadata-store.ts#organizationOutOfWriteReach); the one door-level pin of it is the presigned COMMIT door's 409 RESOURCE_CONFLICT in packages/services/service-storage/src/error-envelope.conformance.test.ts, a sibling door, and chunk-part-record-concurrency.test.ts pins only the store-level miss. Score acceptance[0] on chunk accounting and the missing-chunk refusal alone, and do not cite the pins for the org-switch guard. acceptance[3]'s live leg also needs a second organization, which a stock 18.0 boot refuses (403 ORGANIZATION_CREATION_DISABLED, QA runs #24560 / #24558) — the wave's no-second-organization theme, parked on #24579 §2" + "acceptance[0] names the org-switch guard among what the pins prove, but none of the three automated.ref files drives a CHUNKED door after an organization switch, so the door-level 409 of acceptance[3] (chunk PUT, chunked complete, a progress read whose expiry stamp is due) is unpinned. The guard is the doors' shared requireStartingOrganization (packages/services/service-storage/src/storage-routes.ts, asking packages/services/service-storage/src/metadata-store.ts#organizationOutOfWriteReach); the one door-level pin of it is the presigned COMMIT door's 409 RESOURCE_CONFLICT in packages/services/service-storage/src/error-envelope.conformance.test.ts, a sibling door, and chunk-part-record-concurrency.test.ts pins only the store-level miss. Score acceptance[0] on chunk accounting and the missing-chunk refusal alone, and do not cite the pins for the org-switch guard. acceptance[3]'s live leg, steps[3], needs a second organization the admin can make active, and a stock 18.0 boot holds one: POST /api/v1/auth/organization/create answers 403 ORGANIZATION_CREATION_DISABLED on every 18.0 boot, since 18.0 boots no walled posture (packages/plugins/plugin-auth/src/auth-manager.ts#ORGANIZATION_CREATION_DISABLED_CODE; QA runs #24560 / #24558; see RUNNER.md's environment-facts entry 'A stock 18.0 boot holds exactly one organization'). On a stock 18.0 boot acceptance[3] is not scored: record it skipped with that entry cited, never blocked(fixture) and never fail. With no chunked-door pin of the guard either, a stock 18.0 run carries no verdict on it; a run that stages the second organization itself, as QA run #24560 did with a system write, scores a fixture of its own making and records that its verdict rests on it" ] }, "steps": [ @@ -1159,6 +1159,12 @@ "date": "2026-10-19", "change": "acceptance[0].clause narrowed to what its pins prove and fixtures.knownGaps added — the out-of-scope §3 note of #24622's round (wave anchor #24579 §3; QA run #24560): none of the three automated.ref pins drives a chunked door after an organization switch, so the door-level 409 acceptance[3] asserts is unpinned. The only door-level pin of the shared guard (storage-routes.ts requireStartingOrganization) is the presigned commit door's, in error-envelope.conformance.test.ts, and chunk-part-record-concurrency.test.ts pins the store-level miss only. acceptance[0] no longer claims the org-switch guard. The knownGap records the unpinned leg, and points at §2 for its live half, which needs a second organization stock 18.0 refuses. acceptance[1]–[3] (#24622) are unchanged", "ref": "#24632" + }, + { + "revision": 4, + "date": "2026-10-19", + "change": "fixtures.knownGaps[0]'s pointer to the wave's second-organization theme replaced by the theme's own statement — the 18.0 sweep's wave-wide theme '18.0 permits no second organization' (wave anchor #24579 §2; QA runs #24560 and #24558). Re-derived at base bfc1539fe0: auth-manager.ts beforeCreateOrganization answers 403 ORGANIZATION_CREATION_DISABLED wherever the effective posture enforces no wall, which on 18.0 is every boot (serve.ts walledPostureRefusal), and sys_organization declares no create method. The clause and its pins are unchanged; the gap now says how a stock run scores it", + "ref": "#24687" } ] } diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index 7ec137e50c9..5a4d78ffc48 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -1014,7 +1014,7 @@ "title": "A paused record-change run's stored state serves its trigger record masked — credential-class fields as the mask, internal fields absent — to a run-state reader, hot and after a cold boot", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": [ @@ -1029,7 +1029,7 @@ "a member persona whose permission set grants read on sys_automation_run (and on the echo object) but is not a platform admin — mint the member with POST /api/v1/auth/admin/create-user (then its password change), author the permission set at runtime, and grant it as the admin with POST /api/v1/data/sys_user_permission_set {user_id, permission_set, organization_id} (QA run #24545)" ], "knownGaps": [ - "Stock showcase has the credential-class object (showcase_field_zoo carries f_password and f_secret) but no ACTIVE record-change flow on it that pauses: showcase_approver_bindings is draft on purpose. Author the fixture flow at runtime (and the member's permission set) or score the item from its pin, recording which the verdict rests on.", + "Stock showcase has the credential-class object (showcase_field_zoo carries f_password and f_secret) but no ACTIVE record-change flow on it that pauses: the one flow on it, showcase_approver_bindings, is status 'obsolete' on purpose — loaded and listed, never handed to a trigger (examples/app-showcase/src/automation/flows/approver-bindings.flow.ts#ApproverBindingsFlow); it is not draft, which would bind and fire like active. Author the fixture flow at runtime (and the member's permission set) or score the item from its pin, recording which the verdict rests on.", "The pin reads every surface as the seeded admin. The mask is applied where the trigger record is built, so it does not depend on the reader, but the member-persona reads in steps 3 and 4 are this item's own clause: score them by hand, never from the pin." ] }, @@ -1104,6 +1104,12 @@ "date": "2026-10-19", "change": "fixtures.requires[2] says how the member gets its grant — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.paused-run-trigger-record-masked fixtures; wave anchor #24579 §3): the run applied the read grant through a sys_user_permission_set row written by the admin. Re-derived at base 9a7838edf5 (sys_user_permission_set declares user_id, permission_set and organization_id). No clause changed", "ref": "#24649" + }, + { + "revision": 3, + "date": "2026-10-19", + "change": "fixtures.knownGaps[0] corrected — a carried note of the 18.0 sweep (round 3's report on wave anchor #24579): it called showcase_approver_bindings 'draft on purpose'. Re-derived at base bfc1539fe0: approver-bindings.flow.ts authors it status: 'obsolete', and its docblock says why not draft — a draft flow binds and fires exactly like an active one, while obsolete is loaded and never armed. It is still the only flow on showcase_field_zoo, so the gap's conclusion holds. No clause changed", + "ref": "#24687" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 1eeb440c891..ad155477000 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -3204,7 +3204,7 @@ "title": "Settings on v18: a global-scope key is stored in sys_platform_setting (readable through /data only with manage_platform_settings), and under a walled posture one organization's tenant save or lock does not change or lock another's, with a data-API read of sys_setting filtered to readable namespaces", "since": "v18", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -3218,6 +3218,9 @@ "a fresh boot of the showcase: `pnpm dev -- --fresh -p PORT --seed-admin` (ephemeral DB; the seeded admin is printed)", "a second boot with OS_TENANCY_POSTURE=isolated (walled posture) on its own port and file DB — the stock single posture leaves every walled clause vacuous, so confirm the posture from the boot log before scoring", "on the walled boot: two organizations A and B with an admin each" + ], + "knownGaps": [ + "acceptance[2] and acceptance[3] are walled legs, and fixtures.requires[1]–[2], personas[2] and steps[2]–[3] serve only them. 18.0 refuses the boot they need: os serve — and os dev / os start, which boot through it — refuses OS_TENANCY_POSTURE=group|isolated (and the legacy OS_MULTI_ORG_ENABLED=true) before a config is read: FATAL: tenancy posture '…' is not supported in 18.0 (NOT_IMPLEMENTED), exit 1 (packages/cli/src/commands/serve.ts#walledPostureRefusal). A stock 18.0 boot also holds one organization, so there is no B to read (see RUNNER.md's environment-facts entries '18.0 boots the single tenancy posture only' and 'A stock 18.0 boot holds exactly one organization'). On a stock 18.0 boot neither clause is scored: record both skipped with those entries cited, never blocked(fixture) and never fail. Their mechanism is scored through acceptance[0]: settings-organization-isolation.pin.test.ts drives two organizations, the lock and the organization-less refusal under a walled posture in-process, and asserts that `single` does not refuse. acceptance[0] and acceptance[1] run as written. The clauses stay: the walled postures return in a later 18.x release" ] }, "steps": [ @@ -3246,7 +3249,7 @@ "evidence": "reads" }, { - "clause": "a tenant key with no organization → 400 SETTINGS_VALIDATION", + "clause": "walled: a tenant key written with no organization in scope → 400 SETTINGS_VALIDATION. Under `single` the same write is admitted, so the refusal is the walled posture's (packages/services/service-settings/src/settings-service.ts#organizationlessTenantKeyError)", "oracle": "api", "verify": "status + code", "evidence": "the answer" @@ -3271,6 +3274,12 @@ "date": "2026-10-16", "change": "initial — authored from the v18 line (18.0.0-next.*) delta: the capability landed on the v18 pre-release line and the ledger carried no `since: \"v18\"` item for it", "ref": "#23879" + }, + { + "revision": 2, + "date": "2026-10-19", + "change": "fixtures.knownGaps added and acceptance[3].clause marked walled — the 18.0 sweep's wave-wide theme '18.0 refuses every walled posture' (wave anchor #24579 §2; QA runs #24557 and #24558 scored both clauses blocked(environment) on the boot's NOT_IMPLEMENTED FATAL). Re-derived at base bfc1539fe0: serve.ts walledPostureRefusal refuses group and isolated before a config is read, and settings-service.ts refuses an organization-less tenant key only under a walled posture, so acceptance[3], which steps[3] already ran on the walled boot, was the second walled leg. No clause deleted; the shared statement is RUNNER.md's environment-facts entry, which this gap points at", + "ref": "#24687" } ] },