Skip to content

Commit 670680e

Browse files
docs(qa): P0 sweep follow-through — re-point query-contract-matrix, correct the run's checklist findings, extend two pins (#21111)
Fixes #21060 Clause-②: no This finishes QA run #21056 (the 2026-10-01 P0 release sweep). It re-points the one stale red row and corrects the run's checklist-accuracy findings, citing source or a local measurement for each one. It also brings two pins up to date. No product code changes. The run's product defects stay on their own cards: #21052, #21057, #21058, objectstack-ai/objectui#11326 and objectstack-ai/objectui#11327. ## 1. The re-point: `api-backend.query-contract-matrix` (rev 2 → 3) | where | old | new | evidence | |---|---|---|---| | acceptance[5] | scalar `$nin` on POST `/query` answers `400 INVALID_FILTER` in both spellings | POST, both spellings: `400 VALIDATION_FAILED` with a located `fields[]` entry (`query.where.status.$nin` for the object body, `query.where` for the AST). GET `$filter`: `400 INVALID_FILTER`. Never a 500, never a widened 200. A GET twin probe is added to step 8 | `FilterConditionSchema.superRefine(checkFilterConditionComparands)` and `lowerFoldedTransportValues` (spec). The POST door answers a `FindDataRequestSchema` failure as `VALIDATION_FAILED` + `fields[]` (`rest-server.ts`). Local parse of both bodies refused at exactly those two paths. The POST-door posture is pinned by `rest-server-repeated-filter-param.test.ts` and `analytics-filter-refusal-envelope.test.ts` | | acceptance[2], step 5 | dotted `fields: ['account.name']` counted as narrowing | refused `400 INVALID_FIELD` by design, with the message naming `expand` | `protocol.ts#assertProjectionFieldsExist` | | negative[0] | probe the widening with `{"filter": …}` | `filter` is a declared transport spelling of `where`, so it is served. The probes are now a mistyped clause (`400 VALIDATION_FAILED`) and an undeclared key (`400 INVALID_FIELD`) | `QueryTransportParamsSchema`; `protocol.ts#assertQueryParamsAreFields`; `request-schema-gate.conformance.test.ts` | A residual for the maintainer (not a defect): the same condition answers `VALIDATION_FAILED` on POST and `INVALID_FILTER` on GET (the #8001 family). ## 2. Checklist-accuracy findings: what changed, old → new - **`access-security.crud-permission-matrix` (3 → 4).** - Old: "31 rows / 25 showcase rows / 100 cells (50/50)". New: 47 rows (41 `showcase_*` + 6 delegate `sys_*`), and the pin's 164 cells (87 allow / 77 deny). The same 9 cells are granted only by the baseline. All of this was recomputed from `access-matrix.json` with the pin's union rule. - `showcase_client_liaison` joins the variants and the seeded-set list. - acceptance[1] names `405 OBJECT_API_METHOD_NOT_ALLOWED` for `sys_user` create/delete, for every caller. Source: `sys_user` `apiMethods` is get/list/update/bulk, and `enforceApiAccess` has no admin bypass. - Clause 5 is recorded as unobservable on stock. The guest anchor ships unbound, and the only anonymous create lane is the public form. - `automated.ref` no longer reads the permission-zoo pin as covering the dev boot (#21057). - steps[0] now mints members invite-first. A new step names the ownership-floor trap on the delegate's edit/delete probes (`owner_only_writes` / `owner_only_deletes`). - **`access-security.write-path-guards` (2 → 3).** - acceptance[5], old: "drops the field for the batch". New: per row. Only the locked row loses the field, reported as `results[i].droppedFields`. Source: `updateManyData` calls `engine.update` per row with its own drop listener. - **`access-security.rls-both-sides` (4 → 5) and `access-security.owd-sharing-matrix` (2 → 3), plus `FOLLOW-UPS.md` §7c.** - The private handle D11 is disambiguated from §7c's public D11 row (an unrelated two-factor defect). The text stays non-disclosing. - Clause numbering is stated: "clause N" here means `acceptance[N]`, and a run record's A(N+1). - **`access-security.anonymous-deny-surfaces` (1 → 2).** `automated.ref` now names all six families the pin drives (see §3). - **`search` recipe `qa-contributor-bound-member`.** - Step 1, old: "this works on a stock dev boot". New: invite first. The undeclared audience posture is `invite_only` (migration entry `audience-posture-default-invite-only`), and an uninvited sign-up answers `403 SELF_REGISTRATION_CLOSED`. - Step 5: point the own-row invoice at a seeded account. - **`ai.mcp-stdio-fail-closed` (3 → 4).** - Two personas: contributor for RLS, client_liaison for FLS. The only `readable: false` grants are client_liaison's, on `public_read_write` `showcase_project`. - acceptance[5] no longer expects `details.forbiddenFields` on the wire. REST's `PERMISSION_DENIED` arm answers `{error, code, object}`. - steps[0] hosts the transport with `os start`. Measured: `os dev` printed 15 banner lines to stdout, and `os start` printed 0. - **`api-backend.packaged-action-disabled-dispatch` (2 → 3).** The MCP door is drivable live: the stock banner prints `MCP: …/api/v1/mcp`. - **`platform-core.boot-health` (5 → 6) and `platform-core.seed-integrity` (2 → 3).** - The seed reads now name the `Seeds:` banner row. At the default warn level, a healthy boot prints no SeedLoader line (`seed-summary.ts`). - The "no ⚠" clause now names the four misauthored flow classes. Stock boots list their policy-disabled scheduled flows with ⚠ (`printAutomationSummary`). - **`cli.dev-boot-contract` (2 → 3).** - steps[4], [6] and [7] write into the app directory they run in, so they need an unshared one. - Exit codes: SIGINT ends plain `dev` with the child's code (0). `--fresh` SIGINT exits 130 (`dev.ts`, `ServeRestartCoordinator`). - The `🔑 Dev admin` banner reprints on later boots and is not seeding evidence (`maybeReportExistingSeedAdmin`). - **`integration-system.datasource-credential-refusal-matrix` (1 → 2).** - The wizard cannot plant a legacy alias row. Measured: `POST /api/v1/datasources` with `passwd` answered `400 DATASOURCE_ADMIN_ERROR`. - The admin door updates by PATCH. Measured: PUT answered 405. - The admin door carries `hasSecret` / `redactedConfigKeys`. - The metadata path is `GET /api/v1/meta/datasource/NAME`. Measured: 200. - `source` gains the mongo composed-branch twin and the nested-options spelling entry. - **`platform-core.console-login` (3 → 4).** - The expiry clause verifies after the server-side sign-out its own step prescribes. - `/_console/home` renders the header and the launcher, with no sidebar by design (objectui `HomeLayout`). - **`platform-core.builtin-apps-nav-render` (3 → 4).** - Re-measured on a stock dev boot: Setup 45 and Account 8 destinations for the admin, and Account 8 for the member. The control reads 8 vs 8; the eighth entry is `nav_connect_agent`. - The member is the seeded Mei (`demo-personas.ts`). - The marketplace catalog's cloud egress dependency is recorded as a known gap. - **`records-forms.crud-roundtrip` (7 → 8).** - The readonly-column half of the clone clause is unobservable on the stock seed. No readonly column on `showcase_account`, no `approval_status` anywhere, and `lead_score` is null on every seeded contact. - An own-row invoice pointing at the run's account turns the later delete into `409 DELETE_RESTRICTED`. Cause: a required lookup turns `set_null` into `restrict` (`engine.ts`). - **`approvals.account-app-entry` (1 → 2).** - The `zh-CN` option is 中文(中国), and there is no bare `zh`. - Tab labels are given in both locales. - The record-link clause needs a row the persona can read. Unreadable rows render no link (`RecordCell`, href null). - The sign-off request must be launched after the persona holds `finance`. - Grounding also showed the seeded personas have been loginable since #9308, so the old reason for provisioning one was wrong. The real reason is their positions. - **`studio-authoring.first-run-loop` (2 → 3).** Each point below was grounded in objectui at the pin: - The object is `repairs_repair_ticket`, because the dialog prefixes the package namespace (`prefixObjectName`). - The New object dialog carries a pre-selected OWD choice. - The "zero nav items" known gap is superseded: the scaffold checkbox defaults on. - steps[8] is a forged request, because authoring is hidden on read-only packages. - acceptance[0]'s cited objectui F1 targets the testids `package-id-input` / `package-name-input`, which no source file declares at the pin. - **`RUNNER.md` Environment facts.** - The CRUD tail now reads `15 verified, 0 gaps, 0 FAILED, 1 needs-fixture, 8 skipped`. - `verify --rls` probes `10 of 10` positions, and the all-personas line is unchanged. - Both were re-measured with `objectstack verify --rls` on this branch. ## 3. The two pins - **`showcase-anonymous-deny-surfaces.dogfood.test.ts`.** - Adds anonymous `POST /batch` (empty operation list) and `GET /security/explain` at 401, each with a member contrast (not 401, not 404). - Both join the rest-flat envelope classification. 42 → 48 tests. - **Merge round (`286870908c`):** merged with main's analytics anonymous-deny change (#21061), keeping both sides. The pin now runs 61 cases (this PR's 6 plus its 13 analytics cases), and all 61 passed on the rebuilt closure. - **`showcase-smoke.spec.ts` + `playwright.config.ts`.** - SURFACES grows from 31 to all 49 destinations `showcase_app` serves. The 18 added ones are derived from `src/ui/apps/index.ts`, including the three filter slices spelled the way objectui's nav renderer builds them. Revenue Pulse and the Hours by Status chart report are chart surfaces. - The main-content wait now treats the boot splash ("Initializing application…") as not rendered, and the assertion refuses a surface still on it. - The config reads `SMOKE_API_URL` for baseURL and the booted server's `-p`. `global-setup.ts` already signs in against that URL. It also reads `OS_TEST_CHROMIUM_EXECUTABLE_PATH` for the browser. Unset, everything stays on port 3000 exactly as before (CI sets neither). ## Tests (union re-run on `de73aa8444`, after merging `origin/main`) - `pnpm check:platform-checklist`. Before, on the base `6f578888a8`: exit 1, with one problem, the pre-existing `identity-auth` `#twoFactor` row. After, on `de73aa8444`: exit 0, `15 areas, 269 items`. `node scripts/check-platform-checklist.mjs --self-test`: exit 0 (221 assertions). - Dogfood pin: `pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/showcase-anonymous-deny-surfaces.dogfood.test.ts`. Result: `48 passed (48)`, on a rebuilt closure. - Ablation of the dogfood pin, run against the committed pin through `scripts/ablation-replace.mjs`. Each mutation was proven on disk, and each restore was proven by blob == HEAD: - Unrouting the explain probe turned exactly its 3 cases red (`expected 404 to be 401`, `expected 404 not to be 404`). - The same mutation on the batch probe also turned 3 red. - Smoke: `SMOKE_API_URL=http://localhost:43871 OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium pnpm exec playwright test e2e/showcase-smoke.spec.ts`, against a console built at the pin (`e420df310f5b`). Result: `49 passed (2.6m)`. The server booted on 43871 and was down afterwards. - An instrumented run (one-off and restored) logged 29 of the 49 surfaces on the boot splash first. The old non-chart check would have passed all 29 on the splash. - `pnpm --filter @objectstack/dogfood typecheck` and `pnpm --filter @objectstack/example-showcase typecheck`: exit 0. Both programs include the edited files. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derived 55 families, all run. 54 exit 0. NOT MEASURED: `check:dual-build-cjs-loads`, reason "PREREQUISITE NOT MET": 8 untouched packages have no `dist/` in this container. `check:type-check-debt` exit 0 on a clean standalone re-run; an earlier run's log was clobbered by a timed-out batch. ## Card findings NOT acted on, and why - **`owd-sharing-matrix` step 1 / crud persona recipe ("grant the position, not the set directly").** The reason is withheld pending the maintainer (RUNNER rule 2, #21052). It cannot be grounded publicly, so it is left untouched. - **Personas, "all member-persona items".** I updated the shared recipe and the P0 items this run drove. About 28 more active items mention sign-up personas, and several sit in `identity-auth.json` (open-PR concurrency). That sweep is left for the PM: an open question, a RUNNER fact versus a per-item sweep. - **Patch round (`db092de284`):** the residual items are now covered by one RUNNER "Environment facts" bullet. It states the stock `invite_only` posture, the `403 SELF_REGISTRATION_CLOSED` refusal, and the two doors (invite-member then sign-up, or admin create-user then change-password), with `search:qa-contributor-bound-member` as the worked example. This follows the seat's ACCEPT on the open question, option A. - **`records-forms.crud-roundtrip` "list quick-search text persists across reloads".** I could not confirm this from objectui source at the pin, so it is not written as fact. - **§3 fixture gaps and environment.** These are recorded on the card. The card says no edit is needed unless chosen, and they lie outside the claimed RUNNER lines. ## Acceptance notes - Two RUNNER lines outside the named "Environment facts" lines changed as a direct consequence of the smoke change. The Playwright fact's test count went from 31 to 49, and it now names the browser override. - The `datasource-credential-redaction.ts` docblock still says the wizard persists through a structural name/label check. A live probe refuses an alias key 400, so this is comment drift; no carrier found. - The URL-branch mongo entry's "Deliberately NOT refused … the composed branch" sentence reads as if the platform did not refuse that branch. Its twin does, since #9147. This is spec registry text, outside this ledger. ## Line budget 15 files changed: +375 / −130 against `origin/main`. Breakdown: - 12 checklist JSON/MD files - the dogfood pin (+54) - the smoke spec (+56) and config (+33) --- _Generated by [Claude Code](https://claude.ai/code/session_01JAhu8u8QfBvRjVZDox7CP9)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b11d778 commit 670680e

