From ae797bd39b4b363e8e9fc1fbbe904151230b763d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 11:41:35 +0000 Subject: [PATCH] =?UTF-8?q?docs(qa):=20wave-3=20checklist=20accuracy=20sli?= =?UTF-8?q?ce=20S9=20=E2=80=94=20the=2020=20W3-C1R=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-verified each of the 20 checklist-accuracy findings W3-C1R recorded at eeec88d1 (QA run #28545) against the current head, and edited the 13 items that carry them across nine area files (revision +1 and one history row each). Row 16 (the studio-authoring publish toast) was already fixed upstream by revision 9 of studio-authoring.first-run-loop. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd --- .../areas/access-security.json | 36 ++++++++++++++----- docs/qa/platform-checklist/areas/ai.json | 10 ++++-- .../platform-checklist/areas/api-backend.json | 20 +++++++---- .../platform-checklist/areas/approvals.json | 16 ++++++--- docs/qa/platform-checklist/areas/cli.json | 15 +++++--- .../areas/integration-system.json | 18 +++++++--- .../areas/platform-core.json | 26 ++++++++++---- .../areas/records-forms.json | 12 +++++-- .../areas/studio-authoring.json | 7 ++-- 9 files changed, 116 insertions(+), 44 deletions(-) diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index 6d738552d7b..1161a4e214d 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -499,7 +499,7 @@ "title": "CRUD × permission-set matrix: every access-matrix.json row holds — allowed verbs succeed, withheld verbs 403, VAMA bounded", "since": "v15", "status": "active", - "revision": 11, + "revision": 12, "priority": "P0", "surface": "api", "personas": [ @@ -521,13 +521,13 @@ ] }, "steps": [ - "boot the showcase on its own port and DB; admin session; for each permission set in access-matrix.json create a fresh member (unique email) and grant exactly that set by inserting the sys_user_permission_set row (system context), mirroring showcase-permission-zoo.dogfood.test.ts. ⚠️ the stock showcase declares no auth.audience, so its posture is the invite_only default and an uninvited POST /api/v1/auth/sign-up/email answers 403 SELF_REGISTRATION_CLOSED: mint each member by an admin POST /api/v1/auth/organization/invite-member for that address first (a pending sys_invitation row admits the sign-up), or by an admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true", + "boot the showcase on its own port and DB; admin session; for each permission set in access-matrix.json create a fresh member (unique email) and grant exactly that set with one sys_user_permission_set row. A live run writes it as the admin over HTTP: POST /api/v1/data/sys_user_permission_set {\"user_id\": MEMBER_ID, \"permission_set\": SET_NAME} answers 201 and the grant takes effect (QA run #28545; the object declares create among its apiMethods, packages/plugins/plugin-security/src/objects/sys-user-permission-set.object.ts). The pins insert the same row in system context (showcase-permission-zoo.dogfood.test.ts, showcase-crud-persona-matrix.dogfood.test.ts). ⚠️ the stock showcase declares no auth.audience, so its posture is the invite_only default and an uninvited POST /api/v1/auth/sign-up/email answers 403 SELF_REGISTRATION_CLOSED: mint each member by an admin POST /api/v1/auth/organization/invite-member for that address first (a pending sys_invitation row admits the sign-up), or by an admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true", "as admin (or a helper member), seed one FOREIGN-owned probe row per object under test (e.g. a showcase_private_note, showcase_inquiry, showcase_invoice + line, showcase_announcement owned by someone other than the persona)", "for every row of access-matrix.json drive all four verbs as that persona: POST /api/v1/data/ {minimal valid payload}, GET /api/v1/data/ + GET /api/v1/data//, PATCH /api/v1/data//, DELETE /api/v1/data// — capture status + the top-level body.code for each cell (acceptance[1])", "for rows with viewAllRecords:true (auditor on showcase_inquiry/showcase_invoice/showcase_invoice_line/showcase_private_note): GET the foreign probe by id and list; for the SAME objects re-run as a persona whose row says viewAllRecords:false", "for rows with modifyAllRecords:true (ops on showcase_announcement): PATCH the foreign-owned announcement; re-run the same PATCH as member_default (modifyAllRecords:false)", "for the guest_portal row pair on showcase_inquiry (create:true, read:false): record clause 5 blocked(dependency, #23267) per knownGaps[2]: no door binds a set to the guest anchor (ADR-0138 D3 item 5, ruling C), a member holding the set reads inquiries through the baseline, and REST anonymous answers 401 to both verbs", - "for the 4 showcase_field_ops_delegate × sys_* rows: their 16 cells are pinned by the dogfood case 'the 16 showcase_field_ops_delegate × sys_* cells answer the matrix' (automated.ref) — cite its output per RUNNER rule 6 rather than re-deriving it; when a run drives the delegate live as well, for its sys_user_position edit and delete cells, probe a row the DELEGATE created — a row another principal created meets the platform's write ownership floor (member_default owner_only_writes / owner_only_deletes, created_by == current_user.id), so its verdict does not read the delegate's grant", + "for the 4 showcase_field_ops_delegate × sys_* rows: their 16 cells are pinned by the dogfood case 'the 16 showcase_field_ops_delegate × sys_* cells answer the matrix' (automated.ref) — cite its output per RUNNER rule 6 rather than re-deriving it; when a run drives the delegate live as well, for its sys_user_position edit and delete cells, probe a row the DELEGATE created — a row another principal created meets the platform's write ownership floor (member_default owner_only_writes / owner_only_deletes, created_by == current_user.id), so its verdict does not read the delegate's grant. A live delegate needs its Field Operations membership beside the set grant of steps[0], or its sys_user_position cells have no subtree to act in: the admin writes POST /api/v1/data/sys_business_unit_member {\"business_unit_id\": \"bu_field_ops\", \"user_id\": DELEGATE_ID} (QA run #28545), the row the pin inserts in system context (showcase-crud-persona-matrix.dogfood.test.ts; bu_field_ops and its offices bu_west_coast and bu_east_coast are seeded in examples/app-showcase/src/data/seed/index.ts)", "record the full verb × set × object verdict matrix and diff it against access-matrix.json (unioned with the baseline per the known gap above)" ], "acceptance": [ @@ -664,6 +664,12 @@ "date": "2026-10-31", "change": "Two findings of QA run #27481. acceptance[1] (clause, verify) and steps[2] said error.code PERMISSION_DENIED; the data door answers {error: MESSAGE, code, object} (packages/types/src/data-error-classification.ts mapDataError), and showcase-crud-persona-matrix.dogfood.test.ts reads the top-level code for every denial and for the 405 OBJECT_API_METHOD_NOT_ALLOWED cells. The clause now names body.code, and the verify says body.error.code does not exist on this door. acceptance[6].verify and steps[6] still called the 16 showcase_field_ops_delegate × sys_* cells hand-driven, though automated.ref has pinned them since revision 9 (that revision's row noted both were left behind). Both now name the pin case, so the pin covers all 45 rows × 4 verbs; steps[6]'s own-row rule for sys_user_position stays for a live pass. No assertion is weakened. Read from source at base efdddc9060. Wave anchor #27377, wave-3 accuracy slice S3; finding parked in anchor comment 6493994883", "ref": "#27481" + }, + { + "revision": 12, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding: steps[0] and steps[6] described the set grant only as a system-context insert. steps[0] now names the live door, an admin POST /api/v1/data/sys_user_permission_set {user_id, permission_set} (201, effective, as #28545 drove it; sys-user-permission-set.object.ts declares create), and keeps the pins' system-context insert as theirs. steps[6] adds the delegate's POST /api/v1/data/sys_business_unit_member into bu_field_ops, the membership the pin places beside the grant (showcase-crud-persona-matrix.dogfood.test.ts). No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, @@ -672,7 +678,7 @@ "title": "Sharing-model / OWD matrix: private, public_read, public_read_write, controlled_by_parent each enforce their declared baseline", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P0", "surface": "api", "personas": [ @@ -693,7 +699,7 @@ "boot the showcase on its own port and DB; create members A and B (baseline), two contributor members C1/C2 (grant showcase_contributor), an ops member, an auditor member", "private: as A create a showcase_private_note; as B list, GET by id, PATCH it", "public_read: as admin (or ops) create a showcase_announcement owned by someone other than B; as B GET list + by id, then PATCH it; as ops PATCH the same announcement (modifyAllRecords contrast)", - "public_read_write: as admin create a showcase_project with status 'planned' in the body — project_status_flow declares initialStates ['planned'] with insert among its events, so any other initial status answers 400 invalid_initial_state (examples/app-showcase/src/data/objects/project.object.ts; QA run #27481); as C1 (allowEdit:true on projects, no project RLS) GET it and PATCH a non-FLS field (e.g. name suffix) — record-level write open by OWD; as A (member_default, allowEdit:false) attempt the same PATCH — the object-level bit must still gate", + "public_read_write: as admin create a showcase_project with {\"name\": \"…\", \"status\": \"planned\", \"account\": \"\"} — account is a required lookup (required: true in examples/app-showcase/src/data/objects/project.object.ts), so a body without it is refused 400 (QA run #28545 read 'Account is required'); and project_status_flow declares initialStates ['planned'] with insert among its events, so any other initial status answers 400 invalid_initial_state (examples/app-showcase/src/data/objects/project.object.ts; QA run #27481); as C1 (allowEdit:true on projects, no project RLS) GET it and PATCH a non-FLS field (e.g. name suffix) — record-level write open by OWD; as A (member_default, allowEdit:false) attempt the same PATCH — the object-level bit must still gate", "controlled_by_parent: as C1 POST /api/v1/data/showcase_invoice {\"name\": \"INV-C1-1\", \"owner\": \"\", \"account\": \"\", \"status\": \"draft\"} (account is a required lookup, so a body without it is refused 400, and an invoice on a churned account is refused by the no_invoice_for_churned_account rule — examples/app-showcase/src/data/objects/invoice.object.ts; QA run #27481) then POST /api/v1/data/showcase_invoice_line {\"invoice\": , \"product\": , \"quantity\": 1}; as C2 list lines, GET C1's line by id, PATCH it", "as C1: GET/PATCH their OWN line by id (the entitled side of ADR-0055 derivation)", "as auditor: list showcase_invoice_line — viewAllRecords crosses the derived scope", @@ -815,6 +821,12 @@ "date": "2026-10-31", "change": "steps[3] and steps[4] name the bodies the showcase accepts, as QA run #27481 found. steps[4]'s invoice body omitted account, a required lookup on showcase_invoice (examples/app-showcase/src/data/objects/invoice.object.ts), and an account must not be churned (rule no_invoice_for_churned_account). steps[3]'s project create names status 'planned': project_status_flow declares initialStates ['planned'] with insert among its events (project.object.ts), so any other initial status answers 400 invalid_initial_state, which #27481 hit with 'completed'. No clause changed. Read from source at base efdddc9060. Wave anchor #27377, wave-3 accuracy slice S3; finding parked in anchor comment 6493994883", "ref": "#27481" + }, + { + "revision": 9, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding: steps[3]'s project body named status 'planned' but not the account lookup, which project.object.ts declares required: true, so the create answered 400 'Account is required'. The body is now spelled in full, {name, status: 'planned', account: a seeded showcase_account id}, beside the existing initial-state note. The 400 wording is #28545's live reading; the required flag is read from source. No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, @@ -3802,7 +3814,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": 7, + "revision": 8, "priority": "P0", "surface": "api", "personas": [ @@ -3824,7 +3836,7 @@ }, "steps": [ "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_default (any body; an EXISTING set, so the refusal is of an overwrite — the showcase declares no showcase_member_access, whose read-back answers 404, and naming it tests a create-refusal instead); 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 org owner: POST /api/v1/security/permission-sets/SOME_SET/clone; POST /api/v1/meta/_migrate-stored with {} (the preview: the walled refusal answers before anything is read, preview and apply alike, packages/rest/src/meta-migrate-stored-walled-refusal.test.ts); POST /api/v1/automation with a minimal flow definition named qa_walled_flow (the walled refusal answers right after the manage_metadata gate, before the automation service is touched, packages/runtime/src/domains/automation-walled-definition-write.test.ts), then GET /api/v1/automation/qa_walled_flow, which must not find it", "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)" ], @@ -3832,11 +3844,11 @@ { "clause": "every environment-metadata and catalog write door refuses a non-operator under a walled posture — the permanent pins pass at the run subject sha (RUNNER rule 6: cite their output, never re-derive by hand)", "oracle": "test", - "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=2 FILE`, never through a bare `--`); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", + "verify": "run each file named in automated.ref with its package's vitest (`pnpm --filter PKG exec vitest run --maxWorkers=1 FILE`, one package at a time, never through a bare `--`: RUNNER environment facts hold the Tier 1 vitest pins under this container's memory limit only at --maxWorkers=1); every named file reports ≥1 test and 0 failed. A file that reports 0 tests or is not found is NOT a pass — it is a stale ref, fix the item", "evidence": "the vitest summary line per pin file, with the package name and the subject sha" }, { - "clause": "each org-owner write on the walled boot answers 403 (FORBIDDEN on /meta, PERMISSION_DENIED on the dispatcher /packages and /security doors) and stores nothing (GET of the item afterwards is unchanged/404)", + "clause": "each org-owner write on the walled boot answers 403 (FORBIDDEN on /meta, POST /meta/_migrate-stored included; PERMISSION_DENIED on the dispatcher /packages, /security and /automation doors, POST /automation included) and stores nothing (GET of the item afterwards is unchanged/404; the automation read-back finds no qa_walled_flow)", "oracle": "api", "verify": "status + error.code per call + a read-back", "evidence": "one line per call + the read-backs" @@ -3906,6 +3918,12 @@ "date": "2026-10-31", "change": "Three findings of the wave-3 sweep. steps[0] PUT /api/v1/meta/permission/showcase_member_access named a set the showcase does not have (QA runs #27409 and #27481: the read-back answers 404), so the refusal it scored was a create-refusal; it now names the existing showcase_member_default (examples/app-showcase/src/security/permission-sets.ts), so the refused write is an overwrite and acceptance[1]'s read-back is 'unchanged', not 404. fixtures.requires[0] keeps `pnpm dev -- --fresh` and adds the shared-checkout boot: a default os dev compiles objectstack.config.ts into the app's dist/objectstack.json (packages/cli/src/commands/dev.ts, compile flag default on), so concurrent runners boot `objectstack dev --fresh --no-compile -p PORT`, which serves the existing artifact (still compiled when missing; warns when stale) — #27481's own boot line. knownGaps[1] is added for the install-local leg #27481 parked as a gap: it rests on the wall-environment-metadata-refusal dogfood pin (acceptance[0]), and a live leg boots from a scratch copy per RUNNER.md's install-isolation fact. No clause changed. Read from source at base efdddc9060. Wave anchor #27377, wave-3 accuracy slice S3; finding parked in anchor comment 6488948228 (C2a) and 6493994883 (C1)", "ref": "#27481" + }, + { + "revision": 8, + "date": "2026-11-01", + "change": "Two QA run #28545 (W3-C1R) findings. acceptance[0].verify prescribed --maxWorkers=2 against RUNNER's memory fact (the Tier 1 vitest pins hold under the container's limit only at --maxWorkers=1); it now says --maxWorkers=1, one package at a time, as cli.organization-ownership-ceremony acceptance[0] does. The title names _migrate-stored and POST /automation, but no step drove them: steps[1] adds both doors as the org owner, and acceptance[1] names their codes, 403 FORBIDDEN (the /meta door, pinned by meta-migrate-stored-walled-refusal.test.ts) and 403 PERMISSION_DENIED (the dispatcher, pinned by automation-walled-definition-write.test.ts), the codes #28545 read live. acceptance[1] gains two calls; no clause weakened. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/ai.json b/docs/qa/platform-checklist/areas/ai.json index 34dc9ccf21b..05a4f9760ea 100644 --- a/docs/qa/platform-checklist/areas/ai.json +++ b/docs/qa/platform-checklist/areas/ai.json @@ -233,7 +233,7 @@ "title": "MCP stdio auto-start fails closed without OS_MCP_STDIO_API_KEY; with a member-bound key, reads AND aggregates honor RLS/FLS and revocation bites on the next call", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P0", "surface": "cli", "personas": ["restricted member API key (for the RLS half — on showcase, a contributor-bound member)", "a second restricted member API key for the FLS half (on showcase, a client_liaison-bound member — see fixtures.requires)", "admin API key (for the full-row comparison)"], @@ -257,7 +257,7 @@ "run the same two reads over REST as the same member (Bearer the same key) and as the admin key; compare. On `os start` the stdio transport answers tool calls before the HTTP listener is up, so poll GET /api/v1/health until 200 before the first REST leg; a refused connection there is the boot still coming up, not a result", "aggregate parity: over MCP stdio as the member, aggregate_records the OWD-private object with a count grouped by a readable field ({ aggregations: [{ function: 'count', alias: 'n' }], groupBy: [''] }); run the SAME aggregate over REST as the member and as the admin; compare the per-group counts", "FLS aggregate fail-closed: over MCP stdio as the member, aggregate_records with an aggregation (or a groupBy) naming the field the member cannot read (the masked field above); capture the refusal and confirm no numeric result is returned", - "revoke the member key while the stdio session is up (as the admin, PATCH /api/v1/data/sys_api_key/ {\"revoked\": true}); issue one more read on the SAME session; capture the tool result and parse its error envelope (code and status, not only the prose)", + "revoke the member key while the stdio session is up (as the admin from a SIGNED-IN SESSION, PATCH /api/v1/data/sys_api_key/ {\"revoked\": true}, or revoke it on Setup's API Keys page. Never with the admin's API key: a sys_api_key write from a principal an API key admitted is refused 403 PERMISSION_DENIED 'Changing an API key requires a signed-in session…' and nothing is written, packages/core/src/security/api-key-management-credential.ts); issue one more read on the SAME session; capture the tool result and parse its error envelope (code and status, not only the prose)", "start once more with stdio enabled and the key revoked in the previous step; capture the refusal to start (the lifecycle arm of the up-front admission)", "start once more with stdio NOT enabled and no key set, again with `--log-level info` (the not-auto-started line is logged at INFO, like the principal-bound line of step 2); confirm a clean boot (the key is only demanded when stdio is on)" ], @@ -359,6 +359,12 @@ "date": "2026-10-28", "change": "steps[4] waits for the HTTP listener, and automated.ref names two corroborating files. Checklist-accuracy findings of the 18.2.0 wave 2 (QA runs #26481, priority:P0, and #26468, Tier 1 half A, both at 756220c4; wave anchor #25807, slice B). (1) #26481: on `os start` stdio answered tool calls before the HTTP listener was up, so the REST comparison leg met a refused connection; the step now polls health 200 first. (2) #26468: plugin.test.ts's 'Transport not auto-started' cases and plugin-execution-context.test.ts (generic 'no longer valid' refusal) corroborate without scoring; the ref names both, and its statement that no packages/mcp test asserts API_KEY_REVOKED still holds. Re-read at base fd246c22 (grep API_KEY_REVOKED over packages/mcp/src: no hit; plugin-execution-context.test.ts, its /no longer valid/ case). No clause changed", "ref": "#25807" + }, + { + "revision": 9, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding: steps[7]'s revoke 'as the admin' failed when the admin used an API key, which answers 403 PERMISSION_DENIED 'Changing an API key requires a signed-in session' since #27692 (api-key-management-credential.ts: every non-read sys_api_key operation from an API-key credential is refused). The step now says to revoke from the admin's signed-in session, or Setup's API Keys page, and names that refusal. No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 9587ebad67f..6f53c3ad174 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -396,7 +396,7 @@ "title": "Data-API query contract: every filter operator gives known answers; $-params, select, sort, expand honored; malformed input 400s with the exact code", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P0", "surface": "api", "personas": [ @@ -478,7 +478,8 @@ } ], "negative": [ - "silent WIDENING is the catastrophic failure mode (#3899): a malformed query body must never degrade into an unfiltered 200 full read. A mistyped clause (e.g. {\"limit\": \"ten\"}) answers 400 VALIDATION_FAILED at the request schema; an UNDECLARED top-level key (e.g. {\"not_a_query_key\": {…}}) is lowered as an implicit field filter and answers 400 INVALID_FIELD. ⛔ Do not probe with {\"filter\": {…}}: `filter` is a DECLARED transport spelling of `where` and is served. Verify by asserting each probe's status and code AND that no response carries the full table" + "silent WIDENING is the catastrophic failure mode (#3899): a malformed query body must never degrade into an unfiltered 200 full read. A mistyped clause (e.g. {\"limit\": \"ten\"}) answers 400 VALIDATION_FAILED at the request schema; an UNDECLARED top-level key (e.g. {\"not_a_query_key\": {…}}) is lowered as an implicit field filter and answers 400 INVALID_FIELD. ⛔ Do not probe with {\"filter\": {…}}: `filter` is a DECLARED transport spelling of `where` and is served. Verify by asserting each probe's status and code AND that no response carries the full table", + "#26644 — an empty projection or a count flag that is neither true nor false is refused, never read as the absent default (every column, or a reported total). GET /api/v1/data/showcase_account?$select= (and ?$select=%20, ?$select=, with a lone comma, ?select= and ?fields=) answers 400 VALIDATION_ERROR, \"Query parameter '$select' names no field\" naming the spelling sent; GET ?$top=2&$count=maybe (and ?$count=1, ?count=) answers 400 VALIDATION_ERROR, \"Query parameter '$count' must be true or false\"; POST /api/v1/data/showcase_account/query {\"fields\": []} (and {\"select\": \"\"}) answers 400 VALIDATION_FAILED at the body schema, with fields[0] on the key, and {\"limit\": 2, \"count\": \"maybe\"} answers 400 VALIDATION_FAILED; GET /api/v1/data/showcase_account/ID?select= answers 400 VALIDATION_ERROR naming select. No probe returns rows: a 200 carrying every column or a total is the widening this contract closed (packages/metadata-protocol/src/protocol.ts projectionFieldsValue and countFlagValue; door pins packages/rest/src/data-empty-projection-count-door.test.ts and packages/runtime/src/domains/data-empty-projection-count.test.ts)" ], "variants": [ "op:eq", @@ -579,7 +580,8 @@ "change": "automated.ref names the HTTP pin for acceptance[4] and says what two named pins do and do not reach; the variants gain the six AST_OPERATOR_MAP operators the list omitted; steps[1] names the comparand-less null spellings. Checklist-accuracy findings of the 18.2.0 wave 2 (QA run #26468, release-18.2.0-w2-tier1-a, at 756220c4; wave anchor #25807, slice A): showcase-expand-crud-gate.dogfood.test.ts drives acceptance[4] over HTTP and was missing (so 'no clause has a pin at its oracle's layer' was false); request-schema-gate.conformance.test.ts has no collection-operator case; engine-filter-array-lowering asserts the engine's INVALID_FILTER, not the POST door's code; and AST_OPERATOR_MAP serves icontains, ilike, is_empty/is_not_empty and before/after beyond the list. Re-read at base e4ba846f (packages/spec/src/data/filter.zod.ts AST_OPERATOR_MAP). acceptance[8] now asks a verdict of six more rows", "ref": "#25807" }, - { "revision": 8, "date": "2026-10-31", "change": "steps[1] names the prefix-form compound AST and steps[2] the case contract of the substring operators. Checklist-accuracy findings of QA run #27409 (18.3.0 sweep, wave 3, Tier 1 half A, at c4c515ad; sweep anchor #27377): the infix compound [cond, 'and', cond] answered 400 while the step showed only the single-condition form, and the substring operators matched case-exactly on the SQL driver while the step gave no case note. Re-read at base 488b3ed0: packages/spec/src/data/filter.zod.ts lowerFilterAST and the CONTAINS / NOT_CONTAINS / STARTS_WITH / ENDS_WITH / ICONTAINS descriptions. No clause changed", "ref": "#27409" } + { "revision": 8, "date": "2026-10-31", "change": "steps[1] names the prefix-form compound AST and steps[2] the case contract of the substring operators. Checklist-accuracy findings of QA run #27409 (18.3.0 sweep, wave 3, Tier 1 half A, at c4c515ad; sweep anchor #27377): the infix compound [cond, 'and', cond] answered 400 while the step showed only the single-condition form, and the substring operators matched case-exactly on the SQL driver while the step gave no case note. Re-read at base 488b3ed0: packages/spec/src/data/filter.zod.ts lowerFilterAST and the CONTAINS / NOT_CONTAINS / STARTS_WITH / ENDS_WITH / ICONTAINS descriptions. No clause changed", "ref": "#27409" }, + { "revision": 9, "date": "2026-11-01", "change": "QA run #28545 (W3-C1R) finding: the #26644 contract (an empty projection or an unrecognised $count answers 400) had no acceptance or negative row, so its recheck rested on a dispatcher note. negative[1] adds it, spelled from source: protocol.ts projectionFieldsValue / countFlagValue (400 VALIDATION_ERROR naming the spelling) and the body schema's 400 VALIDATION_FAILED, with the probe set of data-empty-projection-count-door.test.ts. #28545 observed the #26644 fix holding. A negative is added; nothing is removed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", "ref": "#28545" } ] }, { @@ -1676,7 +1678,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": 10, + "revision": 11, "priority": "P0", "surface": "api", "personas": [ @@ -1744,9 +1746,9 @@ "evidence": "post-restart traces + the row read" }, { - "clause": "a disabled `type: flow` action is refused by its OWN switch, ahead of the type branch — the refusal is ACTION_DISABLED for the action's name, independent of anything the target flow's own ledger row says", + "clause": "a disabled `type: flow` action is refused by its OWN switch, ahead of the type branch — the refusal is 409 ACTION_DISABLED on the action's own route (POST /api/v1/actions/showcase_task/showcase_bulk_reassign) with the turned-off sentence of acceptance[0], which names no action (describeDisabled ignores the name, packages/objectql/src/action-activation.ts), independent of anything the target flow's own ledger row says, and the target flow is left untouched", "oracle": "api", - "verify": "step-8: 409 ACTION_DISABLED naming showcase_bulk_reassign; the automation service is never consulted for the run", + "verify": "step-8 on /actions/showcase_task/showcase_bulk_reassign: status 409, body.error.code ACTION_DISABLED and body.error.message the turned-off sentence (first sentence \"This action is turned off for this deployment.\", the Setup › Packaged automation remedy). Do not look for the action's name in it: the sentence names none, and the live pin's expectDisabledSentence ignores the name too. The automation service is never consulted for the run, so showcase_reassign_wizard records no run from it", "evidence": "the trace" } ], @@ -1841,6 +1843,12 @@ "date": "2026-10-30", "change": "acceptance[0]'s clause and verify respelled for #27842: the disabled-action sentence is now written for the end user who clicked (what happened, that an administrator can turn the action on again under Setup › Packaged automation, or use another action) and names no system table and no ADR; the engine's describeDisabledAction writes the ledger table, ADR-0126 §8 and the ACTION_DISABLED code to its log line instead. The code and the 409 are unchanged, so every other clause stands; the clause's 'naming the ledger' was the only text that turned false. The dogfood pin's sentence helper and packaged-activation-ledger-reach.dogfood.test.ts's /ACTION_DISABLED|is disabled/ are flipped in the same change (the latter now reads the code alone)", "ref": "#27842" + }, + { + "revision": 11, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding: acceptance[6] (clause and verify) expected ACTION_DISABLED 'naming showcase_bulk_reassign', but since #27842 the sentence names no action (action-activation.ts describeDisabled returns describeTurnedOffPackagedItem('action'), and packaged-action-disabled-dispatch-live.dogfood.test.ts expectDisabledSentence ignores its action argument). The clause now asks 409 ACTION_DISABLED with the turned-off sentence on the /actions/showcase_task/showcase_bulk_reassign route, and the target flow untouched. The status, the code and the own-switch ordering are kept; only the expectation that could never hold is replaced. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index e740579b77f..b73e8e7b19b 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -1119,7 +1119,7 @@ "title": "A non-admin reaches the full Approvals Inbox from the account app's Approvals entry", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P0", "surface": "browser", "personas": ["a NON-ADMIN business user who is a pending approver and has no Setup access"], @@ -1132,10 +1132,10 @@ ] }, "steps": [ - "build the console from this checkout (`pnpm console:build`) and keep its ready line; boot the showcase on its own port and DB (dogfood §0)", + "build the console from this checkout (`pnpm console:build`), or take the build the dispatcher made of this checkout at the run SHA, and keep its ready line; boot the showcase on its own port and DB (dogfood §0); then compare the entry chunk the served /_console/ page loads with packages/console/dist's (acceptance[0])", "grep the BUILT bundle for the registration key: `grep -o 'approvals:inbox' packages/console/dist/assets/index-*.js` — the entry chunk must carry the register call, because the unit pin (#7231) stubs the page at its module boundary and does not assert main.tsx performs the side-effect import", "sign in as the non-admin persona; GET /api/v1/meta/app and record which apps the server returns for this session", - "open the account app and click the Approvals entry in the Inbox group; screenshot", + "open the account app from the avatar (user) menu's Profile item — it opens /apps/account/component/account/profile_card, which QA run #28545 read as /apps/com.objectstack.account/component/account/profile_card. Not from the app switcher: the account app declares hidden: true (packages/platform-objects/src/apps/account.app.ts), so for this persona the switcher offers only Showcase, Back to home and All apps. Then click the Approvals entry in the Inbox group; screenshot", "read the resulting URL and the rendered surface (tabs, list rows, keyboard hint bar)", "open a request from its inbox row — it navigates to the request's served record page, /apps/APP/sys_approval_request/record/ID — and read the rendered action set; execute one decision and re-read the request", "cold-load the same request via ?request= on this component route and read the URL it lands on", @@ -1145,8 +1145,8 @@ { "clause": "the registration survives BUNDLING — the built dist, not just source, contains the approvals:inbox component registration", "oracle": "build", - "verify": "`pnpm console:build` ran in this checkout at the run's framework SHA and printed its ready line (`✓ @objectstack/console dist ready (… KB) from ui/apps/console`) AND the built entry chunk contains the `approvals:inbox` register call; a source-only grep does not satisfy this clause", - "evidence": "the framework SHA + the console:build ready line + the grep hit with surrounding bundle context" + "verify": "`pnpm console:build` ran in this checkout at the run's framework SHA and printed its ready line (`✓ @objectstack/console dist ready (… KB) from ui/apps/console`) — run by the runner, or by the dispatcher that prepared the checkout (RUNNER.md: one container builds one tree at a time), whose ready line the run record then cites — AND the server serves that build: the entry chunk the served /_console/ page loads (index-HASH.js) is the one in packages/console/dist, same name and same bytes (QA run #28545 compared index-RFCp05oY.js) — AND that built entry chunk contains the `approvals:inbox` register call; a source-only grep does not satisfy this clause", + "evidence": "the framework SHA + the console:build ready line (and who ran it) + the served-entry comparison + the grep hit with surrounding bundle context" }, { "clause": "the entry renders the FULL inbox — tabs (en: My Pending / Submitted by me / All; zh-CN: 待我审批 / 我发起的 / 全部) and the request list — not a component-not-found empty state", @@ -1228,6 +1228,12 @@ "date": "2026-10-29", "change": "fixtures.requires[2] no longer says a persona-created invoice without `owner` lands and reads 404 to its creator: the showcase contributor set now carries the ownership check on insert as well as update (invoice_owner_on_create beside invoice_owner_immutable, examples/app-showcase/src/security/permission-sets.ts), so such a create is refused 403 PERMISSION_DENIED with nothing written. The requirement that the persona own the invoice is unchanged", "ref": "claude/harden-invoice-owner-check-on-create" + }, + { + "revision": 8, + "date": "2026-11-01", + "change": "Two QA run #28545 (W3-C1R) findings. steps[3] did not say how to reach the account app: it is hidden from the app switcher (account.app.ts hidden: true), which offered the persona only Showcase / Back to home / All apps, and is entered from the avatar menu's Profile item (AppHeader.tsx, /apps/account/component/account/profile_card, which #28545 read under the com.objectstack.account segment). acceptance[0] asked the runner to run pnpm console:build itself; it now also accepts the dispatcher's build of the same checkout at the run SHA, and adds a served-entry check (the chunk /_console/ loads is the dist's), so the clause still proves the bundle under test is this checkout's. steps[0] and the evidence follow. No clause weakened: the build, SHA and grep requirements stand, and one check is added. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 75b68134068..7bdedaf3558 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -44,7 +44,8 @@ "knownGaps": [ "no magic-link mail and no email.attachment.reclaim job: 18.0.0-next.5's served composition mounts no magic-link door, and identity mail carries no attachment, so acceptance[5]'s magic-link and reclaim arms read nothing here (QA run #24838 planted both by hand). All four mail jobs are `pending`; none is `running` or `completed`, so the 'completed jobs unchanged' arm reads the two organization-invitation jobs only", "the second boot keeps delivering: the mail settings still name the closed port, so it retries the re-pointed sends, and each moved sys_identity_email row's attempt_count and updated_at advance (2 → 3 in the #26624 re-drive). Read 'equal over the columns both tables hold' at the apply, before the second boot, and after it on every column but those two; that the retry reads the row from sys_identity_email is itself evidence the job was re-pointed", - "the database carries no sys_http_delivery row: on 18.0.0-next.5 the organization-less declared webhook refuses to fan out an organization's event ('belongs to NO organization … refusing to fan out', QA run #24838), so the ceremony's deliveriesResourced is 0 here and the delivery leg is the post-upgrade PATCH alone" + "the database carries no sys_http_delivery row: on 18.0.0-next.5 the organization-less declared webhook refuses to fan out an organization's event ('belongs to NO organization … refusing to fan out', QA run #24838), so the ceremony's deliveriesResourced is 0 here and the delivery leg is the post-upgrade PATCH alone", + "the restored telemetry sibling holds 9 sys_automation_run rows, two of them showcase_task_follow_up runs paused at the `hold` wait node on the two fixture tasks (status paused, node_type wait; packages/qa/dogfood/test/upgrade-harness/fixture-18.0.0-next.5/telemetry.sql). Their PT1M timers fell due one minute after the dump was generated (hold.waitUntil 2026-10-27T16:43Z; examples/app-showcase/src/automation/flows/index.ts TaskFollowUpFlow), so every boot that serves the restore resumes both, and both fail at the `remind` notify node: the two tasks carry assignee null, and the node's recipient expression then adds no recipient. QA run #28545 (at eeec88d1) read the two paused rows replaced by run_run_ history rows on the second boot: the table's count held 9 → 9 while the two pre-existing ids vanished. Count sys_automation_run like any table; do not expect its paused ids to survive the second boot, and do not score their absence against the apply (cli.organization-ownership-ceremony acceptance[5])" ] } }, @@ -2073,7 +2074,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": 7, + "revision": 8, "priority": "P0", "surface": "cli", "personas": [ @@ -2112,7 +2113,7 @@ "evidence": "the vitest summary line per pin file, with the package name and the subject sha" }, { - "clause": "a bare run writes the plan file and changes nothing: exit 0, the plan JSON names every table's fate, and the primary DB file's checksum is unchanged — and under `--dev`, where a telemetry sibling sits beside the primary, the sibling's tables and per-table row counts are unchanged (its file checksum is no oracle: the plan's open checkpoints the `-wal` an earlier refused boot left, which moves the hash with no schema or row change); `--apply` without --plan-file is refused before the boot (exit 1, plan-file-required)", + "clause": "a bare run writes the plan file and changes nothing: exit 0, the plan JSON names every table's fate, and the primary DB file's checksum is unchanged — and under `--dev`, where a telemetry sibling sits beside the primary, the sibling's tables and per-table row counts are unchanged (its file checksum is no oracle: the plan's open checkpoints the `-wal` an earlier refused boot left, which moves the hash with no schema or row change); `--apply` without --plan-file is refused before the boot (exit 1; the literal plan-file-required is the `--json` run's reason, {\"error\": \"apply_refused\", \"reason\": \"plan-file-required\", …}, while a text run prints \"✗ Refused: --apply executes only a plan you reviewed. …\" on stdout, which does not carry the literal — packages/cli/src/commands/migrate/organization-ownership.ts, the refuse helper)", "oracle": "build", "verify": "exit codes + the plan file's kind/ceremony/inventoryDigest + the primary file's checksums before/after, and under --dev a row-level read (table list and per-table counts) of the telemetry sibling before/after", "evidence": "the plan excerpt, the refusal line, the checksum pair" @@ -2138,7 +2139,7 @@ { "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", + "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. On the cli-18-prerelease-db restore, sys_automation_run in the telemetry sibling keeps its count across the second boot but not two of its ids: the recipe's knownGaps[3] says why, and those two ids are not data the apply lost. 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" } ], @@ -2203,6 +2204,12 @@ "date": "2026-10-31", "change": "six checklist-accuracy findings and one fixture gap, from QA runs #27409 and #27481. fixtures.requires[1] gives the v17 recipe: a create-objectstack@17.7.0 blank scaffold booted once by its own `os dev`, installed with --legacy-peer-deps, since no 17.x showcase is published. acceptance[4] driven on the 18.0 pre-release restore scores partial. acceptance[1]: the primary file's checksum is the oracle. Under --dev the telemetry sibling's hash moves with no row change, because the plan checkpoints a -wal the refused boot left (#27481), so the sibling is judged by a row-level read. acceptance[3]: serve.ts prints the refusal's message only, never the code (#27409, #27481), so the code is the pins' evidence, as in platform-core.v18-boot-refusals acceptance[1]. The named command carries --dev on a development serve (ceremonyCommand). steps[5]: the dump's reset token expired an hour after generation (#27481, 400 INVALID_TOKEN), so the cli-18-prerelease-db recipe gains step 3, an UPDATE that re-dates sys_verification.expires_at after the restore. Under a production `os serve` the delivery row lands in the primary, because resolveTelemetryDbPath composes no sibling outside dev (#27409). No clause weakened: the sibling is still judged, by its rows. Read from source at base 22ce3d13fe. Wave anchor #27377, wave-3 accuracy slice S7 (#28341)", "ref": "#27409" + }, + { + "revision": 8, + "date": "2026-11-01", + "change": "Two QA run #28545 (W3-C1R) findings. acceptance[1] named plan-file-required without saying which output carries it: it is the --json refusal's reason, and the text run prints the 'Refused: --apply executes only a plan you reviewed' line through printError (organization-ownership.ts refuse). acceptance[5].verify now points at the cli-18-prerelease-db recipe's new knownGaps[3]: the second boot resumes the dump's two paused showcase_task_follow_up wait runs, so sys_automation_run holds its 9 → 9 count while two pre-existing ids vanish. The paused rows, their elapsed PT1M timers and the null assignees are read from the committed dump; the replacement by run_run_ history rows is #28545's live reading. No clause weakened. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index bf76a576b9d..e58b669c790 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1837,7 +1837,7 @@ "title": "A shipped driver's datasource credential can never be authored inline NOR read back: every inline-credential door refuses at publish, and BOTH read doors redact, including legacy alias spellings already sitting in stored rows. A plugin driver's config is its author's to keep credential-free; the platform redacts only its fixed credential spellings there and does not guess", "since": "v17", "status": "active", - "revision": 7, + "revision": 8, "priority": "P0", "surface": "mixed", "personas": ["seeded admin", "authenticated non-admin", "anonymous"], @@ -1852,11 +1852,12 @@ "`encryptionKey` (turso) is RETIRED since #25050 (it encrypted nothing in any mode): the parse refuses it on EVERY turso datasource, remote or local, on config.encryptionKey with the removal prescription, so a write refusal is the correct expectation everywhere. A stored row from before the retirement may still carry it; such a row is reachable only by a direct sys_metadata write, and its value must still be redacted on read through both doors (packages/spec/src/data/datasource-credential-redaction.ts#RETIRED_CREDENTIAL_KEYS). Plant it beside a file: url: the load-path conversion (turso-config-encryption-key-removed) deletes it beside a remote url on the datasource loaders, and leaves a local-url value for its author.", "acceptance[1] (the credential-bound datasource actually connects) and acceptance[6] (judged by a connect probe after the echoed-config round-trip) need a reachable, credential-protected database of a SHIPPED driver (postgres, mysql or mongodb — the driver id since 18; an authored `mongo` is refused by name, packages/spec/src/data/datasource.zod.ts). Stock showcase provides none, but the QA container has a PostgreSQL 16 install (/usr/lib/postgresql/16/bin, with a `postgres` system user), and QA run #24575 proved a recipe on it that scored both clauses pass (#24615 reused it): as `postgres`, `initdb -A scram-sha-256` into a scratch data directory, then `pg_ctl … -o '-p PORT -k SOCKETDIR -c listen_addresses=127.0.0.1' start`; create a login role WITH a password and a database it owns; bind it through POST /api/v1/datasources with the password in the top-level `secret`, never in `config`, so the door binds external.credentialsRef. Every parent directory of the data and socket directories must be traversable by `postgres`, and a socket path may not exceed 107 bytes: a session scratchpad fails both (its parent chain is root 0700, and its socket path is too long). QA run #26481 ran the database from a /tmp/RUN-pg data directory with -c unix_socket_directories='' (TCP only, 127.0.0.1). A failed connect states its real cause only in the create response's `statusReason` (packages/services/service-datasource/src/datasource-admin-service.ts); POST /api/v1/datasources/NAME/test then answers 'no registered driver to introspect', which names no cause, so read the create response first. Runs before #24575 recorded both clauses blocked(environment) (#21784, #21845). CI has postgres and mysql only inside the Temporal Conformance (live PG + MySQL) job (.github/workflows/ci.yml services postgres:16 and mysql:8.0), which runs no checklist item, and has no mongodb anywhere. ⛔ Never score either clause from a successful publish alone, which proves the paved path is accepted, not that it connects. A run that cannot start the database still reads the stored-credential half of acceptance[6] (after the PATCH, the admin door still reports hasSecret and the bound secret handle is unchanged); record that as a partial reading, separate from the connect half", "the plugin-driver boundary is RULED (#21921, docs note in content/docs/data-modeling/drivers.mdx): for a driver the platform ships no contract for, the platform neither validates config nor guesses which of its keys are credentials. Keeping credentials out of a plugin driver's config, through external.credentialsRef, is the plugin author's responsibility. Only the platform's FIXED credential spellings are redacted there by name (acceptance[5]). A credential under any other key of a plugin driver's config being stored and served to administrators as written is that boundary, never a finding", - "no datasource read door serves the mask literal (packages/spec/src/data/secret-mask.ts#SECRET_MASK is read by the encrypted-field read path and the settings REST boundary, not here): the admin door OMITS each withheld credential key and names it in redactedConfigKeys beside hasSecret, and the metadata door serves the external.credentialsRef handle. So the round-trip acceptance[6] drives carries no mask, and what it exercises is the carry-forward of the withheld keys on PATCH (packages/services/service-datasource/src/datasource-config-redaction.ts#restoreRedactedConfig). QA run #24575 also measured a hand-written literal mask: PATCH /api/v1/datasources/NAME with `secret` set to it is accepted 200 and rewraps the stored secret to the mask (the reconnect then fails). No read door serves that string, so it is not the echoed case acceptance[6] and negative[5] score; it is held for the maintainer on #24579 §7, where no contract declares it. ⛔ Score it under neither" + "no datasource read door serves the mask literal (packages/spec/src/data/secret-mask.ts#SECRET_MASK is read by the encrypted-field read path and the settings REST boundary, not here): the admin door OMITS each withheld credential key and names it in redactedConfigKeys beside hasSecret, and the metadata door serves the external.credentialsRef handle. So the round-trip acceptance[6] drives carries no mask, and what it exercises is the carry-forward of the withheld keys on PATCH (packages/services/service-datasource/src/datasource-config-redaction.ts#restoreRedactedConfig). QA run #24575 also measured a hand-written literal mask: PATCH /api/v1/datasources/NAME with `secret` set to it is accepted 200 and rewraps the stored secret to the mask (the reconnect then fails). No read door serves that string, so it is not the echoed case acceptance[6] and negative[5] score; it is held for the maintainer on #24579 §7, where no contract declares it. ⛔ Score it under neither", + "Two credential routes beside external.credentialsRef are not driven by any step here; a run records them as not covered by this item, never as pass, and never reads them as holes in the refusal matrix. (a) The code author's route (ADR-0015, the note under §10, #28208): a datasource defined in code reads its credential at connect from the environment variable derived from its name, OS_DATASOURCE_NAME_PASSWORD (`warehouse` reads OS_DATASOURCE_WAREHOUSE_PASSWORD), only on a code-origin connect and only while the host switch OS_DATASOURCE_ENV_CREDENTIALS is on, which the CLI host stamps and every other composition leaves off (packages/services/service-datasource/src/datasource-env-credential.ts). The inline-credential and URL-userinfo refusals, and the datasource-level password guidance, now end with that route's sentence (CODE_DEFINED_CREDENTIAL_ROUTE, packages/spec/src/data/driver/common.zod.ts), so acceptance[0] reads it as part of the prescription. (b) The connector channel (ADR-0015 D7): an API-backed datasource may borrow a declared connector instance's credential through external.connector, under three refusals — connector beside credentialsRef; connector on a built-in database driver or on a managed datasource; a connector the same stack does not declare with a provider and an auth other than none (packages/spec/src/data/datasource.zod.ts#ExternalDatasourceSettingsSchema, packages/spec/src/stack.zod.ts#collectDatasourceConnectorErrors)" ] }, "steps": [ - "enumerate the inline-credential refusal doors from packages/spec/src/migrations/entries/semantic/ — at minimum the config-key door, the URL userinfo door, the URL query-parameter door, the driver-options door (driver `mongodb`: an authored `mongo` is refused by name since 18, so a probe spelled with it meets that refusal instead of the credential one), the placeholder door, and the connector inline-authentication door", + "enumerate the inline-credential refusal doors from packages/spec/src/migrations/entries/semantic/ — at minimum the config-key door, the URL userinfo door, the URL query-parameter door, the driver-options door (driver `mongodb`: an authored `mongo` is refused by name since 18, and the two publish doors report a `mongo` probe differently. The runtime admin door, POST /api/v1/datasources, judges config against the driver contract first, where `mongo` still resolves to the mongodb contract (packages/spec/src/data/driver/config-registry.zod.ts resolveDriverId, retired aliases; packages/services/service-datasource/src/datasource-admin-service.ts assertValidConfig before assertValidRecord), so it refuses on config.password alone; the build door (os build) parses the whole datasource and names the driver refusal beside the credential one (QA run #28545). Spell the probe `mongodb` so both doors score the credential refusal), the placeholder door, and the connector inline-authentication door", "for EACH door, author a datasource carrying the credential in that position and attempt to publish it (os build, then the runtime admin door)", "author the same datasource with the credential bound through `external.credentialsRef` (or the connection form) and confirm it publishes AND connects. A federated spelling (schemaMode other than 'managed', e.g. 'external') must also carry an `external` block, `external: {}` at minimum, or the parse answers 400 on the path `external` (\"schemaMode='external' requires 'external' settings.\", packages/spec/src/data/datasource.zod.ts#DatasourceSchema). That 400 is the shape refusal, not a credential refusal: read the issue path before scoring it", "read the published datasource back through the datasource-admin door (GET /api/v1/datasources)", @@ -1906,7 +1907,7 @@ { "clause": "the echoed-config write guard holds: writing the read's config back verbatim (it carries no mask: the withheld credential keys are absent from it, knownGaps[4]) is treated as 'unchanged' and does NOT overwrite or delete the stored credential", "oracle": "api", - "verify": "read a datasource through the admin door, PATCH /api/v1/datasources/ with the response's config back unmodified (that door updates by PATCH only — GET, POST, PATCH, DELETE; a PUT answers 405), then probe connectivity — the connection must still work. A form round-trip that destroys the stored credential is the ADR-0100 §B3 defect", + "verify": "read a datasource through the admin door, PATCH /api/v1/datasources/ with the response's config back unmodified (that door updates by PATCH only: /api/v1/datasources/NAME serves GET, PATCH and DELETE, and a PUT answers 405 with Allow: DELETE, GET, HEAD, PATCH; POST belongs to the collection, /api/v1/datasources — packages/services/service-datasource/src/admin-routes.ts), then probe connectivity — the connection must still work. A form round-trip that destroys the stored credential is the ADR-0100 §B3 defect", "evidence": "the round-trip + the post-round-trip connect probe" }, { @@ -1940,7 +1941,8 @@ "sibling coverage this item deliberately does NOT duplicate: integration-system.datasource-admin-lifecycle (admin-door lifecycle, 'secret never echoes'), platform-core.settings-hub-roundtrip (settings secrets), records-forms.encrypted-field-behavior (ADR-0100 field-level masking)", "packages/spec/src/data/driver/common.zod.ts#CANONICAL_CREDENTIAL_KEYS (the fixed canonical spellings redacted by name for a driver with no contract)", "packages/spec/src/data/datasource-credential-redaction.ts#redactableConfigKeys (for a contractless driver: the canonical keys + the former aliases; URL credentials are scrubbed by value)", - "content/docs/data-modeling/drivers.mdx (the plugin-driver boundary: the platform does not guess which config keys hold credentials; keeping secrets out of config is the plugin author's responsibility) · #21921 (the ruling; #21840 closed as not planned) · #21927 (the docs note)" + "content/docs/data-modeling/drivers.mdx (the plugin-driver boundary: the platform does not guess which config keys hold credentials; keeping secrets out of config is the plugin author's responsibility) · #21921 (the ruling; #21840 closed as not planned) · #21927 (the docs note)", + "packages/services/service-datasource/src/datasource-env-credential.ts + ADR-0015 D7 (the code-author env route and the external.connector channel, fixtures.knownGaps[5])" ], "history": [ { @@ -1984,6 +1986,12 @@ "date": "2026-10-27", "change": "fixtures.knownGaps[2]'s PostgreSQL recipe names where its directories can live and where a failed connect's cause is read. Checklist-accuracy findings of the 18.2.0 wave 2 (QA run #26481, priority:P0, at 756220c4; wave anchor #25807, slice A): a scratchpad data/socket directory failed (root-0700 parent chain, socket path over 107 bytes), /tmp/RUN-pg with unix_socket_directories='' worked, and the cause of a failed connect surfaced only as statusReason. Re-read at base e4ba846f (datasource-admin-service.ts statusReason; objectql engine-compatibility-api.ts, the 'no registered driver to introspect' throw). No clause changed", "ref": "#25807" + }, + { + "revision": 8, + "date": "2026-11-01", + "change": "Three QA run #28545 (W3-C1R) findings. acceptance[6].verify listed the item route's methods as 'GET, POST, PATCH, DELETE', mixing in the collection's POST; /datasources/NAME serves GET, PATCH and DELETE (admin-routes.ts), and the 405's Allow reads DELETE, GET, HEAD, PATCH. knownGaps[5] and source name the two credential routes new since c4c515ad: the code-author env route OS_DATASOURCE_NAME_PASSWORD behind OS_DATASOURCE_ENV_CREDENTIALS, whose sentence (CODE_DEFINED_CREDENTIAL_ROUTE) now ends the refusals, and the external.connector channel (ADR-0015 D7) with its three refusals. Both are described as the code and the ADR state them. steps[0] says which door names both refusals for a `mongo` probe: the runtime door judges config first and reports config.password alone (datasource-admin-service.ts), and the build door names the driver refusal too. No clause weakened. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 1af9d3d5ea7..59a74ed7580 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -310,7 +310,7 @@ "title": "Every showcase nav surface renders without page errors, and failures surface the error boundary — never a blank page", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P0", "surface": "browser", "personas": ["seeded admin (admin@objectos.ai / admin123)"], @@ -321,9 +321,9 @@ ] }, "steps": [ - "run the pinned suite: pnpm -C examples/app-showcase test:smoke e2e/showcase-smoke.spec.ts (the SURFACES array in e2e/showcase-smoke.spec.ts — all 49 showcase_app nav destinations at revision 5: pages, object lists, the three filter slices, the tabular view, dashboards, reports, the view-gallery pages). On a shared box pass SMOKE_API_URL=http://localhost:: the config signs in, drives every page and boots `serve --dev -p ` from that one URL, and reuses whatever already answers it — on the default port 3000 that can be somebody else's server. Where the browser cache holds a different chromium build than @playwright/test resolves (RUNNER.md Environment facts), pass OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium. Name the spec: the config's testDir is ./e2e (examples/app-showcase/playwright.config.ts), so a bare `test:smoke` also collects bulk-capability-gate.spec.ts and detail-shapes.spec.ts beside the pin (QA run #25812), and their results are not this item's", + "run the pinned suite: pnpm -C examples/app-showcase test:smoke e2e/showcase-smoke.spec.ts (the SURFACES array in e2e/showcase-smoke.spec.ts — 49 entries, which cover 49 of the 50 served showcase_app nav leaves; the one outside it is nav_report_index, steps[2]: pages, object lists, the three filter slices, the tabular view, dashboards, reports, the view-gallery pages). On a shared box pass SMOKE_API_URL=http://localhost:: the config signs in, drives every page and boots `serve --dev -p ` from that one URL, and reuses whatever already answers it — on the default port 3000 that can be somebody else's server. Where the browser cache holds a different chromium build than @playwright/test resolves (RUNNER.md Environment facts), pass OS_TEST_CHROMIUM_EXECUTABLE_PATH=/opt/pw-browsers/chromium. Name the spec: the config's testDir is ./e2e (examples/app-showcase/playwright.config.ts), so a bare `test:smoke` also collects bulk-capability-gate.spec.ts and detail-shapes.spec.ts beside the pin (QA run #25812), and their results are not this item's", "GET /api/v1/meta/app?id=showcase_app and extract every nav destination from the response (?id= matches the app NAME; the package-id spelling returns an empty items array and would yield zero destinations)", - "diff the served nav destinations against the SURFACES array; hand-walk any destination the suite does not cover (screenshot first, then DOM). The console draws no nav destination of its own: the report index (…/reports) renders only where an app's navigation declares a `report` entry with no `reportName` (showcase: nav_report_index in the Reports group), so it is served in meta/app and falls inside that diff like any other entry. #27525 retired the sidebar's injected 'All reports' row (objectui#13020) that QA run #25812 walked as the 50th surface", + "diff the served nav destinations against the SURFACES array; hand-walk any destination the suite does not cover (screenshot first, then DOM). The console draws no nav destination of its own: the report index (…/reports) renders only where an app's navigation declares a `report` entry with no `reportName` (showcase: nav_report_index in the Reports group), so it is served in meta/app and falls inside that diff like any other entry. #27525 retired the sidebar's injected 'All reports' row (objectui#13020) that QA run #25812 walked as the 50th surface. nav_report_index is the STANDING hand-walk: the served nav has 50 leaves (examples/app-showcase/src/ui/apps/index.ts: 25 pages, 17 objects, 5 reports, 3 dashboards) and SURFACES holds 49, so /reports is outside the suite on every run until SURFACES gains an entry for it — walk it each run and record it as hand-walked. A diff of the served entry ids reads 51: the 50 leaves plus the nav_sep_reports separator, which draws no surface (QA run #28545)", "on each hand-walked surface: wait for render, screenshot, then check for pageerror / empty
/ placeholder leaks ('no actions configured')", "chart surfaces (the chart:true entries in SURFACES — Delivery Operations, Chart Gallery, Command Center, Revenue Pulse, Hours by Status (Chart)): the suite now POLLS boundingBox against a bounded deadline (SURFACE_BUDGET_MS in the spec) instead of reading it once after a fixed wait, so a chart red here is a real finding and not the measurement window — #7569 measured the difference: 3 fresh loads each drew 5 real charts, and polling every 400 ms returned null ONLY at t = 1.5 s, the old fixed measurement instant and exactly the recharts ResponsiveContainer initial-layout window, then non-null on all 19 later samples. Read WHICH assertion failed: 'no chart SVG laid out' means the box stayed null for the whole budget; a width/height failure means the chart laid out but collapsed (#2616 D). If you hand-walk a chart surface outside the suite, poll the same way — never sleep-then-read-once", "probe the failure path: navigate to a nonexistent route under the app (/_console/apps/com.example.showcase/object_that_does_not_exist); screenshot what renders", @@ -398,6 +398,12 @@ "date": "2026-10-29", "change": "steps[2], acceptance[1] (clause, verify) corrected: #27525 stops the console injecting the 'All reports' sidebar row into every app but Studio. The report index is now a navigation entry an app declares (a `report` item with no `reportName`); showcase declares nav_report_index, so it is served in meta/app and the served-vs-SURFACES diff sees it. The hand-walk of console-added destinations is dropped because there are none. Re-read at UnifiedSidebar.tsx and examples/app-showcase/src/ui/apps/index.ts on the #27525 branch", "ref": "#27525" + }, + { + "revision": 9, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding, stale since #27525: steps[0] called SURFACES 'all 49 showcase_app nav destinations', but the served nav has 50 leaves, nav_report_index among them, and SURFACES holds 49, so /reports was a hand-walk every run with no step naming it. steps[0] now says which leaf SURFACES lacks, and steps[2] names it as the standing hand-walk and notes that the id diff reads 51 with the separator. Counts read from source (apps/index.ts leaves; SURFACES in showcase-smoke.spec.ts). Adding the entry to SURFACES is outside this checklist file. No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, @@ -591,7 +597,7 @@ "title": "The built-in apps a stock boot REGISTERS (Setup / Account) render every merged-nav destination; app-level and entry-level gates are enforced, not errored — and Studio's deliberate absence is asserted, never hunted", "since": "v15", "status": "active", - "revision": 11, + "revision": 12, "priority": "P0", "surface": "browser", "personas": [ @@ -614,9 +620,9 @@ "as admin, GET /api/v1/meta/app?id=setup and extract every merged nav destination — Setup is a SHELL of empty group anchors (packages/platform-objects/src/apps/setup.app.ts: group_overview/apps/people_org/access_control/approvals/configuration/diagnostics/integrations/advanced) filled by SETUP_NAV_CONTRIBUTIONS (setup-nav.contributions.ts) plus capability-plugin contributions (plugin-security Positions/Permission Sets, plugin-sharing Sharing Rules/Record Shares, plugin-approvals, plugin-webhooks)", "GET /api/v1/meta/app with NO query first and record the names actually served — a stock boot answers exactly showcase_app, setup, account. Then GET /api/v1/meta/app?id=account (account.app.ts — Profile + Inbox/Security/Developer groups). ⚠️ GET /api/v1/meta/app?id=studio answers 200 {\"items\":[]} on every stock boot and that is the expected result (knownGap): record the absence as an assertion and move on — do NOT install @objectstack/studio to make the step runnable", "as admin, hand-walk each destination in each app. EXPAND every nav group first: some render collapsed by default — Setup's Advanced group and Account's Developer group declare `expanded: false` (packages/platform-objects/src/apps/setup.app.ts, account.app.ts), and QA run #24557 found Setup's Configuration group collapsed as well — so a walk that never expands them never reaches their entries. Then navigate, wait for render, SCREENSHOT FIRST, then read the DOM — assert no pageerror, no 'failed to load', no blank
, no 'no actions configured' placeholder", - "Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/) opens the settings namespace form (localization/company/branding/auth/mail/storage/ai/knowledge/feature_flags), and Users / Organization / Business Units / Teams / Sessions / OAuth Applications render. User preferences and identity links are not Setup destinations: open a user from Users and confirm the user page's Preferences and Linked Accounts tabs render that user's rows", + "Setup detail: confirm each settings URL entry (nav_settings_* → /apps/setup/system/settings/) opens the settings namespace form — the 11 namespaces of a stock single-posture boot, localization/company/branding/auth/mail/sms/storage/ai/knowledge/lifecycle/feature_flags: the nav_settings_* entries of packages/platform-objects/src/apps/setup-nav.contributions.ts, plus nav_settings_branding, which packages/services/service-settings/src/settings-service-plugin.ts contributes only where no organization wall stands, and Users / Organization / Business Units / Teams / Sessions / OAuth Applications render. User preferences and identity links are not Setup destinations: open a user from Users and confirm the user page's Preferences and Linked Accounts tabs render that user's rows", "Studio detail is NOT walked from here — the app is not registered on a stock boot (knownGap), so there is no merged nav to render. Its metadata:resource lists and component surfaces (App Builder, All Metadata Types, Packages, API Console, Flow Runs, Public Forms) are reached through the console surface at /_console/studio// and are covered by area studio-authoring. What THIS item asserts about Studio is the absence itself, per clause 6", - "Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (nav_account_approvals, the approvals:inbox component, served as …/component/approvals/inbox — the Approvals Inbox, not the sys_approval_request grid; account.app.ts), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) and Connect an Agent (nav_connect_agent) each render — 8 Account destinations and 48 Setup destinations for the admin on a stock single-org showcase boot, re-measured at 3fd80337 for #28338 (GET /api/v1/meta/app leaves, which match the nav source minus its gates; QA run #27481 read 44 and 8, QA run #24557 read 42 at 5b14b7f3, 45 on 2026-10-01, and #9453 recorded 43 and 7). Recount from the payload each run", + "Account detail: confirm Profile (account:profile_card), Notifications (sys_inbox_message/mine), Approvals (nav_account_approvals, the approvals:inbox component, served as …/component/approvals/inbox — the Approvals Inbox, not the sys_approval_request grid; account.app.ts), Linked Accounts (sys_account), Active Sessions (sys_session/mine), API Keys (sys_api_key/mine), OAuth Applications (sys_oauth_application/mine) and Connect an Agent (nav_connect_agent) each render — 8 Account destinations and 50 Setup destinations for the admin on a stock single-org showcase boot, as QA run #28545 read them at eeec88d1 (GET /api/v1/meta/app leaves, which match the nav source minus its gates; 48 re-measured at 3fd80337 for #28338, QA run #27481 read 44 and 8, QA run #24557 read 42 at 5b14b7f3, 45 on 2026-10-01, and #9453 recorded 43 and 7). Recount from the payload each run", "confirm the gated entries resolve to ABSENT-not-erroring for the admin: nav_organizations (requiresService org-scoping) is absent in single-org mode; nav_jwks Signing Keys is ABSENT for EVERY persona, admin included — the entry was removed in #7544 because sys_jwks declares apiEnabled:false, so its list answers OBJECT_API_DISABLED (404) for all callers and no permission gate could prune it; SSO Providers is absent unless OS_SSO_ENABLED (knownGap)", "sign in as the plain member: attempt to open Setup (app name setup) — capture the app-level refusal (App.requiredPermissions setup.access); open Account (declares no requiredPermissions) — capture it opening. Studio is not part of this comparison: it is absent for EVERY persona including the admin, so a member failing to reach it proves nothing about the gate (the same reasoning #7544 applied to nav_jwks)", "as the member, GET /api/v1/meta/app for a reachable app and confirm every manage_platform_settings-gated entry (API Keys, the manage_platform_settings settings URLs) is ABSENT from the member's payload — the server prunes, the client does not merely hide. JWKS is deliberately NOT in this list any more: it is absent for admins too, so finding it absent for a member proves nothing about the gate (#7544)", @@ -670,7 +676,7 @@ "a built-in app destination that renders a blank
or a 'failed to load' / 'no actions configured' placeholder with no error boundary is a FAIL (screenshot-first rules out transitional emptiness, then the persistent blank is the finding)", "the member reaching Setup or Studio content (app gate bypassed) is a FAIL — App.requiredPermissions is a server contract, prove denial on the wire, not just a hidden launcher tile", "a permission-gated entry rendered for the member and then erroring on click is a FAIL of the 'absent not erroring' contract — the nav must prune server-side", - "a member who opens one of Setup's url-type entries directly (/apps/setup/system/settings/) is not stopped by the app-level gate: the console draws the route inside the app shell it was opened from (the Showcase shell for QA run #25812), and the gate is the server's — every settings namespace read answers 403 SETTINGS_FORBIDDEN for the member (packages/services/service-settings/src/settings-routes.ts). Score the wire refusal per namespace and record the shell the route rendered in; a 200 carrying a namespace's values is a FAIL" + "a member who opens one of Setup's url-type entries directly (/apps/setup/system/settings/NAMESPACE) meets Setup's own access verdict, not the shell of the app they came from: the console waits on that verdict before it mounts the settings page, then draws \"You don't have access to this app\" with no app shell — no header, no navigation (ui/packages/app-shell/src/console/AppContent.tsx, appSurfaceAwaitsVerdict and the app-access-denied screen). A refused caller never mounts the page, so the page issues no namespace read: drive the wire half directly as the member, GET /api/settings/NAMESPACE per namespace, and every read answers 403 SETTINGS_FORBIDDEN (packages/services/service-settings/src/settings-routes.ts). The wire refusal is the scored part; record the screen the route drew beside it. A 200 carrying a namespace's values is a FAIL" ], "traps": ["hydration-race", "wrong-persona", "stale-console-bundle"], "source": [ @@ -737,6 +743,12 @@ "date": "2026-10-31", "change": "steps[5] recounted. Checklist-accuracy finding of QA run #27481 (18.3.0 wave 3, parked on anchor #27377; slice S4, #28338): steps[5] said 42 Setup destinations and the run's admin payload served 44 (Account 8); revision 10 did not recount. Recounted at head: a fresh `os dev --seed-admin` showcase boot at 3fd80337 serves 48 Setup leaves and 8 Account leaves to the admin in GET /api/v1/meta/app?id=setup and ?id=account (groups excluded). The step still tells the runner to recount from the payload each run; the number is the latest reading, not a pass mark. Re-read at 3fd80337", "ref": "#27481" + }, + { + "revision": 12, + "date": "2026-11-01", + "change": "Two QA run #28545 (W3-C1R) findings. negative[3], stale since #27507: the member's settings route no longer renders inside the app shell it was opened from. AppContent.tsx holds a Setup pseudo-route until Setup's access verdict, then draws the 'You don't have access to this app' screen above the layout, with no shell; the page never mounts, so the wire half is driven directly (GET /api/settings/NAMESPACE, 403 SETTINGS_FORBIDDEN, kept as the scored part). Counts: steps[4] lists the 11 namespaces a stock boot serves (sms and lifecycle added; setup-nav.contributions.ts plus the unwalled-only branding entry), and steps[5] carries #28545's 50 Setup and 8 Account destinations, which stay 'recount from the payload each run'. The namespace list is read from source; the 50 is #28545's live reading. No clause weakened. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index 945d245fa83..ad265bf6dbd 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -187,7 +187,7 @@ "title": "Create → read → update → delete a record through the console UI", "since": "v15", "status": "active", - "revision": 16, + "revision": 17, "priority": "P0", "surface": "browser", "personas": [ @@ -213,7 +213,7 @@ "capture the create request/response (expect POST /api/v1/data/showcase_account). The payload also carries the select defaults of fields the step leaves untouched — sales_region:'amer' (its AMER option is default:true, examples/app-showcase/src/data/objects/account.object.ts) — so expect that key in the body you diff (QA run #24556)", "re-read server-side: GET /api/v1/data/showcase_account?$filter=[[\"name\",\"=\",\"os-qa-\"]] — field-by-field vs the submitted payload", "open the record detail; edit exactly one field (annual_revenue); submit it (an edit form's button reads 'Update'); capture the PATCH /api/v1/data/showcase_account/; re-read the full row via GET /api/v1/data/showcase_account/", - "read the record's field-change history on the record page's timeline stream, under its Changes chip (变更). Record page v3 draws no tab named History at any width (#27055). The stream's whole face, with its chips, is the 动态 (Activity) tab: open it from the header of the 动态 panel, the first panel of the record page's right column (below the details in one column under 760 px, ui/packages/plugin-detail/src/synth/recordPageColumns.ts#recordPageTierFor), or by ?related=timeline (#27524). The panel itself shows only the newest lines, with no chips. Screenshot first, then read the change lines", + "read the record's field-change history on the record page's timeline stream, under its Changes chip (变更). Record page v3 draws no tab named History at any width (#27055). The stream's whole face, with its chips, is the 动态 (Activity) tab: open it from the header of the 动态 panel, the first panel of the record page's right column (below the details in one column under 760 px, ui/packages/plugin-detail/src/synth/recordPageColumns.ts#recordPageTierFor), or by ?related=timeline (#27524). At 1440 px the page also draws a top-level tab row — Overview, Activity (动态), then its panels (ui/packages/plugin-detail/src/RecordPageColumns.tsx, the tab row) — and its Activity tab is the same door as ?related=timeline. The panel itself shows only the newest lines, with no chips, and can lag the update line until a fresh read (QA run #28545): read the change lines on the Activity tab after a reload, not off the panel. Screenshot first, then read the change lines", "clone the record: POST /api/v1/data/showcase_account//clone (optionally with {overrides:{name:'os-qa--clone'}}); capture the 201 result (new id ≠ source, sourceId, record); re-read the clone via GET /api/v1/data/showcase_account/", "RLS probe on clone: as a persona WITHOUT read on the source row (a permission-zoo persona, or a non-owner under owner-RLS), POST the same clone route and capture the refusal — expect 404 RECORD_NOT_FOUND, never a silent duplicate (record blocked(fixture) if no non-reading persona is bound on this boot)", "delete via the row/detail action; confirm the dialog; re-read via the filtered GET. ⚠️ If clause 7's recipe gave the member an own-row invoice pointing at THIS account, the delete answers 409 DELETE_RESTRICTED: the invoice's account lookup is required, and the engine turns a required set_null into restrict (packages/objectql/src/engine.ts). Point that invoice at a seeded account instead (the recipe's step 5 says so)", @@ -247,7 +247,7 @@ { "clause": "the record's field-change history (the record page's sys_audit_log read, shown as the timeline stream's Changes chip) 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 Changes chip is selected on the account's timeline stream (steps[5]), read its change lines — same contract ui/e2e/live/record-history-display.spec.ts pins on showcase_account, the showcase's enable.trackHistory object, at 1440×900 (its option labels, the localized Customer Since date and the resolved owner). The page reads sys_audit_log only for an object whose enable.trackHistory is true (ui/packages/app-shell/src/views/RecordDetailView.tsx, historyEnabled), and the rows reach the stream as field_change items (ui/packages/plugin-detail/src/recordTimelineStream.ts#auditEntriesToFeedItems). The chip also lists the stream's sys_activity created/updated rows, so attribute a line to the audit read before scoring it. ⛔ Never look for a tab named History: record page v3 removed it at every width (#27055; buildDefaultPageSchema.test.ts asserts the default tabs carry neither Activity nor History), so its absence fails a working product. The spec is a live Playwright spec in this checkout, run from ui/ with `pnpm test:e2e:live` against a backend on port 3000 and the console dev server on port 5180", + "verify": "after a fresh read of the page (the right column's 动态 panel can lag the update line, QA run #28545), open the stream by the Activity tab, the 动态 panel header or ?related=timeline (steps[5]); after a screenshot confirms the Changes chip is selected on the account's timeline stream, read its change lines — same contract ui/e2e/live/record-history-display.spec.ts pins on showcase_account, the showcase's enable.trackHistory object, at 1440×900 (its option labels, the localized Customer Since date and the resolved owner). The page reads sys_audit_log only for an object whose enable.trackHistory is true (ui/packages/app-shell/src/views/RecordDetailView.tsx, historyEnabled), and the rows reach the stream as field_change items (ui/packages/plugin-detail/src/recordTimelineStream.ts#auditEntriesToFeedItems). The chip also lists the stream's sys_activity created/updated rows, so attribute a line to the audit read before scoring it. ⛔ Never look for a tab named History: record page v3 removed it at every width (#27055; buildDefaultPageSchema.test.ts asserts the default tabs carry neither Activity nor History), so its absence fails a working product. The spec is a live Playwright spec in this checkout, run from ui/ with `pnpm test:e2e:live` against a backend on port 3000 and the console dev server on port 5180", "evidence": "screenshot + entries" }, { @@ -379,6 +379,12 @@ "date": "2026-10-29", "change": "steps[5] and acceptance[4] (clause and verify) re-pointed from the History tab to the record page v3 surface, the same shape records-forms.field-history-tracking took at its revision 10 (#27492). Record page v3 (#27055) removed the History tab at every width, so a runner following the old step found no tab and scored a working product red or blocked. The step now reads the account's sys_audit_log field-change history under the timeline stream's Changes chip, located by width tier (recordPageTierFor), and the verify names the gate (RecordDetailView historyEnabled), the stream mapping (auditEntriesToFeedItems), the sys_activity rows the chip also lists, and the 1440×900 viewport the re-pointed live spec record-history-display.spec.ts uses. The clause's contract (display values, real actor, no raw payloads) is unchanged; no run verdict on it is invalidated beyond the surface it is read on", "ref": "#27654" + }, + { + "revision": 17, + "date": "2026-11-01", + "change": "QA run #28545 (W3-C1R) finding: at 1440 px the record page also has a top-level Activity tab, the same face as ?related=timeline, and steps[5] named only the panel header and the query. steps[5] now names the tab row (RecordPageColumns.tsx: Overview, Activity, then the panels), and steps[5] and acceptance[4].verify say to read the change lines on the full stream after a fresh read, because the right-column panel lagged the update line in #28545. The tab row is read from source; the lag is #28545's live reading. No clause weakened. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", + "ref": "#28545" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index bf91d4452dd..eca3e702379 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": 9, + "revision": 10, "priority": "P0", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai)", "end user (same account driving the published app)"], @@ -27,7 +27,7 @@ "steps": [ "boot the showcase example (objectstack dev --ui --seed-admin -p -d file:/tmp//data.db), sign in as admin@objectos.ai/admin123, and note the boot timestamp in the server log (the zero-restart clause reads it later)", "Home → the build chip → create a NEW writable package (维修中心 / com.example.repairs, namespace repairs) via the new-package wizard. Type the identifier yourself: a CJK-only display name derives no package id (objectui PackageFormDialog.tsx toIdSegment: 'Empty when nothing usable remains (a CJK-only name, say)'), and the namespace derives from the id you type. The build chip has two forms (ui/packages/app-shell/src/console/home/HomePage.tsx): with an agent catalog it reads 'Build an app' (data-testid home-build-app) and opens the Studio landing, where New package opens the wizard; with none it reads 'New in Studio' (home-build-in-studio) and opens Studio with the New package dialog already open, the form QA run #27481 met on the stock showcase. Record which", - "Data pillar → new object 'Repair Ticket' (the identifier auto-suggests repair_ticket from the display name, and the dialog PREFIXES it with the package namespace, so the object is named repairs_repair_ticket — the name every later step and clause uses (framework#2694, objectui StudioDesignSurface prefixObjectName). The dialog also carries an OWD (sharing model) choice, pre-selected to the platform's recommended baseline: confirm or change it, then click 'Save as draft', the dialog's confirm button (engine.studio.createDraft; no Studio create dialog has a button named Create)) → add a picklist field Status with 3 values. There is no Save draft button: the designer autosaves, and a picklist field is held unsaved until it has at least one option (the status line reads 'Not saved yet: the field “Status” needs at least one option'), so add the field and fill its options, and the draft save fires", + "Data pillar → new object 'Repair Ticket' (the identifier auto-suggests repair_ticket from the display name, and the dialog PREFIXES it with the package namespace, so the object is named repairs_repair_ticket — the name every later step and clause uses (framework#2694, objectui StudioDesignSurface prefixObjectName). The dialog also carries an OWD (sharing model) choice, pre-selected to the platform's recommended baseline: confirm or change it, then click 'Save as draft', the dialog's confirm button (engine.studio.createDraft; no Studio create dialog has a button named Create)) → add a picklist field Status with 3 values. After a navigation the object surface opens on its Records tab (the Data pillar's default view, ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx); Add field is the Form tab's button (where QA run #28545 found it; the Records grid's add-column header carries it too). Each option's stored value derives from its label as you type it — 'In Repair' stores in_repair (ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx OptionsEditor) — so overwrite the value wherever the run wants a different one (a short code such as wip), and note the three stored values: acceptance[1] and acceptance[2] read them. There is no Save draft button: the designer autosaves, and a picklist field is held unsaved until it has at least one option (the status line reads 'Not saved yet: the field “Status” needs at least one option'), so add the field and fill its options, and the draft save fires", "open the Changes panel and Publish, then confirm with 'Publish all' in the review sheet; the toast names the count — 'Published N items in this package' (N = the drafts promoted), 'Published 1 item in this package' for one (engine.studio.publishedAllCount / publishedAllCountOne, objectui#11807; 'Published all drafts in this package' is the fallback when the count is unknown) — and, because the publish puts an object live, it adds 'Members can use it once a permission set grants them access.' with an 'Open Access' action (StudioDesignSurface.tsx)", "create a record in the runtime-faithful Records grid with a Status value chosen", "Create app 'Repair Center' (identifier auto-suggested) → Interfaces pillar → add a nav item bound to repairs_repair_ticket → Publish", @@ -93,7 +93,8 @@ { "revision": 6, "date": "2026-10-20", "change": "acceptance[0].verify names the console pin cf153eb2e535. PR #24982's pin finding: the verify stated the current pin as 616df6598b42, and PR #24982 moved .objectui-sha to cf153eb2e535. Read in a full objectui clone: e2e/live/studio-object-designer.spec.ts keeps blob 58a7f1cbf024 at both pins (over the range, e2e/live/ changes only ci/backend.env), and studio-pkg-chip, package-form-id-input and package-form-submit are still declared in app-shell's StudioDesignSurface.tsx and PackageFormDialog.tsx at cf153eb2e535. The spec's live verdict at the pin is not measured here. No clause changed", "ref": "#25003" }, { "revision": 7, "date": "2026-10-26", "change": "fixtures.requires[0] and acceptance[0].verify drop the retired objectui-pin wording (.objectui-sha, 'not runnable from this checkout'), which #26035 left to this file's card: objectui is imported under ui/, so F1 resolves at ui/e2e/live and runs from this checkout, and the console builds from this tree with pnpm console:build. Wording only; the clause is unchanged", "ref": "#25848" }, { "revision": 8, "date": "2026-10-31", "change": "steps[1], [2] and [6] name the controls as the console draws them. steps[1]: Home's build chip reads 'New in Studio' and opens Studio with the New package dialog when the deployment has no agent catalog, which is what QA run #27481 met, and 'Build an app' (opening the Studio landing) only when it has one (ui/packages/app-shell/src/console/home/HomePage.tsx). steps[6]: the way from the Studio landing to the console Home is the top bar's product mark, a link named 'PRODUCT home' (data-testid app-bar-product-home, ui/packages/app-shell/src/layout/AppHeader.tsx), which is why the run found no control named 'Home'. steps[2] carries the same stale button name the run met elsewhere: the New object dialog confirms with 'Save as draft' (engine.studio.createDraft), not Create. Accuracy finding of QA run #27481. No clause changed. Wave anchor #27377, wave-3 accuracy slice S2; read at base a0f865491d", "ref": "#27481" }, - { "revision": 9, "date": "2026-11-01", "change": "steps[3] quotes the publish toast as the console draws it. It still read 'Published 1 item in this package (one atomic release)', a wording retired when the walkthrough of #27521 (row 28) dropped 'atomic' from the publish title, toast and confirm note: the toast is now 'Published N items in this package' ('Published 1 item in this package' for one; engine.studio.publishedAllCount / publishedAllCountOne in ui/packages/i18n/src/namespaces/studio/en.ts), and a publish that puts an app or object live adds the permission-set hint and an 'Open Access' action (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx). Wording only; no clause changed. Read at base efb1620c", "ref": "#27521" } + { "revision": 9, "date": "2026-11-01", "change": "steps[3] quotes the publish toast as the console draws it. It still read 'Published 1 item in this package (one atomic release)', a wording retired when the walkthrough of #27521 (row 28) dropped 'atomic' from the publish title, toast and confirm note: the toast is now 'Published N items in this package' ('Published 1 item in this package' for one; engine.studio.publishedAllCount / publishedAllCountOne in ui/packages/i18n/src/namespaces/studio/en.ts), and a publish that puts an app or object live adds the permission-set hint and an 'Open Access' action (ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx). Wording only; no clause changed. Read at base efb1620c", "ref": "#27521" }, + { "revision": 10, "date": "2026-11-01", "change": "QA run #28545 (W3-C1R) finding on steps[2]: the object surface opens on Records after a navigation, Add field is on the Form tab, and an option's stored value derives from its label ('In Repair' gives in_repair), so a run that wants wip must overwrite it. steps[2] now says so, read from source (StudioDesignSurface.tsx viewMode default 'grid'; OptionsEditor derives a new row's value with toFieldNameOrEmpty). The same card's steps[3] finding, the stale publish toast, was fixed upstream by revision 9 (#28526, after eeec88d1). No clause changed. Read from source at base 6b9c1ca991. Wave anchor #27377, wave-3 accuracy slice S9 (#28641); finding parked in anchor comment 6554042054", "ref": "#28545" } ] }, {