15 files changed

Lines changed: 403 additions & 130 deletions

‎docs/qa/platform-checklist/FOLLOW-UPS.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,14 @@ declares exactly `.` and `./logger`. Do not re-derive the subpath.
280280
| D14 | `MigrationRecoveryPlugin` is composed by NO boot path — `serve.ts` auto-registers `PlatformObjectsPlugin` but never the recovery plugin; standalone-stack, default-host, the showcase config, and the migrate CLI boot all omit it; only its unit test instantiates it. Interrupted-migration detection therefore never runs on any shipped boot, while `sys-migration-journal.object.ts` argues recovery must need "zero host wiring" | `packages/runtime/src/index.ts` (exported); `serve.ts` (what IS auto-registered) | platform-core.interrupted-migration-boot-report (fixtures an explicit registration; knownGap names the composition hole) | correctness/composition — safe to file |
281281
| D15 | `extract-hook-body.ts`'s header promises "the build fails… no silent fallback" on a forbidden pattern, but the DEFAULT `os build` catches every extraction error and silently falls back to the.mjs bundle (`lower-callables.ts`), printing the warnings nowhere; only `--strict-body` (`compile.ts`) produces the worded refusals with exit 1. `hook-bodies.mdx` documents the warn-and-bundle default, so code comment and docs disagree with each other | `extract-hook-body.ts` vs `lower-callables.ts`, `compile.ts` | cli.hook-body-extraction-gates (default-path silent-fallback encoded as expected-fail contradiction clause) | correctness — safe to file |
282282

283+
⚠️ **Handle collision on D11 (#21060).** The D11 row above is the two-factor `get-totp-uri`
284+
re-reveal. It is **not** the private handle D11 that `access-security.rls-both-sides`
285+
clause 5 and `access-security.owd-sharing-matrix` clause 4 cite. That handle names a
286+
security-sensitive defect delivered to the maintainer privately (#7463), and it is
287+
deliberately not reproduced in this file. The two numbers collided when this table continued
288+
the D-series. Cite the item and its clause, never the bare handle, and never read one D11 as
289+
the other.
290+
283291
Two design notes captured inside items rather than as defect rows: `sys_user.mfa_required_at`
284292
is stamped lazily and never cleared anywhere in source, so post-disable re-gating branches on
285293
a pre-existing stamp (identity-auth.two-factor-disable-lifecycle, design-note clause); and

‎docs/qa/platform-checklist/RUNNER.md‎

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,17 +139,19 @@ contradicts it, and correct it here when it does.
139139
section at all.** `runRlsProofs` runs only behind the flag
140140
(`packages/cli/src/commands/verify.ts`: `rls: Flags.boolean({ default: false })`, the
141141
proofs sit inside `if (flags.rls)`, and the report prints `if (rls)`). **Check:** the
142-
last block of a bare run is the CRUD summary — `── 16 verified, 0 gaps, 0 FAILED, 1
143-
needs-fixture, 7 skipped` on stock showcase — with no `PROVEN`/`HOLES` line anywhere.
142+
last block of a bare run is the CRUD summary — `── 15 verified, 0 gaps, 0 FAILED, 1
143+
needs-fixture, 8 skipped` on stock showcase — with no `PROVEN`/`HOLES` line anywhere.
144144
Adding `--rls` appends the RLS block. ⛔ Do not cite plain `verify` output as the
145145
oracle for an RLS clause — that run never consulted one.
146146

147147
**The discriminator is the PRESENCE of a `PROVEN`/`HOLES` line, never its digits.**
148148
The counts move with the seed set and are recorded here only as the shape to expect:
149-
the CRUD tail read `15 verified` through 2026-08-18 and reads `16` on 17.1.0 because
149+
the CRUD tail read `15 verified` through 2026-08-18 and `16` on 17.1.0 because
150150
the showcase gained one object (`client-brief.object.ts`, added between the two sweep
151-
subjects); a run whose digits differ has a different seed, not a regression. Re-derive
152-
the number, do not file it.
151+
subjects), and reads `15 verified … 1 needs-fixture, 8 skipped` again by 2026-10-01
152+
(QA run #21056, re-measured for #21060): one object moved from verified to skipped,
153+
the 24-object total unchanged. A run whose digits differ has a different seed, not a
154+
regression. Re-derive the number, do not file it.
153155

154156
⚠️ **The `--rls` block prints TWO summary lines in DIFFERENT units — do not compare
155157
them.** `formatRlsReport` (`packages/verify/src/rls.ts`) emits a per-persona
@@ -160,7 +162,9 @@ contradicts it, and correct it here when it does.
160162
source stamps that unit into the line precisely so the two are not read as one number
161163
moving. Measured on 17.1.0: the total line reads
162164
`all personas: 38 PROVEN (38 consistent, 0 HOLES) · 226 NOT PROVEN`, alongside
163-
`9 of 9 declared position(s) probed`. An older note here recorded
165+
`9 of 9 declared position(s) probed`; re-measured 2026-10-01 (#21060), the total line is
166+
unchanged and the positions read `10 of 10` — the showcase declares ten positions
167+
now (digits only, as above). An older note here recorded
164168
`20 PROVEN … over 23 objects` — that was the per-persona line, so it is **not
165169
comparable** to the total rather than merely stale. Name which of the two lines you
166170
are quoting whenever you cite either.
@@ -174,15 +178,16 @@ contradicts it, and correct it here when it does.
174178
dies at browser launch — the error names the **headless-shell** variant it wanted
175179
(`Executable doesn't exist at .../chromium_headless_shell-1234/...`), which is the
176180
signature to recognise. Pass `launchOptions.executablePath=/opt/pw-browsers/chromium`
177-
and the same specs pass (verified: Playwright launches through it, reporting Chromium
181+
(for the smoke, `OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium`, which the
182+
config reads since #21060) and the same specs pass (verified: Playwright launches through it, reporting Chromium
178183
141.0.7390.37, and drives a real page). ⚠️ Use that **alias**, not the versioned
179184
`chromium-1194/chrome-linux/chrome` beneath it: `/opt/pw-browsers/chromium` is a
180185
symlink maintained by the image build, so it still resolves after the image moves to
181186
1234, while the versioned literal stops existing at exactly that moment — and a dead
182187
path copied out of this section is the `absence-inference` trap one level up.
183188
**The discriminator is uniformity:** a launch/environment failure
184189
takes down the whole run at once (`showcase-smoke.spec.ts` generates one test per
185-
`SURFACES` entry — 31 today, so "31 failed" means all of them), while a product defect
190+
`SURFACES` entry — 49 today, so "49 failed" means all of them), while a product defect
186191
fails selectively. ⛔ Do not file a whole-run red as a product defect before checking
187192
the browser resolved.
188193

@@ -263,6 +268,33 @@ contradicts it, and correct it here when it does.
263268
upload is broken". ⛔ Do not drive any storage/upload surface from an injected
264269
token: **sign in through the form** so both halves exist.
265270

271+
- **A stock showcase boot refuses self-registration — no item can mint a member by
272+
"fresh sign-up".** The showcase declares no `auth.audience`, so its posture is the
273+
`invite_only` default (`packages/plugins/plugin-auth/src/audience-posture.ts`:
274+
`resolveAudience` — undeclared ⇒ `invite_only`; the migration entry
275+
`audience-posture-default-invite-only` records the flip), and
276+
`POST /api/v1/auth/sign-up/email` for an address holding no pending invitation answers
277+
**`403 SELF_REGISTRATION_CLOSED`** (`decideAudienceAdmission` in the same file; the code
278+
is in the ADR-0112 ledger, `error-code-ledger.zod.ts`). QA run #21056 met it on every
279+
member persona it needed. Two doors mint one instead:
280+
1. **Invite, then sign up.** As admin, `POST /api/v1/auth/organization/invite-member`
281+
with `{"email": "EMAIL", "role": "member"}`; a pending, unexpired `sys_invitation`
282+
row for that address is what admits the following `sign-up/email`
283+
(`packages/plugins/plugin-auth/src/auth-manager.ts`, `hasPendingInvitationFor`).
284+
2. **Admin create.** `POST /api/v1/auth/admin/create-user`, which answers
285+
`mustChangePassword: true` by default
286+
(`packages/plugins/plugin-auth/src/admin-user-endpoints.ts`, `runAdminCreateUser`);
287+
have the persona `POST /api/v1/auth/change-password` before driving it.
288+
289+
The worked example is the shared recipe `search:qa-contributor-bound-member`
290+
(`areas/search.json`), whose `requires` carries the invite ahead of its step-1 sign-up.
291+
Wherever an item still says "fresh sign-up", read it as an identity minted one of these
292+
two ways. ⛔ Do not record a member clause `blocked(fixture)` on the strength of the 403,
293+
and do not file it: the refusal is the posture working. The seeded demo personas
294+
(`examples/app-showcase/src/security/demo-personas.ts`, password `showcase123`) are
295+
loginable already, but they hold fixed positions, so they fit only where an item names
296+
them.
297+
266298
- **A cold tree cannot boot the app from the console-build recipe alone.**
267299
`pnpm objectui:build` runs `scripts/build-console.sh`, which builds the **console**, not
268300
the framework CLI. On a fresh tree `packages/cli` has no `dist`, and the bare binary

0 commit comments

Comments
 (0)