diff --git a/docs/qa/platform-checklist/areas/access-security.json b/docs/qa/platform-checklist/areas/access-security.json index b433d2cb75..dffe1d08d3 100644 --- a/docs/qa/platform-checklist/areas/access-security.json +++ b/docs/qa/platform-checklist/areas/access-security.json @@ -9,7 +9,7 @@ "title": "Row-level security: restricted member sees only their rows; admin sees all", "since": "v15", "status": "active", - "revision": 6, + "revision": 7, "priority": "P0", "surface": "api", "personas": [ @@ -27,7 +27,7 @@ ] }, "steps": [ - "boot showcase isolated (own port + file DB per dogfood skill §0); sign in as admin; sign up members A and B (fresh emails; invite each one first — the stock showcase declares no auth.audience, so the invite_only default answers an uninvited POST /api/v1/auth/sign-up/email 403 SELF_REGISTRATION_CLOSED; an admin POST /api/v1/auth/organization/invite-member for the address admits the sign-up (or admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true)) — both hold only the everyone baseline showcase_member_default", + "boot the showcase on its own port and file DB (dogfood skill §0); sign in as admin; sign up members A and B (fresh emails; invite each one first — the stock showcase declares no auth.audience, so the invite_only default answers an uninvited POST /api/v1/auth/sign-up/email 403 SELF_REGISTRATION_CLOSED; an admin POST /api/v1/auth/organization/invite-member for the address admits the sign-up (or admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true)) — both hold only the everyone baseline showcase_member_default", "as A: POST /api/v1/data/showcase_private_note {\"title\": \"A note 1\"} and a second note; as B: POST one note — capture the returned ids", "as A: GET /api/v1/data/showcase_private_note (list); as B: same; capture both record sets", "as A: GET /api/v1/data/showcase_private_note/ (foreign by-id read); as A: PATCH /api/v1/data/showcase_private_note/ {\"body\": \"forged\"} (foreign by-id write)", @@ -124,6 +124,12 @@ "date": "2026-10-06", "change": "partial-coverage gap of QA run #21720 parked on #22017 (row 11). A6 was blocked(dependency): the clause forbids ticking it without the private D11 write-up. Disposition: recorded knownGap. The item gains a knownGap naming the environment the D11 spot-check needs (the maintainer's private write-up and fix card, which no public record or CI carries by design) and separating the skipped-object half, which a public run can still score. Non-disclosing, as the clause requires", "ref": "#22017" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated (own port + file DB per dogfood skill §0)' → 'boot the showcase on its own port and file DB (dogfood skill §0)'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -132,7 +138,7 @@ "title": "Write-path guards: readonly strip, owner_id forge/transfer denied, bulk validation", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P0", "surface": "api", "personas": [ @@ -148,7 +154,7 @@ ] }, "steps": [ - "boot showcase isolated; admin session + two members Alice and Bob (fresh sign-ups — invite each one first — the stock showcase declares no auth.audience, so the invite_only default answers an uninvited POST /api/v1/auth/sign-up/email 403 SELF_REGISTRATION_CLOSED; an admin POST /api/v1/auth/organization/invite-member for the address admits the sign-up (or admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true)); resolve their sys_user ids via the data API or objectql", + "boot the showcase on its own port and DB; admin session + two members Alice and Bob (fresh sign-ups — invite each one first — the stock showcase declares no auth.audience, so the invite_only default answers an uninvited POST /api/v1/auth/sign-up/email 403 SELF_REGISTRATION_CLOSED; an admin POST /api/v1/auth/organization/invite-member for the address admits the sign-up (or admin POST /api/v1/auth/admin/create-user, which answers mustChangePassword:true)); resolve their sys_user ids via the data API or objectql", "as admin (contact create is admin-side in the stock matrix): POST /api/v1/data/showcase_contact {\"name\": \"RO probe\", \"email\": \"ro-probe@verify.test\", \"lead_score\": 10} — forging the readonly field on INSERT; GET the row back", "PATCH the same contact with {\"lead_score\": 99}; GET again", "as Alice: POST /api/v1/data/showcase_private_note {\"title\": \"planted\", \"owner_id\": \"\"} (insert forge); then POST a note with {\"owner_id\": \"\"} (explicit self-owner); then PATCH her own note with {\"owner_id\": \"\"} (transfer) and with {\"owner_id\": null} (disown)", @@ -249,6 +255,12 @@ "date": "2026-10-16", "change": "wire shapes the steps left out (#23878 checklist-accuracy findings, re-measured on main c91af8ed6). createMany takes a bare JSON array (a {records} wrapper answers 400 VALIDATION_FAILED); updateMany takes {records:[{id,data}]} (a where body answers 400 naming records); and a bulk run stops at the first validation failure by default, marking later rows NOT_ATTEMPTED, so clause 7's mixed-outcome step now sends options.continueOnError true. Measured on showcase_invoice_line [valid, quantity -1, valid]: default → succeeded 1, failed 2 (VALIDATION_FAILED, NOT_ATTEMPTED), only row 0 changed; continueOnError → succeeded 2, failed 1, rows 0 and 2 changed, row 1 unchanged", "ref": "#24036" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -451,7 +463,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": 6, + "revision": 7, "priority": "P0", "surface": "api", "personas": [ @@ -473,7 +485,7 @@ ] }, "steps": [ - "boot showcase isolated; 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 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", "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 + error.code for each cell", "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", @@ -586,6 +598,12 @@ "date": "2026-10-16", "change": "counts and the known gap re-measured on main c91af8ed6 (#23878 checklist-accuracy findings). access-matrix.json now has 45 rows: 41 showcase_* plus 4 delegate × sys_* (sys_business_unit, sys_business_unit_member, sys_user, sys_user_position), so the fixture line, steps[6], acceptance[6] and automated.ref say 45 / 4 instead of 47 / 6; the 164 showcase cells and their 87 allow / 77 deny / 9 baseline-only split are unchanged. knownGaps[4] (A1 a KNOWN FAIL until #21057) is removed, as its own ruling prescribed once the C1 business-unit leg landed: #21057 closed on 2026-10-08, the five seeded sys_business_unit rows carry organization_id, and the delegate's in-subtree create answers 201 while bu_hq_finance stays 403. A1 is therefore expected to pass; a 403 on the in-subtree create is now a fail, not a known one", "ref": "#24036" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -594,7 +612,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": 3, + "revision": 4, "priority": "P0", "surface": "api", "personas": [ @@ -612,7 +630,7 @@ ] }, "steps": [ - "boot showcase isolated; create members A and B (baseline), two contributor members C1/C2 (grant showcase_contributor), an ops member, an auditor member", + "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; 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", @@ -707,6 +725,12 @@ "date": "2026-10-01", "change": "cross-reference disambiguation from QA run #21056: the private handle D11 is not FOLLOW-UPS.md §7c's public D11 row (an unrelated two-factor defect that took the same number), and clause numbers here count from 0 — this item's clause 4 is a run record's A5, rls-both-sides clause 5 is its A6. Non-disclosing as before", "ref": "#21060" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -831,7 +855,7 @@ "title": "Scope depth (readScope/writeScope): org-wide read with own-only write, per persona — depth widens along geometry, never bypasses", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -851,7 +875,7 @@ ] }, "steps": [ - "boot showcase isolated; create member M plus manager/executive/ops personas; as M: POST /api/v1/data/showcase_inquiry {\"name\": \"M probe\", \"email\": \"m.probe@example.com\", \"message\": \"scope probe\"} (showcase_inquiry requires name, email and message and has no `subject` field — examples/app-showcase/src/data/objects/inquiry.object.ts); as the manager: POST their OWN inquiry", + "boot the showcase on its own port and DB; create member M plus manager/executive/ops personas; as M: POST /api/v1/data/showcase_inquiry {\"name\": \"M probe\", \"email\": \"m.probe@example.com\", \"message\": \"scope probe\"} (showcase_inquiry requires name, email and message and has no `subject` field — examples/app-showcase/src/data/objects/inquiry.object.ts); as the manager: POST their OWN inquiry", "as the manager: GET /api/v1/data/showcase_inquiry (list) — M's inquiry must appear (readScope org on a private object)", "as the manager: PATCH M's inquiry (foreign write, writeScope own) and PATCH their own inquiry", "as the executive: GET the inquiry list and M's inquiry by id; then attempt PATCH (no edit bit at all); also GET /api/v1/data/showcase_private_note list (readScope org over private notes)", @@ -932,6 +956,12 @@ "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] added, under round 6's theme-B rule (drop `automated` where no pin asserts a full clause, with a knownGaps line) — round 11 of wave anchor #24579, whose round-3 summary named this nominally Tier 1 item. Its revision-3 automated.ref already said the three dogfood pins cover no clause. Re-read at 46bb5e3e54: they drive business-unit depth on showcase notes and never the manager / executive / ops personas or showcase_inquiry. The item is now Tier 2, every clause driven by hand as revision 3 already prescribed. No clause changed", "ref": "#24801" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1170,7 +1200,7 @@ "title": "Per-record manual shares grant, scope, and revoke access on a private-OWD record; rule evaluate reconciles the audience", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1189,7 +1219,7 @@ ] }, "steps": [ - "boot showcase isolated (own port + file DB, dogfood skill §0); admin session + members A/B/C (fresh emails); resolve their sys_user ids system-side", + "boot the showcase on its own port and file DB (dogfood skill §0); admin session + members A/B/C (fresh emails); resolve their sys_user ids system-side", "as A: POST /api/v1/data/showcase_private_note {\"title\": \"A share probe\"} — capture the id; POST a SECOND note as A too (the revoke-scope probe below needs a different record id under the same object)", "as B: GET /api/v1/data/showcase_private_note/ — the PRE-grant baseline (private OWD, B is not owner → must be refused)", "as A: POST /api/v1/data/showcase_private_note//shares {\"recipientType\": \"user\", \"recipientId\": \"\", \"accessLevel\": \"read\"} — capture the 201 sys_record_share row (source 'manual')", @@ -1296,6 +1326,12 @@ "date": "2026-10-19", "change": "acceptance[6] (clause, verify, evidence) corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24559, access-security.record-share-grant-revoke acceptance[6]; wave anchor #24579 §3). The clause required source_id to be the rule's sys_sharing_rule ROW id and NOT its name; that holds only for an organization-authored rule. The rule it evaluates, share_key_account_qualified_contacts_with_managers, is DECLARED, and a declared rule is projected with id = name (sharing-rule-service.ts projectDeclaredRule, ADR-0131 D2), so its grants carry the name. The clause now asserts source_id equals the rule's id as evaluate reports it, and the verify names revokeRuleGrants, the current name of the purge it cited as purgeRuleGrants", "ref": "#24632" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated (own port + file DB, dogfood skill §0)' → 'boot the showcase on its own port and file DB (dogfood skill §0)'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2247,7 +2283,7 @@ "title": "A package-declared capability is served by the registry (no sys_capability row — the object is retired, ADR-0131 D3) and resolves across the grant/require three-way (ADR-0066) on the PLATFORM action path; platform names cannot be shadowed", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -2274,7 +2310,7 @@ "shadow-refusal — a declaration whose name collides with PLATFORM_CAPABILITY_NAMES, or with a name another package declares, is refused loudly by the registry's one-holder rule" ], "steps": [ - "boot showcase isolated; confirm the declared capability is served: GET /api/v1/meta/capability/showcase.export_data answers its declared name/label/description/scope, and GET /api/v1/data/sys_capability answers 404 OBJECT_NOT_FOUND (the object is retired, ADR-0131 D3 — there is no row table to find the capability in)", + "boot the showcase on its own port and DB; confirm the declared capability is served: GET /api/v1/meta/capability/showcase.export_data answers its declared name/label/description/scope, and GET /api/v1/data/sys_capability answers 404 OBJECT_NOT_FOUND (the object is retired, ADR-0131 D3 — there is no row table to find the capability in)", "read its fields — they are the declaration's own; nothing is transcribed into a row", "drive the dispatcher /actions domain (knownGaps): POST /api/v1/actions/showcase_field_zoo//, read the record id off GET /api/v1/data/showcase_field_zoo. Do NOT score /api/v1/data/:object/:id/actions/... — @objectstack/rest mounts no action-invoke route by design (#7680), so a 404 there scores nothing", "on that surface, INVOKE showcase_zoo_perm_held (requires showcase.export_data) as the GRANTED member holding OpsPermissionSet, then as a NON-granted member: granted executes, non-granted is refused with the actionPermissionError text naming the action and the missing capability", @@ -2351,6 +2387,12 @@ "date": "2026-10-19", "change": "steps[7] and acceptance[1].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24559, access-security.capability-declaration-lifecycle; wave anchor #24579 §3). The located `capabilities.0.scope` refusal appears only for a raw capabilities entry: defineCapability parses its argument (packages/spec/src/security/capabilities.ts), so a malformed shape written through it throws at config load. The item stated no expectation for the admin on showcase_zoo_perm_held. The run measured the admin refused, and actionPermissionError has no administrator exemption (only a system context skips it), so the verify now says so. The run also measured DELETE /api/v1/meta/capability/NAME answering 200 while removing nothing; no clause asserts that door, and it is left to the wave anchor as an observation, not encoded here", "ref": "#24632" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2522,7 +2564,7 @@ "title": "Activation-ledger write authority (ADR-0126 §5): the manage_metadata tier refuses first on every posture, and the shared operator gate — INERT on stock `single` — demands the platform_admin POSITION in walled postures, at both the flow and action doors", "since": "v17", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "api", "personas": [ @@ -2545,7 +2587,7 @@ ] }, "steps": [ - "boot showcase isolated; admin session; PROVE the posture is `single` and record the evidence — the boot banner's `Tenancy: single` line (no API reports the posture; at OS_LOG_LEVEL=info the \"[security] tenancy posture 'single'\" line corroborates it) — the posture selects which gate can answer", + "boot the showcase on its own port and DB; admin session; PROVE the posture is `single` and record the evidence — the boot banner's `Tenancy: single` line (no API reports the posture; at OS_LOG_LEVEL=info the \"[security] tenancy posture 'single'\" line corroborates it) — the posture selects which gate can answer", "as the plain member: POST /api/v1/automation/showcase_urgent_task_alert/toggle {\"enabled\": false} — capture status, code and the EXACT message string", "as the plain member: POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {\"enabled\": false} — capture status, code and the exact message", "as the plain member again: repeat the flow toggle with a deliberately INVALID body (e.g. {\"enabled\": \"false\"}) — the refusal must still be the 403, not a 400 body-validation answer (the gate runs before body validation, automation.ts; activation-gate.ts)", @@ -2669,6 +2711,12 @@ "date": "2026-10-20", "change": "fixtures.requires[3] names the recipe for the walled legs' org-owner manage_metadata grant, and knownGaps[0]'s prerequisite sentence says os dev — QA run #24783 (findings F7 and F2), round 11 of wave anchor #24579. No HTTP door placed the grant on the walled boot: the operator's insert into an organization it is not a member of is refused by the Layer 0 tenant check, and the owner's own insert by ADR-0090 D12 condition (c), manage_metadata being scope 'platform'. The recipe is the dogfood harness's system-context engine insert (wall-environment-metadata-refusal.dogfood.test.ts, its boot helper). A bare production os serve judges none of the walled prerequisites until OS_AUTH_SECRET is set (RUNNER.md), so the refusal sentence now names os dev. No clause changed", "ref": "#24801" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2677,7 +2725,7 @@ "title": "Write-door parity on a packaged flow: PUT/DELETE /automation/:name must refuse the same packaged artifact that PUT /meta/flow/:name refuses (ADR-0126 §2 locked base)", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -2694,7 +2742,7 @@ ] }, "steps": [ - "boot showcase isolated; admin session; GET /api/v1/automation/showcase_urgent_task_alert and store the full definition (the restore payload); also GET /api/v1/meta/flow/showcase_urgent_task_alert?layers=true to prove the artifact is package-backed (populated code layer, _packageId com.example.showcase)", + "boot the showcase on its own port and DB; admin session; GET /api/v1/automation/showcase_urgent_task_alert and store the full definition (the restore payload); also GET /api/v1/meta/flow/showcase_urgent_task_alert?layers=true to prove the artifact is package-backed (populated code layer, _packageId com.example.showcase)", "control leg, the /meta door: PUT /api/v1/meta/flow/showcase_urgent_task_alert with a trivially modified copy of the definition (e.g. label suffix) — capture status, code and WHICH layer answered; repeat with ?package=com.example.showcase and capture that code too", "probe leg 1: PUT /api/v1/automation/showcase_urgent_task_alert with the same trivially modified definition — capture status and, if 2xx, GET the flow back to prove the live registration mutated", "probe leg 2: DELETE /api/v1/automation/showcase_urgent_task_alert — capture status and, if 2xx, confirm GET /api/v1/automation/showcase_urgent_task_alert now 404s (the shipped flow is gone from the live engine)", @@ -2752,7 +2800,8 @@ "ref": "#12438" }, { "revision": 2, "date": "2026-10-02", "change": "retired the expected-fail framing: 4b45afae (#20817) locked the /automation write doors against the packaged base, and the 17.6.0 release-verification run measured PUT and DELETE /automation/showcase_urgent_task_alert answering 403 NOT_OVERRIDABLE with the /meta door's lock message, the flow unchanged and still firing. Title, knownGaps and clauses 2-3 now state the parity as holding; the restore step is conditional (a restore PUT is itself refused), and the knownGap names the permanent pin packaged-flow-write-door-parity.dogfood.test.ts", "ref": "#21330" }, - { "revision": 3, "date": "2026-10-20", "change": "negative[1] rewritten as the product outcome it guarded, and negative[2] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): both stated a runner rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[1]'s 'do not read the /meta refusal as covering the /automation door' guarded one: an /automation write door answering 2xx on the artifact /meta refused, which is now the FAIL. negative[2] (the toggle door is not this item) guards none, so its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#24963" } + { "revision": 3, "date": "2026-10-20", "change": "negative[1] rewritten as the product outcome it guarded, and negative[2] moved to steps[6]. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): both stated a runner rule with no product expectation, so they could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). negative[1]'s 'do not read the /meta refusal as covering the /automation door' guarded one: an /automation write door answering 2xx on the artifact /meta refused, which is now the FAIL. negative[2] (the toggle door is not this item) guards none, so its text moved, unchanged in substance, to steps[6]. No acceptance clause changed", "ref": "#24963" }, + { "revision": 4, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -3011,7 +3060,7 @@ "title": "The /auth/me/permissions aggregation (and its /auth/me/localization + /me/apps siblings) mirrors server-side enforcement in both directions — and the trio's anonymous 200 is the deliberate exception to the 401 floor", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -3035,7 +3084,7 @@ ] }, "steps": [ - "boot showcase isolated; provision C (grant showcase_contributor via sys_user_permission_set), P (baseline only), admin; use a FRESH http client per persona (cache-staleness)", + "boot the showcase on its own port and DB; provision C (grant showcase_contributor via sys_user_permission_set), P (baseline only), admin; use a FRESH http client per persona (cache-staleness)", "as C: GET /api/v1/auth/me/permissions — record objects.showcase_project, fields['showcase_project.budget'], permissionSets, systemPermissions", "verb parity, both directions: as C PATCH a showcase_project name (the map says allowEdit) — 2xx; as P read P's own map (allowEdit absent/false on showcase_project) then issue the identical PATCH — 403 PERMISSION_DENIED", "field parity: C's map says budget editable:false — C's budget PATCH is refused/stripped with the stored value unchanged (the fls-mask-and-strip oracle; cross-check only, do not re-score that item here)", @@ -3128,6 +3177,12 @@ "date": "2026-09-25", "change": "the parity steps never reached the population the map got wrong: a wall-less org admin (organization_admin_no_bypass, a plain '*' with no super-user bit) had NO entry for an app object covered only by that wildcard, so current_user.can() answered false where checkObjectPermission allowed. Added persona W, its step and acceptance clause (an object no set names present and writable, a narrower named entry widened, the private sys_secret absent and refused — the shape measured on a booted showcase), the source anchor for the producer, and the unit parity table to automated.ref", "ref": "#20083" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 951daedf2a..1597865200 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -9,7 +9,7 @@ "title": "transactionalBatch capability bit matches the mounted /batch behavior — atomicity, rollback codes, size cap", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -26,7 +26,7 @@ ] }, "steps": [ - "boot showcase isolated; GET /api/v1/discovery and read capabilities.transactionalBatch.enabled — on the wire each capability is a CapabilityDescriptor object { enabled, features?, description? } (discovery.zod.ts), never a bare boolean. The @objectstack/client getter is the exception: it FLATTENS each descriptor to a real boolean, so client.capabilities.transactionalBatch is a boolean and the two reads are spelled differently on purpose", + "boot the showcase on its own port and DB; GET /api/v1/discovery and read capabilities.transactionalBatch.enabled — on the wire each capability is a CapabilityDescriptor object { enabled, features?, description? } (discovery.zod.ts), never a bare boolean. The @objectstack/client getter is the exception: it FLATTENS each descriptor to a real boolean, so client.capabilities.transactionalBatch is a boolean and the two reads are spelled differently on purpose", "POST /api/v1/batch with a well-formed cross-object atomic batch: operations = [create showcase_private_note {title}, create showcase_task {title, project: }] and a TOP-LEVEL atomic: true; verify both rows exist afterwards. ⚠️ CrossObjectBatchRequestSchema declares operations + a top-level atomic and no options key at all — an options wrapper is silently STRIPPED by Zod, so the request runs with atomic defaulting to true", "POST /api/v1/batch with the same operations and a TOP-LEVEL atomic: false; capture status + error code. ⛔ Spelled as options {atomic: false} the key is stripped, atomic defaults to true, and the batch simply SUCCEEDS — the clause below then reads as a product failure when nothing failed", "POST /api/v1/batch where a MIDDLE operation must fail (e.g. create showcase_invoice missing its required name), sandwiched between two valid creates; capture the per-operation results and re-read all three would-be rows", @@ -123,6 +123,12 @@ "date": "2026-10-20", "change": "acceptance[5], steps[6], negative[1] and knownGaps[0] added for #24781, which QA run #24779 found beside the clauses (round 3 of the 18.0 sweep, 580fd4ed): no clause asked what the cross-object door answers for an empty-string id, and it answers 500 where a missing id answers 400 VALIDATION_FAILED. The clause asserts the missing-id answer and stays red until #24781's fix lands. No existing clause changed", "ref": "#24802" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -378,7 +384,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": 4, + "revision": 5, "priority": "P0", "surface": "api", "personas": [ @@ -393,7 +399,7 @@ ] }, "steps": [ - "boot showcase isolated; as admin GET /api/v1/data/showcase_account with no filter and a generous $top — this unfiltered baseline is the local ground truth every operator answer is computed from (never hardcode counts; the seed can drift)", + "boot the showcase on its own port and DB; as admin GET /api/v1/data/showcase_account with no filter and a generous $top — this unfiltered baseline is the local ground truth every operator answer is computed from (never hardcode counts; the seed can drift)", "for each operator variant below, run BOTH spellings against showcase_account (or showcase_task where the type fits): the POST /api/v1/data/showcase_account/query body form (where: {field: {$op: value}}) and the AST array form ([field, op, value] — spellings per AST_OPERATOR_MAP); e.g. where {status: {$eq: 'active'}}, {annual_revenue: {$gt: 10000000}}, {status: {$in: ['active','prospect']}}, {annual_revenue: {$between: [5000000, 50000000]}}, {name: {$contains: 'orth'}}, {name: {$startsWith: 'Con'}}, {churn_reason: {$null: true}}", "for each operator: compute the expected id set from the baseline locally and compare with the returned records — a known-answer check, not a smoke 200", "pagination window: GET /api/v1/data/showcase_account?$orderby=name&$top=3&$skip=2 and verify it equals rows 3–5 of the locally sorted baseline; also POST body limit/offset/top equivalents", @@ -536,6 +542,12 @@ "date": "2026-10-19", "change": "steps[4] states the projection the build serves — checklist-accuracy finding of the 18.0 QA sweep (QA run #24556, api-backend.query-contract-matrix step 5; wave anchor #24579 §3): $select narrows to exactly the requested keys and adds no identity key. Re-derived at base 9a7838edf5 (objectql engine.ts: SqlDriver emits builder.select(query.fields) verbatim). No clause changed", "ref": "#24649" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -544,7 +556,7 @@ "title": "Sampled endpoints return the standard error envelope with ledgered codes — no invented codes, no retired keys, statuses match the map", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -560,7 +572,7 @@ ] }, "steps": [ - "boot showcase isolated; induce one error per family and capture the FULL body + status each time:", + "boot the showcase on its own port and DB; induce one error per family and capture the FULL body + status each time:", "validation: as admin POST /api/v1/data/showcase_invoice {} (missing required name) — expect the field-level envelope", "auth: anonymous GET /api/v1/data/showcase_private_note — the 401 sample", "authorization: as the baseline member POST /api/v1/data/showcase_announcement {\"title\": \"probe\"} — the 403 sample", @@ -659,6 +671,12 @@ "date": "2026-10-19", "change": "acceptance[3] (clause, verify) names the oracle it can be judged by — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.error-envelope-ledger acceptance[3]; wave anchor #24579 §3): HttpStatusErrorCodeMap is a status → floor-code map (400 → VALIDATION_ERROR, 404 → RESOURCE_NOT_FOUND, …), and OBJECT_NOT_FOUND / RECORD_NOT_FOUND / VALIDATION_FAILED have no entry in it. Re-derived at base 9a7838edf5 (packages/spec/src/api/errors.zod.ts: HttpStatusErrorCodeMap, standardErrorCodeForHttpStatus, ErrorHttpStatusMap)", "ref": "#24649" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -667,7 +685,7 @@ "title": "Bulk write doors (createMany/updateMany/deleteMany, per-object batch): per-row outcomes, size cap, single/bulk parity, no silent full-table writes", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -682,7 +700,7 @@ ] }, "steps": [ - "boot showcase isolated; as the member POST /api/v1/data/showcase_private_note/createMany with a BARE JSON ARRAY of 3 notes with unique-marker titles (the body is the array; a {records} wrapper answers 400 VALIDATION_FAILED); GET them back", + "boot the showcase on its own port and DB; as the member POST /api/v1/data/showcase_private_note/createMany with a BARE JSON ARRAY of 3 notes with unique-marker titles (the body is the array; a {records} wrapper answers 400 VALIDATION_FAILED); GET them back", "single/bulk parity: POST a 4th note via the single door (POST /api/v1/data/showcase_private_note) and diff its response/stored shape against one createMany row (same fields stamped: owner_id, timestamps, defaults)", "as the member POST /api/v1/data/showcase_private_note/updateMany {\"records\": [{\"id\": \"\", \"data\": {…}}, …]} naming ONLY the marker rows' ids — updateMany is id-based (UpdateManyRequestSchema); a {where, data} body answers 400 VALIDATION_FAILED naming `records`; re-read", "as the member POST updateMany naming ids that match nothing the member may touch: a nonexistent id, and the id of another user's private note (invisible to the member) — in SEPARATE calls, or in one call with options.continueOnError: true: under the default (continueOnError: false) the first failed row ends the run and the second id reports NOT_ATTEMPTED, which says nothing about the no-existence-oracle parity; capture the per-row results, then re-read the foreign note system-side. Also send {\"records\": []}", @@ -769,6 +787,12 @@ "date": "2026-10-19", "change": "steps[3] and acceptance[2].verify send the two ids so each gets an answer — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.bulk-write-contract step 4 / acceptance[2]): updateMany stops at the first failed row by default, so one batch [nonexistent, foreign] reports the foreign id NOT_ATTEMPTED. Re-derived at base 9a7838edf5: BatchOptions continueOnError defaults false (packages/spec/src/api/batch.zod.ts). No clause changed", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -962,7 +986,7 @@ "title": "Metadata-authored `api` endpoints mount as real URLs with their declared auth/cache policy", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -983,7 +1007,7 @@ "script/proxy target — refused at authoring, never served; the runtime's 501 is a second door, pinned" ], "steps": [ - "boot showcase isolated; sign in as admin", + "boot the showcase on its own port and DB; sign in as admin", "GET /api/v1/apps/showcase/tasks authed; capture status, body, and response headers", "GET the same endpoint unauthenticated; capture status", "a script / proxy target cannot be authored into a live server. `api` is code-only (#5488: allowRuntimeCreate false, so no scratch package can hold one), and the stack schema refuses `type: 'script'` / `'proxy'`. Author one in a scratch config and run `os validate` (or `os build`); capture the refusal naming the endpoint, the type and the flow prescription. Do not attempt a live 501", @@ -1049,6 +1073,12 @@ "date": "2026-10-16", "change": "the fixture text named two live declarative endpoints while the showcase declares three and os build reports '3 APIs' (#23878 checklist-accuracy finding). Re-measured on main c91af8ed6: apis/index.ts declares showcase_task_feed (object_operation), showcase_inquiry_purge_api and showcase_recalc_estimates_api (both flow). Wording only, no re-run owed; the feed's live answers still hold (authed 200 with Cache-Control: private, max-age=30; anonymous 401 UNAUTHENTICATED; an unmatched /apps path 404)", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1057,7 +1087,7 @@ "title": "Package REST lifecycle: POST /packages creates (201), a duplicate name is refused 409 (no silent manifest clobber), PATCH partial-patches the manifest, and an explicit overwrite re-install replaces in place without duplicating", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -1073,7 +1103,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as admin; choose a scratch id com.example.qa-pkg-lifecycle-probe", + "boot the showcase on its own port and DB; sign in as admin; choose a scratch id com.example.qa-pkg-lifecycle-probe", "POST /api/v1/packages with { manifest: { id: 'com.example.qa-pkg-lifecycle-probe', name: 'QA Probe', version: '1.0.0', scope: 'project', type: 'app' } }; capture status + body; then GET /api/v1/packages/com.example.qa-pkg-lifecycle-probe to confirm it persisted (the dispatcher install lands in BOTH the in-memory registry and durable sys_packages) (scope must be one of cloud | system | project — 'custom' answers 400 VALIDATION_ERROR naming manifest.scope — and the install derives a namespace from the id)", "POST the SAME manifest again with NO overwrite flag; capture status + body", "PATCH /api/v1/packages/com.example.qa-pkg-lifecycle-probe with { name: 'QA Probe Renamed', description: 'edited', version: '1.1.0' }; GET the package back and confirm the patch persisted and that id/scope/type and the lifecycle fields (enabled/status/installedAt) are untouched", @@ -1153,6 +1183,12 @@ "date": "2026-10-16", "change": "step 2's manifest used scope 'custom', which POST /api/v1/packages refuses 400 VALIDATION_ERROR ('manifest.scope: Invalid option: expected one of cloud|system|project'), so the install the rest of the item builds on never happened (#23878 checklist-accuracy finding). It now uses scope 'project'. Clause 4 pinned two message sentences, one of which had changed ('version must be a SemVer 2.0.0 string …'); it now scores status 400 and code VALIDATION_ERROR for all three bodies. Re-run on main c91af8ed6: 201 install, 409 RESOURCE_CONFLICT duplicate, partial PATCH 200, the three malformed PATCHes 400 VALIDATION_ERROR, overwrite re-install 201 with exactly one row", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1161,7 +1197,7 @@ "title": "Developer API Console (a deep link on a stock boot): the endpoint tree mirrors /discovery, a seeded-object GET executes with live JSON + status + timing, and a malformed body surfaces the server error envelope verbatim", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": [ @@ -1175,7 +1211,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as admin; open /_console/apps/setup/developer/api-console directly (the Studio → Developer → API Console nav lives only in the Studio app, which a stock boot does not serve — fixtures.requires[0])", + "boot the showcase on its own port and DB; sign in as admin; open /_console/apps/setup/developer/api-console directly (the Studio → Developer → API Console nav lives only in the Studio app, which a stock boot does not serve — fixtures.requires[0])", "GET /api/v1/discovery out of band and record its services + routes maps — the ground truth the tree is gated on (useApiDiscovery gates each service group on isServiceUsable(discovery.services[name]), ADR-0076 D12)", "screenshot the endpoint tree AFTER the 'Discovering APIs' spinner settles; only then read the DOM", "spot-check the tree against REST_ROUTE_LEDGER families: a seeded object (showcase_task) shows its CRUD family (GET/POST/PATCH/DELETE /api/v1/data/showcase_task) under a Data group, the Metadata group lists /api/v1/meta/object/showcase_task, System lists discovery/packages/health", @@ -1242,6 +1278,12 @@ "date": "2026-10-13", "change": "steps[0], fixtures.requires[0] and the title reach the page by deep link (stale step, #23163 C0; parked on #23121, carried by #23169). The 'Studio → Developer → API Console' nav entry, nav_api_console, is declared only in packages/platform-objects/src/apps/studio.app.ts, and a stock boot does not serve the Studio app. QA run #23163 reached /_console/apps/setup/developer/api-console by URL and passed every clause there", "ref": "#23169" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1565,7 +1607,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": 5, + "revision": 6, "priority": "P0", "surface": "api", "personas": [ @@ -1586,7 +1628,7 @@ ] }, "steps": [ - "boot showcase isolated on the real HTTP stack (os dev); sign in as admin", + "boot the showcase on its own port and DB, on the real HTTP stack (os dev); sign in as admin", "disable the probe: POST /api/v1/actions/_activation/showcase_task/showcase_mark_done {\"enabled\": false} → 200 (the door's own arms are api-backend.action-activation-door-contract's item — do not re-prove them here); confirm the row via GET /api/v1/data/sys_metadata_activation (metadata_type 'action', name showcase_mark_done, active false) and confirm the row's KEY SET carries NO organization_id at all — the ledger has no tenant column (#15024 / ADR-0131 D7). ⛔ Never confirm the tenant half by reading the value: a check of the shape `row.organization_id ?? null` answers `null` for a column that does not exist, so it passes while measuring nothing", "REST door: POST /api/v1/actions/showcase_task/showcase_mark_done with {\"recordId\": \"no-such-record-0000\"}; capture status + body; then re-read a REAL seeded task and confirm no done/progress write happened anywhere", "param-ordering discriminator: disable showcase_action_param_gallery (POST /actions/_activation/showcase_field_zoo/showcase_action_param_gallery {\"enabled\":false}), then invoke POST /actions/showcase_field_zoo/showcase_action_param_gallery with params: {\"bogus_key\": 1} (missing required p_text AND an unknown key) plus a nonexistent recordId; capture. Re-enable and repeat the IDENTICAL request; capture the contrast", @@ -1700,6 +1742,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0] no longer falls back to the unit pin for the MCP clause. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, the MCP clause's unit fallback), read under RUNNER rule 6. acceptance[4]'s oracle is the MCP tool's answer. The 'door 2' case of packages/runtime/src/domains/action-activation-dispatch.test.ts calls invokeBusinessAction in-process over a stubbed engine and metadata service (read at base feb09db14a), so where the endpoint is not mounted the clause is blocked(environment), with the pin beside it. No clause changed", "ref": "#24963" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated on ' → 'boot the showcase on its own port and DB, on '. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1708,7 +1756,7 @@ "title": "POST /actions/_activation/:object/:action — the activation door's ordered arms: authority before body and lookup, the exact 400 shapes, 404 undeclared, 409 ambiguity, 501/503 honesty, and the reserved segment surviving the real mount", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "api", "personas": [ @@ -1730,7 +1778,7 @@ ] }, "steps": [ - "boot showcase isolated (os dev — the LIVE mount matters here, see the trap note); sign in as admin and as the non-admin member", + "boot the showcase on its own port and DB (os dev — the LIVE mount matters here, see the trap note); sign in as admin and as the non-admin member", "authority-first probe, as the NON-ADMIN: POST /api/v1/actions/_activation/showcase_task/no_such_action with body {\"enable\": false} — a request that is simultaneously mis-bodied AND undeclared; capture. Repeat the same request anonymous; capture", "shape arms, as admin: POST /api/v1/actions/_activation/showcase_mark_done {} (2-segment); POST /api/v1/actions/_activation/showcase_task/showcase_mark_done/extra {} (4-segment); capture both", "body arms, as admin, against the declared action: {\"enable\": false} (one letter off); {\"enabled\": \"false\"} (string, not boolean); [] (non-object body); {} (empty); capture each with full envelope", @@ -1838,6 +1886,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[2] re-pointed — QA run #24783 (finding F5), round 11 of wave anchor #24579. 'Needs an enterprise tenancy runtime no example app composes' is stale since 18.x step 4 (#24580): a scratch app declaring the open @objectstack/organizations composes the walled posture (RUNNER.md 'A walled boot is a fixture of its own'), and QA run #24783 measured the authority-first order on both walled postures. The line now points at that entry and names the handler whose order it asserts. No clause changed", "ref": "#24801" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1846,7 +1900,7 @@ "title": "Server-side aggregation contract: every AggregationFunction × DateGranularity gives known answers — ruled empty-set values, ISO-Monday week buckets, retired array_agg/string_agg refuse with prescriptions, and aggregate stays secured (engine-only, never raw driver)", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "api", "personas": [ @@ -1870,7 +1924,7 @@ }, "steps": [ "read the two enums and pin the matrix: AggregationFunction (packages/spec/src/data/query.zod.ts — 6 members: count/sum/avg/min/max/count_distinct, with retirement error messages wired for array_agg/string_agg) and DateGranularity (— 5 members: day/week/month/quarter/year). Only ONE enumSource pin fits an item; AggregationFunction carries it — hand-count DateGranularity's 5 members at run time and record the count", - "boot showcase isolated; as admin read the unfiltered showcase_account baseline (generous $top) — the local ground truth every expected value below is computed from", + "boot the showcase on its own port and DB; as admin read the unfiltered showcase_account baseline (generous $top) — the local ground truth every expected value below is computed from", "function matrix: for each of the 6 functions POST /api/v1/data/showcase_account/query with { groupBy: ['status'], aggregations: [{ function, field: 'annual_revenue', alias: 'v' }] } (count also in its fieldless/'*' spelling), plus one UNgrouped run per function; compute each expected per-group value locally from the baseline and compare exactly — known answers, not smoke 200s. Add the #11152 leg: min/max over a boolean field answer the NUMBER it is worth (0/1, maintainer ruling 2026-08-28), never false/true", "granularity matrix: record wall clock + timezone, then for each of the 5 granularities POST { groupBy: [{ field: 'signed_on', dateGranularity }], aggregations: [{ function: 'count', alias: 'n' }] }; reconcile the returned bucket KEYS and counts against locally computed ISO-8601 buckets (bucketDateValue's own shapes: 'YYYY', 'YYYY-Qn', 'YYYY-MM', 'YYYY-MM-DD', 'YYYY-Wnn' with weeks starting Monday / week 1 = first Thursday); rows with null/unparseable dates must land in ONE real-null-keyed bucket (#3839 — never a '(null)' sentinel string). The TIMEZONE lever is not on this door: POST /api/v1/data/showcase_account/query refuses a `timezone` key 400 INVALID_FIELD (the protocol does not forward one; engine.aggregate reads only query.timezone). The one wire lever is POST /api/v1/analytics/query with `timezone` — on the stock cube showcase_delivery, a count measure over its time dimension due_date at granularity day, once at UTC and once in a negative-offset zone such as America/Los_Angeles (QA run #24595: the answer's data.sql is present on the driver path and absent on the in-memory one, and the SQL echo refuses a non-UTC bucket)", "empty-set semantics: (a) a query whose where matches nothing, grouped and ungrouped; (b) a per-aggregation filter (#10576 — aggregations[].filter, SQL FILTER(WHERE …) semantics) that empties one measure while a sibling measure keeps the full bucket. Expected per the platform ruling (emptyGroupValueFor, packages/spec/src/data/aggregation-policy.ts): count/count_distinct/sum → 0, avg/min/max → null — a 0 flattened onto avg (or a null on count) is a FAIL either direction (objectui#3136)", @@ -1971,6 +2025,12 @@ "date": "2026-10-19", "change": "steps[3], steps[5], knownGaps[1] and negative[2] name the doors that answer — checklist-accuracy findings of the 18.0 QA sweep (QA run #24541 step 6, and QA run #24595 knownGaps[1] / step 4 / negative[2], api-backend.aggregate-contract-matrix; wave anchor #24579 §3, comment 6272314386). On POST /data/:object/query a non-array aggregations is refused by the request schema before the protocol's worked example, and that door carries no timezone; the lever is POST /api/v1/analytics/query on cube showcase_delivery / due_date. negative[2] now says a date dimension's labels are zone-independent; that row's FAIL was the product defect #24618, which PR #24642 (787939130c) fixed on main after this round's base; the clause is unchanged. Re-derived at base 9a7838edf5 (protocol.ts assertAggregationFieldsExist, objectql engine.ts ENGINE_AGGREGATE_OPTION_KEYS, spec data/analytics.zod.ts timezone, examples/app-showcase/src/data/analytics/showcase.cube.ts)", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[1] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2216,7 +2276,7 @@ "title": "RestServerConfig.crud is the CRUD surface's construction contract: five switches gate six mounts, dataPrefix moves the routes and their discovery advertisement together, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -2235,7 +2295,7 @@ ] }, "steps": [ - "boot showcase isolated; as admin fire the six default-config data mounts and capture status per route: GET /api/v1/data/showcase_task (list), GET /api/v1/data/showcase_task/ (read), POST /api/v1/data/showcase_task (create), POST /api/v1/data/showcase_task/query (the QueryAST door), PATCH /api/v1/data/showcase_task/ (update), DELETE /api/v1/data/showcase_task/ (delete)", + "boot the showcase on its own port and DB; as admin fire the six default-config data mounts and capture status per route: GET /api/v1/data/showcase_task (list), GET /api/v1/data/showcase_task/ (read), POST /api/v1/data/showcase_task (create), POST /api/v1/data/showcase_task/query (the QueryAST door), PATCH /api/v1/data/showcase_task/ (update), DELETE /api/v1/data/showcase_task/ (delete)", "fire the routing-404 control (GET /api/v1/definitely-not-a-route) and keep its body — every absence verdict below is scored against this shape, never against a bare status", "GET /api/v1/discovery and read routes.data — it must equal the base plus the configured dataPrefix", "harness: construct a RestServer with { crud: { operations: { create: false } } }, registerRoutes(), and read getRoutes() — POST {dataPrefix}/:object must be absent while the other five mounts remain", @@ -2330,6 +2390,12 @@ "date": "2026-10-19", "change": "knownGaps[1], automated.ref and acceptance[2] corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24541, api-backend.rest-crud-config-contract knownGaps[1] + automated.ref and acceptance[2]; wave anchor #24579 §3). \"The MOUNT half has no permanent pin\" is stale: rest-config-mount-table.pin.test.ts (#15544) pins every crud.operations.* radius, measured at base 9a7838edf5 under os-verify-lock (24 passed). acceptance[2] was an api clause no stock boot can observe (knownGaps[0] makes every non-default clause harness-scored) and its dispatcher-vs-hono-route trap is not discharged by a RestServer-only table, since the dispatcher's data domain also serves /data; it now reads oracle test and says so. Re-derived at base 9a7838edf5; FOLLOW-UPS.md is not edited here", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2338,7 +2404,7 @@ "title": "RestServerConfig.batch is the bulk surface's construction contract: three switches and enableBatchEndpoint gate four per-object mounts (each ANDed with its protocol member), maxBatchSize is the one cap every bulk door measures, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -2358,7 +2424,7 @@ ] }, "steps": [ - "boot showcase isolated; as admin fire the four per-object bulk doors on the default config and capture status: POST /api/v1/data/showcase_private_note/batch, /createMany, /updateMany, /deleteMany; fire the routing-404 control alongside them", + "boot the showcase on its own port and DB; as admin fire the four per-object bulk doors on the default config and capture status: POST /api/v1/data/showcase_private_note/batch, /createMany, /updateMany, /deleteMany; fire the routing-404 control alongside them", "fire the cross-object door POST /api/v1/batch on the same boot — it is NOT gated by enableBatchEndpoint and must answer whether or not the per-object door is switched off", "cap probe on the live boot: POST /createMany with 201 records against the default cap of 200; capture the status, code and the reported max", "harness: construct with { batch: { enableBatchEndpoint: false } }, registerRoutes(), read getRoutes() — POST {dataPrefix}/:object/batch is absent, the three *Many mounts and POST {basePath}/batch remain", @@ -2459,6 +2525,12 @@ "date": "2026-10-19", "change": "knownGaps[1] and automated.ref corrected — checklist-accuracy finding of the 18.0 QA sweep (QA run #24541, api-backend.rest-batch-config-contract knownGaps[1] + automated.ref; wave anchor #24579 §3): the mount half has a permanent pin, rest-config-mount-table.pin.test.ts (#15544), covering batch.enableBatchEndpoint, operations.*Many and the protocol-member conjunct — measured at base 9a7838edf5 under os-verify-lock (24 passed). No clause changed; FOLLOW-UPS.md is not edited here", "ref": "#24649" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2467,7 +2539,7 @@ "title": "RestServerConfig.metadata is the metadata surface's construction contract: prefix moves eleven mounts and their discovery advertisement, four endpoint switches each gate exactly the face its name states (#15542/#15854), maskObjectFields is the ADR-0106 D8 disclosure gate, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -2488,7 +2560,7 @@ ] }, "steps": [ - "boot showcase isolated; as admin fire the default-prefix metadata mounts and capture status: GET /api/v1/meta, /meta/types, /meta/diagnostics, /meta/_drafts, GET /meta/object, GET /meta/object/showcase_task, /meta/object/showcase_task/references, /meta/object/showcase_task/layers, /meta/book//tree; fire the routing-404 control alongside", + "boot the showcase on its own port and DB; as admin fire the default-prefix metadata mounts and capture status: GET /api/v1/meta, /meta/types, /meta/diagnostics, /meta/_drafts, GET /meta/object, GET /meta/object/showcase_task, /meta/object/showcase_task/references, /meta/object/showcase_task/layers, /meta/book//tree; fire the routing-404 control alongside", "GET /api/v1/discovery and read routes.metadata — it must equal the base plus the configured prefix", "as the restricted persona (a member bound to the client_liaison position) GET /api/v1/meta/object/showcase_project and diff the served fields against the admin's copy of the same document", "harness: construct with { metadata: { prefix: '/metadata' } } and confirm every metadata mount re-bases and the discovery body advertises the new prefix", @@ -2601,6 +2673,12 @@ "date": "2026-10-19", "change": "steps[8], acceptance[4] (clause, verify) and personas[1] corrected — checklist-accuracy findings of the 18.0 QA sweep (QA run #24541, api-backend.rest-metadata-config-contract acceptance[4] / step 9 and personas[1]; wave anchor #24579 §3). The enableCache carve-outs are SEVEN: a ?member= read also bypasses getMetaItemCached (packages/rest/src/meta-item-read-routes.ts, memberScoped, #22877, at base 9a7838edf5); and personas[1] still named showcase_account, which revision 4 moved to showcase_project / client_liaison everywhere else in the item", "ref": "#24649" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index 9bb16bf8aa..2d681e61af 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -9,7 +9,7 @@ "title": "Per-group sign-off (会签) needs one approval from EACH group", "since": "v16", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "browser", "personas": ["approver holding exactly one group (e.g. manager)", "second approver holding the other group (e.g. finance/auditor)"], @@ -25,7 +25,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin (the manager-group holder; NOT in the finance group of this node)", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin (the manager-group holder; NOT in the finance group of this node)", "GET /api/v1/approvals/requests?status=pending and locate the EXP-2001 request opened by showcase_expense_signoff's per_group node (groups manager + finance)", "GET /api/v1/approvals/requests/:id — record status, pending_approvers, pending_approver_names, pending_approver_groups, and the per-group tallies", "open the inbox the way a user reaches it — the Approvals entry in the account app's Inbox group, i.e. /_console/apps/com.objectstack.account/component/approvals/inbox (待我审批 tab) — NOT the bare /system/approvals deep-link route (#7234; that route stays supported and is exercised by approvals.notification-deep-link); open the request drawer; screenshot the server-computed group chips", @@ -96,6 +96,12 @@ "date": "2026-10-19", "change": "acceptance[3].verify corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.per-group-signoff acceptance[3]; wave anchor #24579 §3): it asked for 'the approve branch taken' in the run read, but end nodes log no step and showcase_expense_signoff's approve edge ends at an end node, so no run read can show the branch. The verify now scores the run status transition and the request status, as approvals.quorum-m-of-n already words the same evidence. Clause text unchanged", "ref": "#24632" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -104,7 +110,7 @@ "title": "M-of-N quorum approves at the threshold; one rejection vetoes", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "browser", "personas": ["three DISTINCT users each holding one of the approver positions"], @@ -120,7 +126,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "runnable today (the clamp contrast): GET the seeded showcase_committee_quorum request on EXP-DEMO (behavior quorum, minApprovals 2 over manager/finance/legal — all resolving to the admin); record the collapsed pending slate", "approve it ONCE as the admin and re-read: it finalizes immediately — the documented runtime clamp (minApprovals can never exceed the resolvable approver count), recorded as the CONTRAST, never as M-of-N proof", "apply the runtime fixture (fixtures.requires, steps 1-4) and open a FRESH quorum request; then as approver 1, POST /api/v1/approvals/requests/:id/approve — re-read: status STILL pending (1 of 2), approver 1 dropped from the slate", @@ -188,6 +194,12 @@ "date": "2026-10-19", "change": "fixtures.requires[1] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24542, approvals.quorum-m-of-n; wave anchor #24579 §3): in a shared boot an earlier item (approvals.account-app-entry's runtime persona) had bound another member to 'finance', so the committee slate resolved four distinct holders; the run deleted that binding before opening the quorum requests. The fixture now says to clear such bindings first. No clause changed", "ref": "#24632" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -196,7 +208,7 @@ "title": "Inbox actions are metadata-driven and gated by the viewer's relationship", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": ["an approver who is NOT the submitter and NOT an override actor (no platform-admin or tenant-admin standing), so record.viewer.can_override is false on the request", "the dev admin: the submitter of the invoice request (the own-request case) and an OVERRIDE actor (a platform admin — record.viewer.can_override is true on any PENDING request), for the override half of clause 2"], @@ -209,7 +221,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin for the own-request case and the override half, and as the non-override approver (personas) for the other-submitter case", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin for the own-request case and the override half, and as the non-override approver (personas) for the other-submitter case", "other-submitter case, as the NON-override approver: reach the inbox the way a user does — the Approvals entry in the account app's Inbox group (/_console/apps/com.objectstack.account/component/approvals/inbox), NOT the bare /system/approvals deep link (#7234) — open the other-submitter request from the 待我审批 tab; screenshot, then read the rendered action set from the drawer DOM. Then open the SAME pending request as the dev admin and read its action set too (the override half)", "own-request case: open the invoice request from the 我发起的 tab; screenshot, then read its action set", "GET /api/v1/approvals/requests/:id for both requests, as each viewer, and record the server-computed viewer flags (can_act, is_submitter, can_override) and status", @@ -264,7 +276,8 @@ { "revision": 1, "date": "2026-08-07", "change": "initial import from the #3358 evidence run (7-action table proven after #3411 stamped real submitters)", "ref": "#3358" }, { "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 3, "date": "2026-08-10", "change": "entry path refreshed: the inbox is reached through the account app's Approvals nav entry (component route) instead of the bare /system/approvals deep link, so the item exercises the post-#7213 user path", "ref": "#7331" }, - { "revision": 4, "date": "2026-10-05", "change": "clause 2 re-pointed, with the personas, steps 1, 2 and 4, the clause-3 verify and a fixtures line (stale, #21782 VF4). In sys-approval-request.object.ts, approval_recall's visible is the submitter arm OR record.viewer.can_override == true (the #3424 override lever, button added by #12716), which platform/tenant admins hold on any pending request (isOverrideActor); approval_remind stays submitter-only. Revision 3 (2026-08-10) predates #12716. The verifier's wording is used for the clause. The run's approver persona was the dev admin, an override actor, so against that persona the old clause also read as an invalid pairing. The personas now name a non-override approver, and the admin's override view is asserted as its own leg", "ref": "#21797" } + { "revision": 4, "date": "2026-10-05", "change": "clause 2 re-pointed, with the personas, steps 1, 2 and 4, the clause-3 verify and a fixtures line (stale, #21782 VF4). In sys-approval-request.object.ts, approval_recall's visible is the submitter arm OR record.viewer.can_override == true (the #3424 override lever, button added by #12716), which platform/tenant admins hold on any pending request (isOverrideActor); approval_remind stays submitter-only. Revision 3 (2026-08-10) predates #12716. The verifier's wording is used for the clause. The run's approver persona was the dev admin, an override actor, so against that persona the old clause also read as an invalid pairing. The personas now name a non-override approver, and the admin's override view is asserted as its own leg", "ref": "#21797" }, + { "revision": 5, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -343,7 +356,7 @@ "title": "An approval notification deep-links straight into the request drawer", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "browser", "personas": ["any pending approver"], @@ -352,7 +365,7 @@ "requires": ["at least one pending request on which the signed-in approver can be notified — through the OPENING (since PR #22625 opening a request emits approval.requested to each concrete approver on the slate it opens on) or through REASSIGN (POST /api/v1/approvals/requests/:id/reassign {to}). ⚠ REMIND is SUBMITTER-only: POST …/:id/remind answers 403 'FORBIDDEN: only the submitter may send reminders' to anyone else (packages/plugins/plugin-approvals/src/approval-service.ts remind), and the dev admin is an APPROVER on the seeded demo requests, not their submitter — so as the admin, mint the notification by opening a fresh request (or by reassign), never by remind; the three seeded demo requests still suffice as a reassign source"] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin (a pending approver on the seeded requests)", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin (a pending approver on the seeded requests)", "mint the notification: open a fresh request the admin approves (the opening emits approval.requested to each concrete approver on its slate, PR #22625) — or POST /api/v1/approvals/requests/:id/reassign {to: } on a pending request. Not /remind: it is submitter-only and answers the admin 403 'FORBIDDEN: only the submitter may send reminders' on the seeded requests, which a run must not score as a deep-link FAIL", "read the resulting notification row and record its actionUrl — it must carry /system/approvals?request= (#2678 P1.5; ApprovalService.notify rewrites the bare /system/approvals into the deep link centrally, so every emitting topic inherits it)", "cold-load that exact URL in a fresh page (no prior navigation); wait for settle; capture the drawer that opens", @@ -409,7 +422,8 @@ { "revision": 3, "date": "2026-08-10", "change": "added the route-parity clause: after #7213 the same ?request= link must open the drawer on BOTH /system/approvals (what notifications carry) and the component route the account app mounts", "ref": "#7331" }, { "revision": 4, "date": "2026-08-11", "change": "fixtures re-anchored on the remind/reassign path — the old requires rested on a request's OPEN notifying somebody, and no approval.opened topic exists; and the cold-load clause's evidence restated observably (drawer field-matched against the API instead of 'screenshots with the URL visible', which the console's consumption of the query param makes unmeetable)", "ref": "#7530" }, { "revision": 5, "date": "2026-10-10", "change": "the open-time gap is closed: since PR #22625 (#22607) openNodeRequest publishes approval.requested to each concrete approver on the slate the request opens on, through the same notify() deep-link rewrite — the knownGaps entry that said opening notifies nobody is removed, the source note names the topic, step 2 and the fixture note say the opening is a second source; the fixtures stay on remind/reassign (deterministic on demand) and the revision-4 row stands as history", "ref": "#22631" }, - { "revision": 6, "date": "2026-10-13", "change": "fixtures.requires[0] and steps[1] stop sending the admin through /remind (assertion defect, #23163 A0; parked on #23121, carried by #23169). ApprovalService.remind refuses every caller but the request's submitter, 'FORBIDDEN: only the submitter may send reminders', and the dev admin is an approver on the seeded requests. QA run #23163 met the 403 and minted the notification through the opening path instead, then passed every clause. The opening path (PR #22625) and /reassign are the two notification sources an approver can drive", "ref": "#23169" } + { "revision": 6, "date": "2026-10-13", "change": "fixtures.requires[0] and steps[1] stop sending the admin through /remind (assertion defect, #23163 A0; parked on #23121, carried by #23169). ApprovalService.remind refuses every caller but the request's submitter, 'FORBIDDEN: only the submitter may send reminders', and the dev admin is an approver on the seeded requests. QA run #23163 met the 403 and minted the notification through the opening path instead, then passed every clause. The opening path (PR #22625) and /reassign are the two notification sources an approver can drive", "ref": "#23169" }, + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -417,7 +431,7 @@ "title": "Every approval action executes its REST route and produces the expected state transition plus a timeline entry", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "mixed", "personas": ["dev admin (pending approver on the seeded requests; submitter of the invoice request)"], @@ -434,7 +448,7 @@ }, "variants": ["approve", "reject", "reassign", "revise (send-back)", "resubmit", "recall", "remind", "request-info", "comment"], "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "approve: POST /api/v1/approvals/requests/:id/approve {comment} on the EXP-DEMO request; re-read status + run", "reject: raise a fresh budget-approval request (PATCH a project budget to e.g. 200000); POST /:id/reject {comment}; re-read — the flow resumes down its reject edge", "revise: raise another budget-approval request; POST /:id/revise {comment} as the pending manager — re-read status; GET the flow run (parked at the approval_revise node 'wait_revision')", @@ -542,6 +556,12 @@ "date": "2026-10-20", "change": "automated removed; fixtures.knownGaps[0] rewritten and fixtures.knownGaps[1] added; acceptance[9].verify's envelope corrected — QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: the sweep-wide rule-6 contradiction at approvals acceptance[9], and 'the pin also asserts negative[1]'), round 15 of wave anchor #24579. (1) The ref claimed acceptance[9] from approval-service.test.ts's #12769 describe, and knownGaps[0] said that pin is how acceptance[9] is scored. The pin drives ApprovalService over a fake engine, and the clause scores the wire answer, so by RUNNER rule 6 (a pin scores only the layer it drives) it is evidence beside a blocked(fixture) verdict. (2) Its throttle cases assert negative[1] at the service, and rest-approvals-wire-codes.test.ts maps THROTTLED to 429 over a mocked service; neither drives the remind door, so negative[1] joins no ref. (3) No full clause is pinned, so automated is dropped (theme-B rule). (4) Located while rewriting knownGaps[0]: acceptance[9].verify said READ_BACK_FAILED rides the generic template-generated 500 and is deliberately absent from handleApprovalError's prefix map. It has been a named 500 row there since 5b3ff63cc (packages/rest/src/approvals-routes.ts), pinned by rest-approvals-wire-codes.test.ts. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -550,7 +570,7 @@ "title": "An approval-parked run cannot be resumed through the generic automation route — decisions go through the approvals service only", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": ["dev admin (holds finance + legal, so the seeded unanimous request is decidable)"], @@ -559,7 +579,7 @@ "requires": ["showcase_invoice_signoff parked at its aggregating approval node (the seeded invoice request, or a fresh one via PATCHing an invoice draft→sent)"] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "locate the paused run: GET /api/v1/automation/showcase_invoice_signoff/runs — status=paused at the dual_signoff approval node; note the runId and the matching approval request id", "forge the generic resume: POST /api/v1/automation/showcase_invoice_signoff/runs/:runId/resume with {} and again with {branchLabel: 'approve'}", "re-read the run and the approval request after each forged attempt", @@ -603,7 +623,8 @@ "packages/spec/src/automation/approval.zod.ts#APPROVAL_NODE_TYPE (APPROVAL_NODE_TYPE; ADR-0039 Track A aggregating node)" ], "history": [ - { "revision": 1, "date": "2026-08-07", "change": "initial — pins the #3801 resume-authority gate as a both-sides item (approval 403 + screen 2xx contrast)", "ref": "claude/platform-test-checklist-ocwugl" } + { "revision": 1, "date": "2026-08-07", "change": "initial — pins the #3801 resume-authority gate as a both-sides item (approval 403 + screen 2xx contrast)", "ref": "claude/platform-test-checklist-ocwugl" }, + { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -611,7 +632,7 @@ "title": "A decision's typed outputs route the next stage: expression approvers resolve from the previous decision at node entry", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": ["dev admin (org-membership owner — the stage-1 approver of the dynamic-approval demo)"], @@ -626,7 +647,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "PATCH a showcase_announcement's title over /api/v1/data — showcase_dynamic_approval opens its stage-1 Lead Review request routed at the org owner", "GET /api/v1/approvals/requests/:id — record status and the declared decisionOutputs contract on the request/node", "open the request drawer; screenshot the decision dialog's next_reviewers control — it must be a sys_user MULTI-SELECT picker, not a free-text box", @@ -680,7 +701,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-07", "change": "initial — covers the #3447 dynamic-routing chain (typed decision outputs → vars → expression approvers) with the required-output gate", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-08-11", "change": "the vars clause now names an oracle that is actually readable: stage-2 pending_approvers cardinality + order across a 1-pick and a 2-pick run, instead of 'the outputs present in the run variables snapshot' — a paused run's detail carries no variables key at all (structural), which was the sole reason #7517 scored this item PARTIAL. Gap recorded in knownGaps rather than left to be rediscovered", "ref": "#7530" } + { "revision": 2, "date": "2026-08-11", "change": "the vars clause now names an oracle that is actually readable: stage-2 pending_approvers cardinality + order across a 1-pick and a 2-pick run, instead of 'the outputs present in the run variables snapshot' — a paused run's detail carries no variables key at all (structural), which was the sole reason #7517 scored this item PARTIAL. Gap recorded in knownGaps rather than left to be rediscovered", "ref": "#7530" }, + { "revision": 3, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -688,7 +710,7 @@ "title": "An active out-of-office delegation reroutes an individually-routed approver to the delegate; expiring the window hands the slot back", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "mixed", "personas": ["dev admin — the DELEGATOR A: the delegation row is written BY them FOR them (sys_approval_delegation is self-service-only, so the delegator must be whoever is authenticated)", "a SECOND REAL runtime account — the DELEGATE B (Ada Auditor usr_showcase_auditor_demo, provisioned as a real login per knownGaps), who holds their own bearer token and decides the rerouted request as themselves"], @@ -705,7 +727,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); provision B as a real login — a stock boot already does it, see knownGaps — and hold B's bearer token separately from the admin's", + "boot the showcase on its own port and DB (dogfood §0); provision B as a real login — a stock boot already does it, see knownGaps — and hold B's bearer token separately from the admin's", "sign in as the dev admin (the delegator A)", "register (POST /api/v1/automation) a flow whose start is record-triggered on a stock object (start config objectName 'showcase_field_zoo' + triggerType 'record-after-create') and whose single approval node has approvers=[{type:'user', value:''}], behavior first_response", "baseline (no delegation): start a run by creating one record of the trigger object (POST /api/v1/data/showcase_field_zoo {name, f_master_detail: }); GET /api/v1/approvals/requests?status=pending, find the new request, GET /:id — pending_approvers must be [A]; GET /:id/actions shows NO ooo_substitute row", @@ -768,7 +790,8 @@ { "revision": 3, "date": "2026-08-18", "change": "delegate B needs no hand-provisioning any more: the showcase gives Ada Auditor a real better-auth credential account on a stock boot, so the issuer knownGap is rewritten as closed-by-fixture. The OTHER two gaps are untouched and still bind — the self-service-only delegation guard (#4839) and the writable/scratch package the individually-routed flow needs", "ref": "#9308" }, { "revision": 4, "date": "2026-09-19", "change": "same re-anchoring as the sibling items: the retired plugin-auth backfill citation is replaced by the retirement's migration entry in packages/spec and the reason is kept, the parenthetical 'issuer derived from the dev admin's own row' is withdrawn as false, and the step that told a runner to check issuer=local:credential when provisioning delegate B goes with it — that field no longer exists", "ref": "#17453" }, { "revision": 5, "date": "2026-10-13", "change": "fixtures.requires[0] and steps[2], [3], [5] and [8] start the scratch flow from a record instead of POST …/trigger (assertion defect, #23166 A0; parked on #23121, carried by #23169). The trigger route cannot open an approval: buildAutomationContext maps the body's record into params only, the approval node reads its record id from $record, which the engine seeds from context.record, and the run answers 400 'no record id in $record'. QA run #23166 drove the item through a record-triggered start (the approver-bindings.flow.ts shape) and passed every clause. Read from source at 6d7f4df3c: runtime domains/automation.ts buildAutomationContext, plugin-approvals approval-node.ts", "ref": "#23169" }, - { "revision": 6, "date": "2026-10-16", "change": "the knownGap 'authoring the scratch flow needs a writable/scratch package' is stale (#23878 checklist-accuracy finding): POST /api/v1/automation registers a runtime flow on the stock boot. The fixture line and step 3 now register the user-routed flow that way, record-triggered on the stock showcase_field_zoo, and the run steps create a showcase_field_zoo row to start it. Full re-run on main c91af8ed6 with B = Ada Auditor: baseline pending [A]; an active self-service A→B delegation reroutes the next request to [B] with one ooo_substitute row (actor null, 'A → B — Annual leave'); B's /notifications row is type approval.ooo_substituted with actionUrl /system/approvals?request=ID; B approves with her own token (actor B, via_override false); with valid_until in the past the next request routes back to [A] with no substitution", "ref": "#24036" } + { "revision": 6, "date": "2026-10-16", "change": "the knownGap 'authoring the scratch flow needs a writable/scratch package' is stale (#23878 checklist-accuracy finding): POST /api/v1/automation registers a runtime flow on the stock boot. The fixture line and step 3 now register the user-routed flow that way, record-triggered on the stock showcase_field_zoo, and the run steps create a showcase_field_zoo row to start it. Full re-run on main c91af8ed6 with B = Ada Auditor: baseline pending [A]; an active self-service A→B delegation reroutes the next request to [B] with one ooo_substitute row (actor null, 'A → B — Annual leave'); B's /notifications row is type approval.ooo_substituted with actionUrl /system/approvals?request=ID; B approves with her own token (actor B, via_override false); with valid_until in the past the next request routes back to [A] with no substitution", "ref": "#24036" }, + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -776,7 +799,7 @@ "title": "Approve/Reject render in the record header; Reject fires after ONE dialog; a locking approval hides inline edit", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": ["dev admin (holds the manager position, so showcase_budget_approval's manager_review routes to them — a pending approver standing on the record page)"], @@ -785,10 +808,13 @@ "requires": [ "showcase_budget_approval raised on a showcase_project by PATCHing budget above 100000 (budget != previous.budget) — parks at manager_review (approvers position 'manager' = the admin), lockRecord:false; driving budget above 500000 reaches exec_review, lockRecord:true (the locking contrast, objectui#2902)", "the record page surface: objectui packages/app-shell RecordDetailView + useRecordApprovals + buildApprovalDecisionActions" + ], + "knownGaps": [ + "the steps, clauses, fixtures.requires[1] and source[0]–[1] describe the record header's two hand-written decision buttons (buildApprovalDecisionActions: approve_request / reject_request in record_header, ordered -100 / -99, behind a client-side canDecide). objectui 99782f961 (objectui#3055) retired them before both console pins. At the console pin cf153eb2e535 the record page renders sys_approval_request's own declared actions through DeclaredActionsBar above the page body, not in the header: approval_approve, approval_reject and the other levers at location record_section, gated by the server's viewer block (can_act, can_override, is_submitter), in objectui RecordDetailView.tsx and recordApprovalActions.ts, over the declarations in packages/plugins/plugin-approvals/src/sys-approval-request.object.ts. Read each step and clause against that surface and record where it differs: this item's text has not been re-derived from it" ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "PATCH a showcase_project budget to e.g. 200000 over /api/v1/data — showcase_budget_approval opens and parks at manager_review (routed to manager = admin), lockRecord:false", "open that project's record page — /_console/apps/showcase_app/showcase_project/record/:id, the served record route (the shorthand /apps/showcase_app/showcase_project/:id still lands there: the console's ShorthandRecordRedirect rewrites it); screenshot the header — Approve and Reject must render in the primary slot (buildApprovalDecisionActions: Approve variant 'primary' order -100, Reject variant 'destructive' order -99, both locations ['record_header'])", "because manager_review is lockRecord:false, confirm inline-edit affordances remain live on this record (the editable contrast)", @@ -839,12 +865,13 @@ "source": [ "objectui packages/app-shell/src/views/RecordDetailView.tsx (buildApprovalDecisionActions — record_header Approve/Reject, single-dialog Reject #3126, actionParams comment #2955; approvalLocked/canEdit gating #2902)", "objectui packages/app-shell/src/hooks/useRecordApprovals.ts (recordLockedByApproval, canDecide, /approvals/requests decide routes)", - "objectui packages/app-shell/src/views/RecordDetailView.approvalDecisionActions.test.tsx (the param-contract pin)", + "objectui packages/app-shell/src/views/RecordDetailView.approvalDeclaredActions.test.tsx (objectui#3055, blob 9519ae70f743 at the console pin cf153eb2e535: the record page runs sys_approval_request's declared decision actions, and 'folds a declared decision output into the nested `outputs` body' asserts that a decision's comment and declared outputs reach the POST to the request) + packages/plugins/plugin-approvals/src/sys-approval-request.object.test.ts ('the confirm question rides `description`, not `confirmText`, so one decision opens one dialog (#7278)'). Between them they hold what the deleted RecordDetailView.approvalDecisionActions.test.tsx pinned, the decision params and one dialog per decision; objectui 99782f961 deleted that file before both console pins", "examples/app-showcase/src/automation/flows/index.ts#BudgetApprovalFlow (BudgetApprovalFlow — manager_review lockRecord:false / exec_review lockRecord:true on showcase_project)" ], "history": [ { "revision": 1, "date": "2026-08-08", "change": "initial — the record-page approval surface (distinct from the inbox-drawer items): header Approve/Reject, one-dialog Reject (#3126), decision round-trip, and the lockRecord-driven inline-edit hide (#2902)", "ref": "claude/platform-test-checklist-ocwugl" }, - { "revision": 2, "date": "2026-10-17", "change": "the both-sides clause and step 9 named no persona, and the natural choice — the dev admin on a record routed away from them — sees 'Override Approve' / 'Override Reject' (#23878 checklist-accuracy finding), so it can never show the closed side; the step and clause 5 now name a non-override member. The record route in step 3 is the served /_console/apps/showcase_app/showcase_project/record/:id (the shorthand form redirects to it). Re-run on main c91af8ed6 with the console at objectui f8267df93aba: on a project whose budget approval is pending for the admin, the admin's header shows Approve and Reject; a contributor-bound member's header shows neither; with the admin's slot reassigned away the admin's header shows Override Approve and Override Reject", "ref": "#24036" } + { "revision": 2, "date": "2026-10-17", "change": "the both-sides clause and step 9 named no persona, and the natural choice — the dev admin on a record routed away from them — sees 'Override Approve' / 'Override Reject' (#23878 checklist-accuracy finding), so it can never show the closed side; the step and clause 5 now name a non-override member. The record route in step 3 is the served /_console/apps/showcase_app/showcase_project/record/:id (the shorthand form redirects to it). Re-run on main c91af8ed6 with the console at objectui f8267df93aba: on a project whose budget approval is pending for the admin, the admin's header shows Approve and Reject; a contributor-bound member's header shows neither; with the admin's slot reassigned away the admin's header shows Override Approve and Override Reject", "ref": "#24036" }, + { "revision": 3, "date": "2026-10-20", "change": "source[2] re-pointed, and fixtures.knownGaps[0] added. PR #24982's pin finding: source[2] cited objectui's RecordDetailView.approvalDecisionActions.test.tsx, absent at both console pins. Read in a full objectui clone: objectui 99782f961 (objectui#3055) deleted it, and that commit is an ancestor of 616df6598b42 and of cf153eb2e535 (exit 0 both; control, cf153eb2e535 as an ancestor of 99782f961: exit 1). The same commit retired buildApprovalDecisionActions and moved the record page's decisions onto sys_approval_request's declared actions. At cf153eb2e535, RecordDetailView.approvalDeclaredActions.test.tsx holds the params half (the comment and declared outputs reach the POST to the request), and this repository's sys-approval-request.object.test.ts holds the one-dialog half. The knownGap tells a runner that the rest of the item still describes the retired header buttons. steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -852,7 +879,7 @@ "title": "A node's SLA escalation fires once past its timeout — the declared action runs and an escalate timeline row lands", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": ["dev admin"], @@ -870,7 +897,7 @@ }, "variants": ["reassign", "auto_approve", "auto_reject", "notify"], "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "start the server with the wall-clock offset shim loaded (NODE_OPTIONS=--require , offset 0 — see fixtures.requires); as the admin, register one flow per variant through POST /api/v1/automation (no writable package needed) whose approval node config.escalation = {enabled:true, timeoutHours:1, action:, notifySubmitter:true}, plus escalateTo:'' on the reassign flow", "trigger the flow; GET /api/v1/approvals/requests/:id — status=pending and the request carries sla_due_at = created_at + timeoutHours (the SLA is materialized on open)", "advance the shim's offset to +2h (past sla_due_at) and let ONE escalation sweep run: wait for the next 5-minute periodic sweep, or restart the server with the shim and offset kept so the boot catch-up sweep runs", @@ -930,7 +957,8 @@ "history": [ { "revision": 1, "date": "2026-08-08", "change": "initial — pins the ADR-0042 SLA escalation (declared action fires once past timeout + escalate timeline row); blocked on a clock-control timing harness (hour granularity), with the sla_due_at materialization and the strict-schema build clause runnable today", "ref": "claude/platform-test-checklist-ocwugl" }, { "revision": 2, "date": "2026-10-03", "change": "the escalate row's expected actor corrected: step 5 and the single-shot clause asserted actor SLA_ACTOR_ID, but under ADR-0118 D1 the sweep records no actor (actor_id null, absent on the GET /:id/actions read) and the escalate row is the attribution, so a run following the old text would report a false failure. Nothing else in the item changes", "ref": "#21517" }, - { "revision": 3, "date": "2026-10-05", "change": "blocked lifted: run #21845 drove every clause under a wall-clock offset shim loaded with NODE_OPTIONS=--require (Date and Date.now offset from a file, timers untouched, no product logic changed). The shim is written into fixtures.requires as a recipe, not committed as code; the clock knownGap is marked CLOSED-for-runs, not deleted. The item now registers its flows through POST /api/v1/automation (no writable package needed), one flow per escalation action, and names the 5-minute sweep interval plus the boot catch-up sweep that the advance-the-clock and idempotency steps wait on (card #21851 entry 2)", "ref": "#21845" } + { "revision": 3, "date": "2026-10-05", "change": "blocked lifted: run #21845 drove every clause under a wall-clock offset shim loaded with NODE_OPTIONS=--require (Date and Date.now offset from a file, timers untouched, no product logic changed). The shim is written into fixtures.requires as a recipe, not committed as code; the clock knownGap is marked CLOSED-for-runs, not deleted. The item now registers its flows through POST /api/v1/automation (no writable package needed), one flow per escalation action, and names the 5-minute sweep interval plus the boot catch-up sweep that the advance-the-clock and idempotency steps wait on (card #21851 entry 2)", "ref": "#21845" }, + { "revision": 4, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -938,7 +966,7 @@ "title": "A non-admin reaches the full Approvals Inbox from the account app's Approvals entry", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P0", "surface": "browser", "personas": ["a NON-ADMIN business user who is a pending approver and has no Setup access"], @@ -951,7 +979,7 @@ ] }, "steps": [ - "build the console at the pin and verify the stamp; boot showcase isolated (dogfood §0)", + "build the console at the pin and verify the stamp; boot the showcase on its own port and DB (dogfood §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", @@ -1023,6 +1051,12 @@ "date": "2026-10-19", "change": "fixtures.requires[2] and acceptance[6].verify corrected — two checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.account-app-entry; wave anchor #24579 §3). 'Make that invoice one the persona OWNS' did not say which field owns it: showcase_invoice's contributor RLS reads the text field `owner` against current_user.email (invoice_own_rows), so a persona-created invoice is unreadable to its creator until an admin sets `owner`. acceptance[6] did not say where the language switch is: at the console pin 4016370a it is the avatar menu's Preferences › Language row (AppHeader.tsx). No clause text changed", "ref": "#24632" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1031,7 +1065,7 @@ "title": "Setup → Approvals leads with Approvals Inbox, above the raw tables", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": ["platform admin with Setup access"], @@ -1043,7 +1077,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as the dev admin", + "boot the showcase on its own port and DB; sign in as the dev admin", "GET /api/v1/meta/app and read the setup app's group_approvals children in declared order", "open the Setup app, expand the Approvals group, screenshot the expanded group", "read the group's child links in DOM order with their hrefs", @@ -1079,7 +1113,8 @@ "packages/plugins/plugin-approvals/src/nav-contribution.test.ts" ], "history": [ - { "revision": 1, "date": "2026-08-10", "change": "initial — the Setup-side Approvals Inbox entry had no checklist coverage", "ref": "#7331" } + { "revision": 1, "date": "2026-08-10", "change": "initial — the Setup-side Approvals Inbox entry had no checklist coverage", "ref": "#7331" }, + { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1087,7 +1122,7 @@ "title": "The bell badge and Home's action card agree on the pending-approval count and lead to an app the user can open", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "browser", "personas": ["a non-admin pending approver", "an admin with >9 combined unread notifications + pending approvals"], @@ -1102,7 +1137,7 @@ ] }, "steps": [ - "boot showcase isolated; seed >9 combined notifications + pending approvals for a NON-ADMIN member (knownGaps: a runtime flow routing {type:'user', value:} on showcase_field_zoo, one create per approval — coalesced notifications count once, so the pending approvals carry the total); sign in as that member through the form", + "boot the showcase on its own port and DB; seed >9 combined notifications + pending approvals for a NON-ADMIN member (knownGaps: a runtime flow routing {type:'user', value:} on showcase_field_zoo, one create per approval — coalesced notifications count once, so the pending approvals carry the total); sign in as that member through the form", "open the bell popover; screenshot the header line and read the button badge", "open /_console/home; screenshot the 'Needs your attention' card", "click the card's pending-approvals row and record the URL it navigates to", @@ -1140,7 +1175,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-10", "change": "initial — the bell breakdown (objectui#4073) and Home's card target had no checklist coverage", "ref": "#7331" }, - { "revision": 2, "date": "2026-10-17", "change": "knownGaps[0] said a plain member's notification count is structurally 0, so the >9 case was admin-only (#7344); #7344 closed on 2026-08-11 and the #23878 run saw a member read 2 notifications. Re-run on main c91af8ed6 with the console at objectui f8267df93aba as a baseline-only member seeded with 10 pending approvals and 2 notification groups: the bell button read 9+, the popover header '12 total · 2 notifications + 10 pending approvals', Home's Awaiting approval card 10, the inbox's My Pending tab 10 and the approvals API 10; the card's View all opened /_console/apps/com.example.showcase/system/approvals inside the Showcase app. The knownGap, step 1 and clause 3's verify now say so", "ref": "#24036" } + { "revision": 2, "date": "2026-10-17", "change": "knownGaps[0] said a plain member's notification count is structurally 0, so the >9 case was admin-only (#7344); #7344 closed on 2026-08-11 and the #23878 run saw a member read 2 notifications. Re-run on main c91af8ed6 with the console at objectui f8267df93aba as a baseline-only member seeded with 10 pending approvals and 2 notification groups: the bell button read 9+, the popover header '12 total · 2 notifications + 10 pending approvals', Home's Awaiting approval card 10, the inbox's My Pending tab 10 and the approvals API 10; the card's View all opened /_console/apps/com.example.showcase/system/approvals inside the Showcase app. The knownGap, step 1 and clause 3's verify now say so", "ref": "#24036" }, + { "revision": 3, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1148,7 +1184,7 @@ "title": "The anonymous /approvals/act door: a minted one-tap token renders a confirm page and one POST decides as exactly the bound approver — every dead-token shape is a designed refusal page with no state change", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "api", "personas": ["dev admin (submitter of the seeded invoice request — remind is submitter-only). Since PR #24620 the remind fan-out mints no token, so the admin drives only the live legs: the reminder at-rest read and the server reads beside the anonymous probes", "the ANONYMOUS bearer of an action link: a clean HTTP client holding NO session cookie and NO bearer token"], @@ -1165,7 +1201,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "at-rest leg: POST /api/v1/approvals/requests/:id/remind on the admin-submitted invoice request — the answer's notified is positive and GET /:id/actions gains one remind row (notified counts the pending SLOTS fanned out to: a slot listed twice counts twice while messaging dedups the second emit, so read it as a positive count, never as distinct people — QA run #24542, PR #24620's acceptance notes); then GET /api/v1/data/sys_notification (topic approval.reminder, source_id = the request id) as the admin and confirm each reminder payload carries NO actions list and no /api/v1/approvals/act URL — only its title, its body and the /system/approvals deep link. ⛔ Do NOT read sys_approval_token through the data API: since PR #22633 the object is apiEnabled: false with apiMethods: [], so GET /api/v1/data/sys_approval_token answers 404 OBJECT_API_DISABLED for every caller (the administrator included) — that 404 is the designed answer, not a finding", "the live-token legs — the per-approver mint (one approve + one reject row per concrete pending approver, SHA-256 at rest, FORBIDDEN for a non-pending approver), the confirm page that never mutates, the redemption as the bound approver (comment 'Via action link'), the replay, the dead-slot and expired shapes and the bearer probe — need a raw token no stock door mints (knownGaps[0]): run the pins in automated.ref and cite each file's summary line, per clause as each verify names them", "switch to a CLEAN anonymous client (no cookies, no Authorization header — prove it: GET /api/v1/approvals/requests from the same client answers 401); GET and then POST (form body token=garbage) /api/v1/approvals/act?token=garbage — both answer the 'Invalid link · 链接无效' page, 200 text/html, with no request summary rows, and the admin's before/after reads of the invoice request show no change. It is the one dead-token shape a live boot can still produce without a minted token", @@ -1239,7 +1275,8 @@ { "revision": 4, "date": "2026-10-19", "change": "re-pointed to main after PR #24620 (65a8eeb7d4, the fix of #24564) took the one-tap link off the reminder payload — wave anchor #24579 §1, the row round 2 added (comment 6272314386), resting on QA runs #24542 and #24558. The live mint leg read raw tokens out of sys_notification.payload.actions after a remind; on main remind() mints no token and attaches no link, issueActionTokens has no production caller and no REST door reaches it, so no stock boot holds a live token. Every live-token leg moves to the pins, now named per clause in a new automated.ref, each measured at base 9a7838edf5 under os-verify-lock (plugin-approvals vitest: approval-service.test.ts 367, action-page-member.integration.test.ts 12, sys-approval-token-generic-door.integration.test.ts 6, reminder-action-link-at-rest.integration.test.ts 4, approval-token-internal-hash.integration.test.ts 1, all passed). The live run keeps the at-rest read (the reminder row carries no link), the token=garbage page and the anonymous-client proof. acceptance[1]–[3] move to oracle test; acceptance[4] keeps its live invalid capture and records the not_pending / not_approver pages as unpinned; acceptance[5]'s bearer probe has no live token and no pin and reads blocked(fixture). The ten steps become five: the mint, capture, confirm-page, redemption, replay, dead-slot and bearer-probe steps fold into one pin step (steps[2]) and the garbage step joins the anonymous-client step (steps[3]); #24542's finding that notified counts slots rides along in steps[1]; knownGaps[0] is new, the email-rendering gap now says no stock producer exists, and personas[0], fixtures.requires and source[2]/[4] follow, plus the at-rest pin in source", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause — checklist-accuracy finding of QA run #24779 (wave anchor #24579 round 12, round 6's theme-B rule): acceptance[4]'s summary-rendering split and acceptance[5]'s bearer probe have no pin, so the ref claims acceptance[0]–[3] whole and names the halves of [4] and the converse of [5] it does pin. Re-measured at base 307ce4c4b4: remind() mints no token and attaches no link, issueActionTokens' callers are test files only, and no step harvests a token from sys_notification.payload.actions; a runner's token comes only from the pins' in-process issueActionTokens, as fixtures.requires[1] already says. No clause changed", "ref": "#24824" }, { "revision": 6, "date": "2026-10-20", "change": "automated.ref names a pin for negative[2] — carried finding 2 of round 12's report on PR #24824 (wave anchor #24579 round 13), against QA run #24779's 'unpinned' score. action-page-member.integration.test.ts's dead-link case compares the invalid, expired and consumed pages, through GET and POST, byte for byte with renderResultPage(kind) and no request, which renders no summary rows (action-link-pages.ts at base 7697ddcf94); a page leaking the summary fails it. That is negative[2]'s full clause on the member door, and the file's parity cases hold the mount to the same bytes. No clause changed", "ref": "#24850" }, - { "revision": 7, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and knownGaps[2] score each live-token leg at its own clause's layer. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: acceptance[0]–[3] are test-oracle clauses and their pins are their oracle; acceptance[4] is a network clause, and action-page-member.integration.test.ts renders its invalid, expired and consumed pages through the plugin's page member and mount on a started kernel (driver-sql over in-memory SQLite, a real Hono app), while its not_pending / not_approver pages and acceptance[5]'s bearer probe have no pin at their door. knownGaps[0] said every such leg is scored from the pins, and knownGaps[2] scored the expired shape from the service test; both now say this, as acceptance[4].verify already did. No clause changed", "ref": "#24963" } + { "revision": 7, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and knownGaps[2] score each live-token leg at its own clause's layer. PR #24891's carried finding (round 15's acceptance notes: legs scored from pins, mixed clause oracles), read under RUNNER rule 6. Read at base feb09db14a: acceptance[0]–[3] are test-oracle clauses and their pins are their oracle; acceptance[4] is a network clause, and action-page-member.integration.test.ts renders its invalid, expired and consumed pages through the plugin's page member and mount on a started kernel (driver-sql over in-memory SQLite, a real Hono app), while its not_pending / not_approver pages and acceptance[5]'s bearer probe have no pin at their door. knownGaps[0] said every such leg is scored from the pins, and knownGaps[2] scored the expired shape from the service test; both now say this, as acceptance[4].verify already did. No clause changed", "ref": "#24963" }, + { "revision": 8, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1247,7 +1284,7 @@ "title": "Every approver type resolves the slate it declares — department includes all descendants, deprecated role warns and resolves, and a slate with no concrete person takes the declared onEmptyApprovers fork, never a fake approval", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": ["dev admin (authors the scratch flows; org owner)", "Ada Auditor / Mei Phone (real logins since #9308 — directory rows the graph legs resolve to)"], @@ -1267,7 +1304,7 @@ "enumSource": { "file": "packages/spec/src/automation/approval.zod.ts", "export": "ApproverType", "expect": 10 }, "variants": ["manager", "position", "department (+ ALL descendant units)", "team", "field (multi-value fan-out)", "expression", "org_membership_level", "role (deprecated → org_membership_level: warn + resolve)", "user", "queue (dead slot, #3508)"], "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin; provision the directory rows — BU memberships over the data API, a team and its members through the organization team doors, a manager chain through set-user-manager (fixtures.requires names each door) — and record their ids", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin; provision the directory rows — BU memberships over the data API, a team and its members through the organization team doors, a manager chain through set-user-manager (fixtures.requires names each door) — and record their ids", "author + register the scratch flows through POST /api/v1/automation (fixtures.requires: a record trigger on a stock object, no writable package); per leg: trigger the flow by creating one record of its trigger object, GET /api/v1/approvals/requests?status=pending, GET /:id and read pending_approvers (and pending_approver_names); deregister the flow before the next leg", "department leg: point the approver at a PARENT unit whose descendant units hold the members — the slate must union the parent's members with every descendant's, deduped; deactivate a descendant and re-trigger: its members drop out (inactive units are skipped)", "field leg: a multi-select user field naming two users — two slots, in order, never one 'u1,u2' literal", @@ -1352,6 +1389,12 @@ "date": "2026-10-19", "change": "steps[6]–[7] and acceptance[3]–[5] verify corrected — three checklist-accuracy findings of the 18.0 QA sweep (QA run #24542, approvals.approver-resolution-matrix; wave anchor #24579 §3). node_config_json, and so __resolvedFrom, is not on GET /api/v1/approvals/requests/:id — rowFromRequest in approval-service.ts projects no node_config_json — and the run read it through GET /api/v1/data/sys_approval_request/ID as the admin, so the steps and verifies now name that door. output.autoApproved is not surfaced by the automation runs read (no step output, run output {}), so the auto_approve leg proves the approve edge with a marker node, as the run did. acceptance[3]'s quoted warn was a paraphrase of the live line; the verify now matches the two names it carries instead of a sentence. No clause text changed", "ref": "#24632" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1360,7 +1403,7 @@ "title": "approvalStatusField mirrors every request transition onto the business record — through the record lock's whitelist, attributed to the acting user", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "api", "personas": ["dev admin (authors the scratch flow; decides and recalls)", "a second real login (Ada) as submitter where the recall/pending attribution needs a non-deciding identity"], @@ -1376,7 +1419,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); register the scratch flows through POST /api/v1/automation (one at a time, fixtures.requires); trigger the mirrored flow by creating a record of its trigger object (e.g. POST /api/v1/data/showcase_field_zoo {name, f_master_detail})", + "boot the showcase on its own port and DB (dogfood §0); register the scratch flows through POST /api/v1/automation (one at a time, fixtures.requires); trigger the mirrored flow by creating a record of its trigger object (e.g. POST /api/v1/data/showcase_field_zoo {name, f_master_detail})", "submit: while the request is pending, GET the trigger record — the mirror field reads 'pending', and the record's updated_by is the SUBMITTER (the pending stamp is attributed to whoever the row calls submitter, #3783)", "decide: approve — the field flips to 'approved' with updated_by = the decider; re-trigger and reject — 'rejected'; re-trigger and recall as the submitter — 'recalled'; re-trigger, send back (revise) — 'returned' (NOTE: the service writes this fifth value though the spec docblock lists only four — assert the actual behavior); drive the revise loop past maxRevisions — the auto-reject stamps 'rejected'", "lock contrast: with lockRecord true (the default) and the request pending, PATCH the record as the submitter — refused (RECORD_LOCKED class); yet the 'pending' mirror already landed on that same locked record — the lock hook whitelists a write whose ONLY change is the configured approvalStatusField", @@ -1432,7 +1475,8 @@ ], "history": [ { "revision": 1, "date": "2026-08-30", "change": "initial — the opt-in status mirror had no coverage. Grounded in the six service write sites (including 'returned', which the spec docblock omits — recorded so a run asserts actual behavior), the lock hook's mirror-field-only whitelist, and the #3783 acting-user attribution the existing integration test pins; no showcase flow declares the key, so the fixture rides the scratch-flow route (knownGaps)", "ref": "#sweep-2026-08-30" }, - { "revision": 2, "date": "2026-10-16", "change": "the scratch-flow route re-pointed: the flows no longer need a writable package (the approvals.ooo-delegation-reroute finding of #23878 names this item too). POST /api/v1/automation registers them on the stock boot, record-triggered on showcase_field_zoo. Scoped re-run on main c91af8ed6 (approvalStatusField f_textarea): submit stamps 'pending' with updated_by the submitter; the submitter's PATCH while pending answers 409 RECORD_LOCKED while the mirror already sits on the record; approve → 'approved', reject → 'rejected', recall → 'recalled'; a flow without the key leaves the field null at every read; a flow naming f_text (no such field) still finalizes approved and logs WARN '[approvals] mirrorStatusField failed: Unknown field …'. The returned / maxRevisions and cascade legs were not re-run", "ref": "#24036" } + { "revision": 2, "date": "2026-10-16", "change": "the scratch-flow route re-pointed: the flows no longer need a writable package (the approvals.ooo-delegation-reroute finding of #23878 names this item too). POST /api/v1/automation registers them on the stock boot, record-triggered on showcase_field_zoo. Scoped re-run on main c91af8ed6 (approvalStatusField f_textarea): submit stamps 'pending' with updated_by the submitter; the submitter's PATCH while pending answers 409 RECORD_LOCKED while the mirror already sits on the record; approve → 'approved', reject → 'rejected', recall → 'recalled'; a flow without the key leaves the field null at every read; a flow naming f_text (no such field) still finalizes approved and logs WARN '[approvals] mirrorStatusField failed: Unknown field …'. The returned / maxRevisions and cascade legs were not re-run", "ref": "#24036" }, + { "revision": 3, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1440,7 +1484,7 @@ "title": "Inbox keyboard flow: j/k and arrows move focus, Enter opens, x selects, ←/→ walk the drawer — and a/r quick-decide through the SAME server transition as the buttons, gated by quick-decidability", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": ["dev admin (pending approver on the seeded demo requests — several quick-decidable rows plus the dynamic-approval stage-1 row as the NOT-quick-decidable contrast)"], @@ -1452,7 +1496,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin; open the inbox via the account app's Approvals entry; confirm the hint bar renders ('Keyboard: j/k move · Enter open · x select · a approve · r reject')", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin; open the inbox via the account app's Approvals entry; confirm the hint bar renders ('Keyboard: j/k move · Enter open · x select · a approve · r reject')", "press j/j/k and ArrowDown/ArrowUp — the focus ring walks the FILTERED list order; press Enter — the focused row's drawer opens (verify the drawer's request identity against GET /api/v1/approvals/requests/:id, not the highlight)", "with the drawer OPEN press ArrowLeft/ArrowRight — the drawer walks to the previous/next visible row without returning to the list; close the drawer", "focus a quick-decidable row and press r — the shared confirm dialog opens (nothing posted yet); confirm — capture POST /api/v1/approvals/requests/:id/reject and re-read the request + /:id/actions", @@ -1505,7 +1549,8 @@ "examples/app-showcase/src/automation/flows/dynamic-approval.flow.ts#decisionOutputs (the stock decisionOutputs row the gating contrast rides)" ], "history": [ - { "revision": 1, "date": "2026-08-30", "change": "initial — the inbox's keyboard surface (documented in the page's own header and advertised by its hint bar) had no checklist coverage; the load-bearing halves are that r/a land the same server transition as the buttons and that quick-decidability gates hotkeys off rows whose decisions require typed outputs", "ref": "#sweep-2026-08-30" } + { "revision": 1, "date": "2026-08-30", "change": "initial — the inbox's keyboard surface (documented in the page's own header and advertised by its hint bar) had no checklist coverage; the load-bearing halves are that r/a land the same server transition as the buttons and that quick-decidability gates hotkeys off rows whose decisions require typed outputs", "ref": "#sweep-2026-08-30" }, + { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { diff --git a/docs/qa/platform-checklist/areas/automation.json b/docs/qa/platform-checklist/areas/automation.json index f19a18fb53..9c18b125f2 100644 --- a/docs/qa/platform-checklist/areas/automation.json +++ b/docs/qa/platform-checklist/areas/automation.json @@ -78,7 +78,7 @@ "title": "Flow Runs render loop/region iterations as a nested execution tree", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": [ @@ -92,7 +92,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "POST /api/v1/automation/showcase_batch_reminders/trigger with body {\"params\": {\"tasks\": [t1, t2, t3]}} — three task-shaped objects each carrying id/title/owner so the loop body's notify node can interpolate {task.title}/{task.owner}", "GET /api/v1/automation/showcase_batch_reminders/runs, take the newest run id, then GET /api/v1/automation/showcase_batch_reminders/runs/:runId for the full step log", "record every step's nodeId, nodeType, status, parentNodeId, iteration, regionKind (ExecutionStepLogSchema #1505 region tags)", @@ -189,6 +189,12 @@ "date": "2026-10-19", "change": "acceptance[1] (clause, verify) stops asking a tag of a step that is never logged — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.flow-run-step-nesting acceptance[1]; wave anchor #24579 §3): both showcase_batch_reminders and showcase_resilient_sync runs end at the last real node, so the clause held vacuously for end. Re-derived at base 9a7838edf5 (packages/services/service-automation/src/engine.ts: a taken end pushes no step; recordSkipped logs an untaken edge's target as skipped)", "ref": "#24649" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -197,7 +203,7 @@ "title": "Time-relative flow triggers author first-class and fire per matching record", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -214,7 +220,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0) with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 exported (fixtures.requires); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0) with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 exported (fixtures.requires); sign in as the dev admin", "open showcase_task_due_reminder in the Studio flow-designer; open the start node's panel", "screenshot the first-class timeRelative editor (sweep object / date field / offset days as a numberList editor — not raw JSON)", "read the server boot log: the Flows banner must count showcase_task_due_reminder bound as a time_relative trigger (requires: ['triggers'] — engine warns 'no time_relative trigger is registered' otherwise)", @@ -288,6 +294,12 @@ "date": "2026-10-16", "change": "the item assumed the time_relative trigger binds on a stock boot (#23878 automation slice finding 1; #23879 out_of_scope_findings). Re-measured on main c91af8ed6: unset OS_AUTOMATION_SCHEDULED_WORK_ENABLED leaves showcase_task_due_reminder unbound by deployment policy (banner: '1 flow declares a 'time_relative' trigger but is NOT bound — disabled by deployment policy'; _status bound:false), and with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 _status reports it bound, triggerType time_relative. The fixture line and step 1 now name the switch; the sweep itself was not re-run", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -296,7 +308,7 @@ "title": "Filtered roll-up summaries recompute only through their own filter", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -310,7 +322,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "GET /api/v1/data/showcase_expense_report for EXP-2001 and record all six roll-ups: total_amount (unfiltered SUM), approved_amount (SUM where status=approved), reimbursable_amount (SUM where billable=true), line_count (unfiltered COUNT), rejected_count (COUNT where status=rejected), over_limit_count (COUNT where amount >= 500)", "GET the child showcase_expense_line rows for that report and independently recompute each roll-up from them", "PATCH one line's status approved→rejected over /api/v1/data/showcase_expense_line/:id; re-read the parent and recompute again", @@ -392,6 +404,12 @@ "date": "2026-10-19", "change": "steps[6] and acceptance[4].verify name the namespace prefix the scratch base requires — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.rollup-summary-filter step 7 / acceptance[4]; wave anchor #24579 §3): without it publish-drafts refuses the objects NAMESPACE_PREFIX and the persisted-metadata half is unreachable. Re-derived at base 9a7838edf5 (packages/spec/src/kernel/namespace-prefix.ts deriveNamespaceFromPackageId / validateObjectNamespacePrefix; metadata-protocol publish preflight NAMESPACE_PREFIX). The active-save asymmetry the run met is the product defect #24547 (open), not edited here. No clause changed", "ref": "#24649" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -400,7 +418,7 @@ "title": "Every demonstrable flow node type authors in the designer, executes in a run, and surfaces its step in run logs", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": [ @@ -441,7 +459,7 @@ "approval_revise" ], "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "GET /api/v1/automation/actions (the designer palette feed) and record the descriptor list; open the Studio flow-designer palette on a WRITABLE scope and screenshot it — the stock showcase package is read-only, so its canvas offers no Add node: clone a flow first (POST /api/v1/automation/NAME/clone, or Setup › Packaged Automation, as QA run #24545 did) and open the clone under Studio's ~org scope", "record_change chain: POST a showcase_task WITH `assignee` set to a real user's email, under a project that has an `owner` (the project lookup is required), with status 'in_review' — task_status_flow allows done only from in_review and does not check a create — then PATCH status→done — fires showcase_task_completed (script+notify), showcase_fan_out_notify (parallel+http), showcase_resilient_sync (try_catch), showcase_task_done_notify_owner (subflow); the create also fires showcase_task_follow_up (wait) and showcase_declarative_connector_ping / showcase_mcp_connector_echo (connector_action). ⚠ The notify steps address the task's assignee and its project's owner: a task created without an assignee, or under a project without an owner, makes them fail LOUDLY for want of a recipient — a correct refusal, not a notify FAIL (QA run #23166)", "on-demand chain: POST /api/v1/automation//trigger for showcase_batch_reminders {params:{tasks:[…]}} (loop), showcase_inquiry_purge (get_record+decision+delete_record — seed at least one closed showcase_inquiry first), showcase_release_signoff {params:{items:[…]}} (map)", @@ -558,6 +576,12 @@ "date": "2026-10-20", "change": "automated removed, fixtures.knownGaps[2] added — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: Tier 1 in name only), round 15 of wave anchor #24579. The ref already said its one dogfood pin covers ONE variant of acceptance[1]. A variant is not a clause, so no full clause is pinned: automated is dropped (theme-B rule), and the knownGap says what the pin asserts. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ], "enumSource": { @@ -571,7 +595,7 @@ "title": "Every flow trigger type fires and produces a run of its bound flow; the run's trigger block records the launch event", "since": "v15", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": [ @@ -593,7 +617,7 @@ "time_relative" ], "steps": [ - "boot showcase isolated (dogfood §0) with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 and OS_PORT exported (fixtures.requires — without the first, the schedule and time_relative variants can never fire); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0) with OS_AUTOMATION_SCHEDULED_WORK_ENABLED=1 and OS_PORT exported (fixtures.requires — without the first, the schedule and time_relative variants can never fire); sign in as the dev admin", "record_change (create leg): POST /api/v1/data/showcase_task with priority='urgent' — showcase_urgent_task_alert fires via record-after-write with previous == null (#3427)", "record_change (update leg): POST a normal-priority task, then PATCH priority→'urgent'; also move a task to done — create it with status 'in_review' and its required project (task_status_flow allows done only from in_review, and a create is not transition-checked), then PATCH status→'done' — and confirm a further save WITHOUT a transition fires nothing", "api: POST /api/v1/automation/hooks/showcase_inbound_task_webhook/intake with JSON {title, assignee, project} and header x-objectstack-signature: t=,v1=.', key 'showcase-webhook-secret'> — signHttpBodyAt(rawBody, 'showcase-webhook-secret', Math.floor(Date.now() / 1000)) from @objectstack/core writes it, and the door refuses a t more than 300 s from the server clock — expect a 202 ACK; then find the created showcase_task", @@ -696,6 +720,12 @@ "date": "2026-10-20", "change": "negative[0]'s pin note narrowed, automated.ref unchanged — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: negative[0]'s pin is not in the ref), round 15 of wave anchor #24579. Read at base b1676441ba: the dogfood case asserts the anonymous trigger's 401 on the served showcase composition and never reads the runs after it. So it asserts half of negative[0] ('denied (401) with NO run row created'). A pin joins the ref only for a full clause at its layer, so it stays out, and negative[0] now says which half it pins. negative[0]'s FAIL condition is unchanged", "ref": "#24891" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -704,7 +734,7 @@ "title": "A failing node inside try_catch is handled (catch region, $error binding) and the failed attempt is still recorded; outside it fails the run loudly", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -718,7 +748,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "handled case: POST a showcase_task carrying its required `project` (a seeded showcase_project id; without it the create answers 400 VALIDATION_FAILED 'Project is required'), status 'in_review' and an assignee, then PATCH status→'done' (allowed from in_review only) — showcase_resilient_sync runs, its try-region http node fails against the unroutable host, retries, then the catch region runs", "GET /api/v1/automation/showcase_resilient_sync/runs/:runId — record the overall status, the try_catch container step's own status, EVERY step carrying regionKind='try' (nodeId, status, error, retryAttempt) in log order, the catch step's status/regionKind, and the run duration", "re-read the task over /api/v1/data/showcase_task/:id — sync_status and sync_error", @@ -807,7 +837,8 @@ }, { "revision": 3, "date": "2026-08-18", "change": "re-pointed step 5 and clause 7 at a trigger-fired specimen. The step prescribed an api-triggered probe, whose failure clause 7 structurally cannot observe: service-automation/src/engine.ts emits the ERROR line only inside trigger.start's callback, and its own text and #4632 verdict give the reason — 'no caller holds this result'. For a manual or api trigger the caller DOES hold it, so no line is written and a clean log reads as a missing diagnostic (#9467 CF-5)", "ref": "#9386" }, { "revision": 4, "date": "2026-10-16", "change": "the handled case's 'POST a showcase_task and PATCH status→done' failed as written (#23878 automation slice finding 2, re-measured on main c91af8ed6): showcase_task requires `project` (a create without it answers 400 VALIDATION_FAILED 'Project is required'), and task_status_flow allows status→done only from in_review (a PATCH from the default backlog answers 400 invalid_transition). A create with project and status 'in_review' answers 201 (create is not transition-checked) and the PATCH to done answers 200, firing the record-change flows. Step 2 now creates the task with project and status 'in_review'; on that path showcase_resilient_sync ran and completed (the catch region handled the failure)", "ref": "#24036" }, - { "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[1] and steps[4] say where the probe flow lands — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.flow-error-handling step 5 / fixtures.requires; wave anchor #24579 §3): POST /api/v1/automation reads no package selector, so \"in the scratch package\" could not be honoured. Re-derived at base 9a7838edf5 (packages/runtime/src/domains/automation-flow-door-package-address.test.ts 'POST / of a flow no package holds saves the package-unbound row'). No clause changed", "ref": "#24649" } + { "revision": 5, "date": "2026-10-19", "change": "fixtures.requires[1] and steps[4] say where the probe flow lands — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.flow-error-handling step 5 / fixtures.requires; wave anchor #24579 §3): POST /api/v1/automation reads no package selector, so \"in the scratch package\" could not be honoured. Re-derived at base 9a7838edf5 (packages/runtime/src/domains/automation-flow-door-package-address.test.ts 'POST / of a flow no package holds saves the package-unbound row'). No clause changed", "ref": "#24649" }, + { "revision": 6, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -815,7 +846,7 @@ "title": "Screen flow round-trip: action trigger → paused run → rendered dialog → resume with inputs → persisted write", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -831,7 +862,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin; open /apps/showcase_app/showcase_task", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin; open /apps/showcase_app/showcase_task", "start capturing POSTs to /api/v1/automation/**", "open the first row's action menu ([data-testid=row-action-trigger]) and click row-action-showcase_bulk_reassign", "record the launch POST — POST /api/v1/actions/showcase_task/showcase_bulk_reassign (the action door; see clause 1) — and GET /api/v1/automation/showcase_reassign_wizard/runs to find the new run — status must be paused", @@ -930,6 +961,12 @@ "date": "2026-10-13", "change": "acceptance[0].verify and steps[3] re-pointed from the trigger route to the action door (stale spec, #23163 A0; parked on #23121, carried by #23169). Since objectui#12037 a click on a declared type:'flow' action — showcase_bulk_reassign is one — launches through POST /api/v1/actions/:object/:action, which runs the flow and answers the pause; the trigger route is left to undeclared flow starts. QA run #23163 saw POST /actions, the run paused at 'collect', and every other clause pass. Re-read from objectui source at 1fb1ced36243 (useConsoleActionRuntime.tsx), still this repo's .objectui-sha; the framework door is runtime route-ledger.ts /api/v1/actions/:object/:action", "ref": "#23169" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -938,7 +975,7 @@ "title": "Suspended runs persist to sys_automation_run, survive a cold restart, and resume — including nested (linked-run) pauses", "since": "v16", "status": "active", - "revision": 1, + "revision": 2, "priority": "P1", "surface": "api", "personas": [ @@ -952,7 +989,7 @@ ] }, "steps": [ - "boot showcase isolated against a file DB (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port against its own file DB (dogfood §0); sign in as the dev admin", "POST a showcase_task — showcase_task_follow_up suspends at its wait node (timer PT1M)", "PATCH a showcase_project status→'completed' — showcase_project_closure parks at its subflow node and showcase_closure_signoff parks at its approval node (linked runs)", "read the paused rows from sys_automation_run over the data API by run id: status, flow name, suspended node, and the rehydration payload", @@ -1015,6 +1052,12 @@ "date": "2026-08-07", "change": "initial — promotes the #4470 durable-suspend proof to a ledger item and adds the nested linked-run pair", "ref": "claude/platform-test-checklist-ocwugl" + }, + { + "revision": 2, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1023,7 +1066,7 @@ "title": "A paused record-change run's stored state serves its trigger record masked — credential-class fields as the mask, internal fields absent — to a run-state reader, hot and after a cold boot", "since": "v17", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -1043,7 +1086,7 @@ ] }, "steps": [ - "boot showcase isolated against a file DB (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port against its own file DB (dogfood §0); sign in as the dev admin", "create a row of the fixture object with all three non-ordinary fields set (a password, a secret, an internal value), then PATCH its ordinary field and its password so the record-change flow fires and pauses", "as the member persona, read the paused sys_automation_run row by id over GET /data/sys_automation_run/:id and parse variables_json and context_json", "as the member persona, read GET /automation/:name/runs/:runId", @@ -1119,6 +1162,12 @@ "date": "2026-10-19", "change": "fixtures.knownGaps[0] corrected — a carried note of the 18.0 sweep (round 3's report on wave anchor #24579): it called showcase_approver_bindings 'draft on purpose'. Re-derived at base bfc1539fe0: approver-bindings.flow.ts authors it status: 'obsolete', and its docblock says why not draft — a draft flow binds and fires exactly like an active one, while obsolete is loaded and never armed. It is still the only flow on showcase_field_zoo, so the gap's conclusion holds. No clause changed", "ref": "#24687" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1127,7 +1176,7 @@ "title": "connector_action dispatches through every registered connector kind, and the registry feeds the designer pickers", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -1152,7 +1201,7 @@ "showcase_mcp_tools (declarative MCP)" ], "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "GET /api/v1/automation/connectors — record the descriptor list and each connector's action list (the MCP instance's actions must come from its tools/list handshake)", "premise guard, before scoring any dispatch clause: confirm the boot EXPORTED OS_PORT= (or SHOWCASE_SELF_URL) — read it back from the server process's environment — and that one declarative dispatch succeeds (after step 4's create, the newest showcase_declarative_connector_ping run is completed, not 'fetch failed'). ⛔ Neither surface you might reach for carries the dial address: GET /api/v1/automation/connectors exposes no baseUrl at all (its rows carry name, label, type, description, icon, origin, state, actions), and GET /api/v1/meta/connector/showcase_status_api serves the BUILD-time providerConfig.baseUrl — http://127.0.0.1:3000 on an artifact compiled without OS_PORT, even while the running instance dials the exported port", "POST a showcase_task carrying its required `project` (a seeded showcase_project id; without it the create answers 400 VALIDATION_FAILED 'Project is required') and status 'in_review' — fires showcase_declarative_connector_ping and showcase_mcp_connector_echo on create", @@ -1218,6 +1267,12 @@ "date": "2026-10-16", "change": "the premise-guard step and knownGaps[1] read the declarative instances' baseUrl out of GET /api/v1/automation/connectors, a key that response does not carry (#23878 automation slice finding 4). Re-measured on main c91af8ed6: the connector rows carry no baseUrl, and /meta/connector/showcase_status_api serves providerConfig.baseUrl http://127.0.0.1:3000 (the build-time artifact) while the boot exported OS_PORT=48731, nothing listened on 3000 and both declarative pings completed. The guard now checks the exported variable and a completed dispatch. Steps 4-5 also create the task with its required project and status 'in_review' before patching it to done (#23878 automation slice finding 2, re-measured on main c91af8ed6): showcase_task requires `project` (a create without it answers 400 VALIDATION_FAILED 'Project is required'), and task_status_flow allows status→done only from in_review (a PATCH from the default backlog answers 400 invalid_transition). A create with project and status 'in_review' answers 201 (create is not transition-checked) and the PATCH to done answers 200, firing the record-change flows", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1226,7 +1281,7 @@ "title": "The developer Flow Runs page triggers a run with typed inputs and drives a screen flow's pause to completion (no orphaned paused rows)", "since": "v16", "status": "active", - "revision": 2, + "revision": 3, "priority": "P2", "surface": "mixed", "personas": [ @@ -1240,7 +1295,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "open the developer Flow Runs PAGE (apps/console FlowRunsPage) at /apps/showcase_app/component/developer/flow-runs (console route developer/flow-runs) — NOT the Studio flow-designer Runs preview", "typed-input run: select 'Batch Task Reminders (Loop)' (showcase_batch_reminders); the Test Run panel shows the `tasks` input as a JSON `list` textarea; paste three task-shaped objects [{id,title,owner}×3] and click 'Run Flow'", "record the trigger POST (client.automation.execute → POST /api/v1/automation/showcase_batch_reminders/trigger) and screenshot the Result envelope; confirm a fresh row appeared in the Recent Runs panel (client.automation.listRuns → GET /automation/showcase_batch_reminders/runs)", @@ -1311,6 +1366,12 @@ "date": "2026-10-13", "change": "steps[5] leaves new_assignee empty (assertion defect, minor, #23166 A2; parked on #23121, carried by #23169). Filling it at Run supplies the screen's one required field, so the run completes without pausing and the hand-off is never exercised (service-automation screen-input-contract: caller-supplied declared fields satisfy the screen). QA run #23166 left it empty, met the pause, the FlowRunner dialog and the resume, and passed every clause", "ref": "#23169" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1319,7 +1380,7 @@ "title": "Toggling a record-change flow OFF is a runtime kill switch — the mutation that fired it produces no new run; ON restores firing", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "api", "personas": [ @@ -1332,7 +1393,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "baseline: POST /api/v1/data/showcase_task with priority='urgent' and its required project (a seeded showcase_project id); GET /api/v1/automation/showcase_urgent_task_alert/runs and record the run count (one new run for this create)", "read the pre-toggle runtime state: GET /api/v1/automation/_status (getFlowRuntimeStates) — showcase_urgent_task_alert enabled=true, bound=true", "toggle OFF: POST /api/v1/automation/showcase_urgent_task_alert/toggle with {enabled:false} (client.automation.toggle → engine.toggleFlow → deactivateFlowTrigger)", @@ -1402,6 +1463,12 @@ "date": "2026-10-16", "change": "the urgent-task creates carried no project, so each answered 400 VALIDATION_FAILED 'Project is required' and no run could be counted (#23878 automation slice finding 2, re-measured on main c91af8ed6). Steps 2, 6 and 8 now send the project; with it the urgent create answered 201 and showcase_urgent_task_alert gained one run", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1410,7 +1477,7 @@ "title": "The flow-dispatch HTTP status contract: every never-dispatched, ran-and-failed and non-terminal outcome answers its own status + ADR-0112 code, at every door that dispatches a flow", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": ["seeded admin (admin@objectos.ai / admin123)", "org member for the resume 403 arm — created by the admin with POST /api/v1/auth/admin/create-user and past its password change (POST /api/v1/auth/change-password): a held session answers 403 PASSWORD_RESET_REQUIRED, the right status for the wrong reason"], @@ -1430,7 +1497,7 @@ }, "steps": [ "read the table first, from its single definition: packages/runtime/src/flow-dispatch-status.ts (FLOW_NOT_FOUND_STATUS, classifyFlowRefusal, isPausedRun) — it is a module rather than a route mapper precisely because three doors must agree on it, and a second copy of it is a defect by construction. The resume arms are a separate list, read them from packages/runtime/src/domains/automation.ts (the `POST /:name/runs/:runId/resume` branch and its code→status arms)", - "boot showcase isolated (dogfood §0) with OS_PORT exported as well as -p (RUNNER.md's connector self-URL fact), sign in as the dev admin, and run the qa-flow-status-doors recipe", + "boot the showcase on its own port and DB (dogfood §0) with OS_PORT exported as well as -p (RUNNER.md's connector self-URL fact), sign in as the dev admin, and run the qa-flow-status-doors recipe", "TRIGGER, per row, against BOTH doors — POST /api/v1/automation/:name/trigger and the legacy POST /api/v1/automation/trigger/:name (the shape client.automation.trigger() calls): a name the deployment does not hold (404 RESOURCE_NOT_FOUND: the status table's 404 row names no code of its own, so the envelope carries the spec's status default, HttpStatusErrorCodeMap in packages/spec/src/api/errors.zod.ts); qa_always_fails; the disabled showcase_urgent_task_alert; qa_no_start_node; a flow that succeeds — showcase_notify_owner with {\"params\": {\"ownerId\": , \"message\": \"…\"}} answers 200 (not showcase_resilient_sync, whose CEL condition reads the record's status and answers a bare trigger 400 FLOW_FAILED); showcase_reassign_wizard (pauses)", "record for every response the HTTP status AND error.code together — neither alone is the contract, and a run that captured only the status cannot tell 409 FLOW_DISABLED from any other 409", "on the 400 FLOW_FAILED responses, additionally record error.details.errorMessage and error.details.summary (the flow author's own text and the per-node accounting) — they are the documented payload of that row and the console reads them from exactly there", @@ -1542,6 +1609,12 @@ "date": "2026-10-20", "change": "steps[5]'s INVALID_SIGNAL body and the 404 row's code — checklist-accuracy findings of QA run #24779 (wave anchor #24579 round 12). {\"output\":{\"$x\":1}} alone answers 400 INVALID_SCREEN_INPUT on the wizard, whose screen requires new_assignee, because refuseInvalidScreenInput runs before applyResumeSignal (engine.ts at base 307ce4c4b4); the body now satisfies the input too. The 404 row answers RESOURCE_NOT_FOUND: flow-dispatch-status.ts names no code for it, and the envelope takes the spec's HttpStatusErrorCodeMap default. steps[2] and acceptance[0] (clause, verify) state it. No other clause changed", "ref": "#24824" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[1] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1550,7 +1623,7 @@ "title": "Disabling a packaged flow writes a durable sys_metadata_activation row that survives a cold restart with the trigger left unbound; re-enable UPDATES the row and restores firing", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P0", "surface": "api", "personas": [ @@ -1565,7 +1638,7 @@ ] }, "steps": [ - "boot showcase isolated against a file DB (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port against its own file DB (dogfood §0); sign in as the dev admin", "baseline: POST /api/v1/data/showcase_task with priority='urgent' — one new run for showcase_urgent_task_alert (the single-boot toggle semantics are automation.flow-toggle-kill-switch's; this item takes only enough baseline to anchor the restart contrast)", "toggle OFF: POST /api/v1/automation/showcase_urgent_task_alert/toggle {\"enabled\": false}", "read the ledger row over the data API: GET /api/v1/data/sys_metadata_activation (reads are open — apiMethods ['get','list'], sys-metadata-activation.object.ts) and locate the row where metadata_type='flow' AND name='showcase_urgent_task_alert' — record active, package_id and the row id, plus the row's whole KEY SET: organization_id is ABSENT from it, because the table carries no tenant column (#15024 / ADR-0131 D7). ⛔ Do not record the tenant half as a value — `row.organization_id ?? null` answers `null` for a column that does not exist, so a value read passes while measuring nothing", @@ -1655,6 +1728,12 @@ "date": "2026-10-20", "change": "negative[3] rewritten as the product outcome it guarded. Checklist-accuracy finding of QA run #24841 (18.0 sweep round 4 at 307ce4c4): it stated a recording rule (a run scoring the ledger clause from the code alone verified nothing) with no product expectation, so it could only score not-run and held the item at partial. A negative names a product outcome whose occurrence is the FAIL (the README's item anatomy: the other side of every gate, its deny/absence cases). The outcome it guards is the refusal naming the ledger, so the FAIL is now a ledger-disabled flow answered with the status dimension's bare 'Flow NAME is disabled'. Read at base feb09db14a in engine.ts describeDisabledFlow: the ledger arm adds the 'packaged-metadata activation ledger (sys_metadata_activation' sentence, and the status arm returns the bare one. acceptance[1].verify keeps its ⛔ on the code alone. No acceptance clause changed", "ref": "#24963" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1663,7 +1742,7 @@ "title": "Disabling a packaged flow that packaged callers invoke as a subflow is refused 409 DELETE_RESTRICTED naming every caller; the refused attempt writes no ledger row; enabling the subflow itself is never guarded", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -1680,7 +1759,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0, OS_PORT exported); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0, OS_PORT exported); sign in as the dev admin", "subflow pair: POST /api/v1/automation/showcase_notify_owner/toggle {\"enabled\": false} — record the full HTTP answer (status, code, message)", "map pair: POST /api/v1/automation/showcase_one_task_signoff/toggle {\"enabled\": false} — record the answer", "after each refusal: GET /api/v1/data/sys_metadata_activation and assert NO row exists for the refused flow; then prove it is still ARMED — trigger a run through its caller (for the notify pair: create a showcase_task with its required project and status 'in_review', then PATCH it to status='done' — task_status_flow allows done only from in_review) and confirm the child's subflow step executed", @@ -1749,7 +1828,8 @@ { "revision": 3, "date": "2026-10-16", "change": "the 'still armed' probe patched a showcase_task to done without saying how to get there (#23878 automation slice finding 2, re-measured on main c91af8ed6): showcase_task requires `project` (a create without it answers 400 VALIDATION_FAILED 'Project is required'), and task_status_flow allows status→done only from in_review (a PATCH from the default backlog answers 400 invalid_transition). A create with project and status 'in_review' answers 201 (create is not transition-checked) and the PATCH to done answers 200, firing the record-change flows. Step 4 now creates the task with project and status 'in_review' first; on that path showcase_task_done_notify_owner ran and completed", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "acceptance[0].clause quotes the remedy the engine writes — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-subflow-disable-refusal acceptance[0]; wave anchor #24579 §3): the remedy names the calling flow, so a substring match on the generic quote fails although the meaning is identical. Re-derived at base 9a7838edf5 (packages/services/service-automation/src/engine.ts, the subflow-caller refusal: 'disable the calling flow 'NAME' first' … ' — or leave this one armed.')", "ref": "#24649" }, { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 707, 795, 816, 1322 and 1339 of the showcase flows index.ts in the comma-range spelling (authored in e0abc386a4 as the list 678 to 1339). At that commit those lines were the names and child flowName values of the three stock caller flows; at this base all of them have moved. source[4] now anchors the three callers, index.ts#TaskDoneNotifyOwnerFlow, #ProjectClosureFlow and #ReleaseSignoffFlow, and names each child in prose. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#24963" } + { "revision": 6, "date": "2026-10-20", "change": "fixtures.knownGaps[0] and negative[1] no longer score door arms from the engine pin, and negative[2] moved to steps[7]. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, engine-pinned arms of api clauses) and QA run #24841's procedural-negative finding (18.0 sweep round 4 at 307ce4c4). Under RUNNER rule 6 the engine pin flow-activation-ledger.test.ts (over a fake store, read at base feb09db14a) is evidence of its own layer only: the non-packaged-caller and self-call arms, and negative[1]'s several-callers arm, are door answers, recorded blocked(fixture) without a scratch caller, with the pin beside them. negative[2] (the UI half belongs to another item) stated a scope rule with no product expectation, so it could only score not-run; its text moved, unchanged in substance, to steps[7]. No acceptance clause changed", "ref": "#24963" }, + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1757,7 +1837,7 @@ "title": "POST /automation/:name/clone: whole-definition copy mutating exactly name/label/status, protection envelope dropped, no ancestry, draft-but-armed (double-fire notice verbatim), 409/404/400 refusal arms — and the honest clause: the clone must survive a restart and reach Studio", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1771,7 +1851,7 @@ ] }, "steps": [ - "boot showcase isolated against a file DB (dogfood §0, OS_PORT exported); sign in as the dev admin", + "boot the showcase on its own port against its own file DB (dogfood §0, OS_PORT exported); sign in as the dev admin", "refusal arms first (no state left behind): POST /api/v1/automation/showcase_urgent_task_alert/clone with {} · with {\"name\":\"x\"} (label missing) · with {\"label\":\"x\"} (name missing) · with {\"name\":\"showcase_urgent_task_alert\",\"label\":\"Same\"} (same-name) · with {\"name\":\"Bad-Name!\",\"label\":\"Illegal\"} (violates ^[a-z_][a-z0-9_]*$) · and POST /api/v1/automation/does_not_exist/clone {\"name\":\"qa_x\",\"label\":\"X\"} (unknown source) — record every status + code + message", "the copy: POST /api/v1/automation/showcase_urgent_task_alert/clone {\"name\":\"qa_urgent_alert_clone\",\"label\":\"QA urgent alert clone\"} — record the FULL response body (flow + notice)", "read both definitions back (GET /api/v1/automation/qa_urgent_alert_clone and …/showcase_urgent_task_alert) and diff them key by key", @@ -1856,7 +1936,8 @@ { "revision": 2, "date": "2026-10-02", "change": "clause 7's expected-fail note now names only the reachability half. The durability half was fixed by cb4c31d (#20907, with #20761): on 17.6.0 the clone survives a cold restart, reads back with its edited label and still fires (release-verification run). The reachability half still fails — no Studio surface lists a package-less flow — and is now filed as #21332 instead of resting on the #12438 sweep", "ref": "#21330" }, { "revision": 3, "date": "2026-10-17", "change": "clause 6's reachability EXPECTED FAIL is stale (checklist-accuracy finding, automation slice report on #23878): objectui#11553 gave Studio an org scope whose Automations rail lists package-less flows, and #21332 closed as a duplicate. The verify now asserts the clone is listed and opens at /_console/studio/~org/automations, steps[7] drives it after the restart, and the objectui source line names the org scope and its pin. Re-run on main c91af8ed6 with the console at objectui f8267df93aba, file DB: POST …/showcase_urgent_task_alert/clone {name qa_urgent_alert_clone, label} → 200 status draft; the Organization flows rail listed 'QA urgent alert clone' and opened it (Trigger: Record created or updated · showcase_task, Enabled, no lock banner); after a cold restart over the same DB the GET answered 200, the rail listed and opened it again, and one urgent task create added one run to the clone (0 → 1) and one to the base (1 → 2); teardown DELETE → deleted true", "ref": "#24036" }, { "revision": 4, "date": "2026-10-19", "change": "acceptance[1] (clause, verify) counts the dropped keys the clone really drops — checklist-accuracy finding of the 18.0 QA sweep (QA run #24545, automation.packaged-flow-clone-contract acceptance[1]; wave anchor #24579 §3): FLOW_CLONE_DROPPED_KEYS is 10, not 9, since packages/runtime/src/flow-clone.ts appends 'protection' (#23120, ADR-0010 §3.5) to the 2 read decorations and 7 protection fields. Re-derived at base 9a7838edf5", "ref": "#24649" }, - { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). acceptance[3].verify cited line 439 of automation-flow-clone.test.ts in the comma-range spelling (authored in e0abc386a4 as the list 368-401 and 439). Those lines are unchanged at this base: the same-name 409 cases and the unknown-source 404 assertion. The verify now anchors the two codes those cases assert, automation-flow-clone.test.ts#RESOURCE_CONFLICT and #RESOURCE_NOT_FOUND. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" } + { "revision": 5, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). acceptance[3].verify cited line 439 of automation-flow-clone.test.ts in the comma-range spelling (authored in e0abc386a4 as the list 368-401 and 439). Those lines are unchanged at this base: the same-name 409 cases and the unknown-source 404 assertion. The verify now anchors the two codes those cases assert, automation-flow-clone.test.ts#RESOURCE_CONFLICT and #RESOURCE_NOT_FOUND. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, + { "revision": 6, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated against a file DB' → 'boot the showcase on its own port against its own file DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1864,7 +1945,7 @@ "title": "The Setup packaged-automation page: reachable from Setup nav, lists packaged flows + packaged actions, toggle persists, clone on flow rows only, no lineage surface, server refusals verbatim in role=alert", "since": "v17", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "browser", "personas": [ @@ -1885,7 +1966,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0, OS_PORT exported); build/serve the pinned console; sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0, OS_PORT exported); build/serve the pinned console; sign in as the dev admin", "nav probe: GET /api/v1/meta/app?id=setup and search the served navigation for a type:'component' item with componentRef 'automation:packaged'; then look for the entry in the rendered Setup sidebar and click it", "regardless of the nav verdict, reach the page and screenshot it settled (only if the sidebar entry is MISSING — a regression of #12457 — fall back to the typed URL /_console/apps/com.objectstack.setup/component/automation/packaged — where the sidebar entry lands, the app segment being the package id (QA run #24545) — recording that the typed URL was needed, so the rest of the item is still scored)", "inventory: compare the flows table rows against GET /api/v1/meta/flow (which items carry a real _packageId and _provenance != 'org') and the actions section against its own packaged-actions read; confirm every switch reads On except a flow authored status: 'obsolete' (showcase_approver_bindings, examples/app-showcase/src/automation/flows/approver-bindings.flow.ts), whose switch correctly reads Off — 31 On / 1 Off at QA run #24545's subject", @@ -2002,6 +2083,12 @@ "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). acceptance[6].verify and source[1] cited objectui PackagedAutomationPage.tsx by the lines 529 and 516 in the comma-range spelling (authored in e0abc386a4; source[1]'s list also left a bare 529). At objectui 9602dc820450, the file's latest commit when the item was authored, 529 was the clone dialog's submit button disabled by cloneNameValid and 516 its role=alert rendering of clone.refusal; at the shipped pin 12dc7b47c340 both have moved. Each now names that code of the PackagedAutomationPage component. objectui is read at the shipped pin 12dc7b47c340 and cited in prose, because no objectui anchor resolves in this gate. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "automated.ref confirmed, unchanged. PR #24982's pin finding: objectui packages/app-shell/src/views/setup/PackagedAutomationPage.test.tsx changed blob between the console pins (b321640a6100 at 616df6598b42, aeb3e5f94e38 at cf153eb2e535; objectui#13185 and objectui#13279). Re-read at cf153eb2e535 it still stubs fetch. Its new cases (the Activation cell reading 'Not running here' for a flow the engine left unarmed, and the clone result's next steps) widen the mocked component half only, and PackagedActionsSection.test.tsx is unchanged, so the ref's scope holds and no clause moves. steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/cli.json b/docs/qa/platform-checklist/areas/cli.json index 6611e3e6db..c58a282446 100644 --- a/docs/qa/platform-checklist/areas/cli.json +++ b/docs/qa/platform-checklist/areas/cli.json @@ -1340,7 +1340,7 @@ "title": "os datasource list-tables/introspect/validate: the federation door answers, the generated draft is cwd-jailed and compiles, and schema drift maps to ✗ diffs with exit 1", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P2", "surface": "cli", "personas": ["operator (local shell) whose token HOLDS manage_platform_settings — all three commands address federation READ routes, which require it since #9901/#10255", "integration author (adopting a remote table), same capability"], @@ -1354,7 +1354,8 @@ "knownGaps": [ "the three CLI commands are unit-tested against the server's wrapped envelope since #10675 (packages/cli/src/commands/datasource/envelope-unwrap.test.ts). That suite drives each command over a stubbed fetch, so this item stays their only end-to-end coverage against a live server", "the #5594 carve-out applies to the fixture DB: .objectstack/data/showcase_external.db is a cwd-relative app-declared path that survives --fresh by documented design — do not file its persistence as a bug", - "acceptance[0]'s NARROWING half (--schema narrows; a non-matching schema prints 'No remote tables found.') cannot be observed on the stock fixture. SQLite tables carry no schema, and listRemoteTables applies ?schema= only to a table it can attribute to one (packages/services/service-datasource/src/external-datasource-service.ts: a table with no schema part is never filtered), so every --schema run lists all tables (QA run #21721, parked on #22017 row 24). On the stock fixture record that half blocked(fixture), with the pins in `automated` beside it as evidence of their own layers only (RUNNER rule 6). The unpinned remainder, the CLI turning --schema into ?schema= (list-tables.ts), is observable live against a schema-bearing driver such as postgres, or with no driver at all: the request line a local echo server receives (steps[1])" + "acceptance[0]'s NARROWING half (--schema narrows; a non-matching schema prints 'No remote tables found.') cannot be observed on the stock fixture. SQLite tables carry no schema, and listRemoteTables applies ?schema= only to a table it can attribute to one (packages/services/service-datasource/src/external-datasource-service.ts: a table with no schema part is never filtered), so every --schema run lists all tables (QA run #21721, parked on #22017 row 24). On the stock fixture record that half blocked(fixture), with the pins knownGaps[3] names beside it as evidence of their own layers only (RUNNER rule 6). The unpinned remainder, the CLI turning --schema into ?schema= (list-tables.ts), is observable live against a schema-bearing driver such as postgres, or with no driver at all: the request line a local echo server receives (steps[1])", + "no pin scores a full clause of this item at its layer (RUNNER rule 6, a pin scores only the layer it drives), so the item carries no automated ref. Every clause's oracle is the CLI's own output or exit code against a served datasource, or os build's verdict. packages/rest/src/remote-tables-twin.equivalence.test.ts drives the real ExternalDatasourceService, over a fake introspector, behind the federation route this CLI calls (GET …/external/tables), through the real HonoHttpServer adapter: '?schema= returns the SAME filtered set on both spellings' and 'a ?schema= that matches nothing returns the SAME empty set on both spellings' (the route layer). packages/services/service-datasource/src/__tests__/external-datasource-service.test.ts 'filters by requested schema' asserts the service's filter, and packages/cli/src/commands/datasource/envelope-unwrap.test.ts 'keeps \"no remote tables\" reachable from an empty server list' runs the CLI over a stubbed fetch. That is evidence for acceptance[0]'s narrowing half at those three layers, never its pass, and the CLI's flag-to-query forwarding is not pinned. automated may return when a pin runs `os datasource list-tables --schema` against a served schema-bearing datasource and reads its printed listing" ] }, "steps": [ @@ -1370,7 +1371,7 @@ { "clause": "list-tables prints the remote tables with schema qualification and column counts (plus row estimates when the driver reports them) through GET /api/v1/datasources/:name/external/tables, and --schema narrows via the ?schema= query — this item pins the CLI's /external/tables FEDERATION door specifically; the admin twin GET /:name/remote-tables is a different mount owned by the integration-system area", "oracle": "log", - "verify": "the listing matches the fixture DB's actual tables (cross-check with sqlite3.tables on showcase_external.db); on a schema-bearing driver the schema filter narrows and the no-match run prints 'No remote tables found.' (on the stock SQLite fixture that half cannot be observed, fixtures.knownGaps: record it blocked(fixture), naming a schema-bearing driver, with the pins in `automated` beside it as evidence of their own layers only, never its pass (RUNNER rule 6, a pin scores only the layer it drives): the federation route's filter through the real Hono adapter, and the CLI's empty-list line over a stubbed fetch) (list-tables.ts; route registered in packages/rest/src/external-datasource-routes.ts, ledgered at rest-route-ledger.ts)", + "verify": "the listing matches the fixture DB's actual tables (cross-check with sqlite3.tables on showcase_external.db); on a schema-bearing driver the schema filter narrows and the no-match run prints 'No remote tables found.' (on the stock SQLite fixture that half cannot be observed, fixtures.knownGaps: record it blocked(fixture), naming a schema-bearing driver, with the pins knownGaps[3] names beside it as evidence of their own layers only, never its pass (RUNNER rule 6, a pin scores only the layer it drives): the federation route's filter through the real Hono adapter, and the CLI's empty-list line over a stubbed fetch) (list-tables.ts; route registered in packages/rest/src/external-datasource-routes.ts, ledgered at rest-route-ledger.ts)", "evidence": "the listings + the sqlite3 cross-check" }, { @@ -1412,10 +1413,6 @@ "wrong-persona shape, NOT a finding: all three commands failing alike with the server's 403 PERMISSION_DENIED naming manage_platform_settings means the token authenticated but is unentitled — re-mint it for a caller holding that capability and re-run before filing anything. Filing it as a CLI or route regression would be a report against the #9901/#10255 gate working as ruled" ], "traps": ["dispatcher-vs-hono-route", "seed-data-thin", "wrong-persona"], - "automated": { - "kind": "test", - "ref": "acceptance[0]'s NARROWING half ONLY. packages/rest/src/remote-tables-twin.equivalence.test.ts drives the real ExternalDatasourceService behind the federation route this CLI calls (GET …/external/tables) through the real Hono adapter: '?schema= returns the SAME filtered set on both spellings' narrows to the public tables, and 'a ?schema= that matches nothing returns the SAME empty set on both spellings'. packages/services/service-datasource/src/__tests__/external-datasource-service.test.ts adds 'filters by requested schema', and packages/cli/src/commands/datasource/envelope-unwrap.test.ts adds 'keeps \"no remote tables\" reachable from an empty server list'. The CLI's flag-to-query forwarding is not pinned; every other clause still needs the run" - }, "source": [ "packages/cli/src/commands/datasource/list-tables.ts (route doc, listing), introspect.ts (route doc, flags, the cwd jail, REVIEW passthrough), validate.ts (route doc, diff rendering + exit)", "packages/rest/src/external-datasource-routes.ts#routes + rest-route-ledger.ts (the served federation routes incl. POST …/external/validate) and external-datasource-routes-auth-guard.test.ts (the auth wall the token satisfies)", @@ -1455,6 +1452,12 @@ "date": "2026-10-20", "change": "acceptance[0].verify, fixtures.knownGaps[2] and steps[1] no longer score the narrowing half from the pins. PR #24891's carried finding (round 15's acceptance notes: the 'score from the pin' spelling, a log clause's half), read under RUNNER rule 6. acceptance[0] is a log clause, the CLI's printed listing. Its pins drive the federation route through the real Hono adapter (remote-tables-twin.equivalence.test.ts), the service (external-datasource-service.test.ts) and the CLI over a stubbed fetch (envelope-unwrap.test.ts), so on the stock SQLite fixture that half is recorded blocked(fixture) with the pins beside it. steps[1] and knownGaps[2] said the same and now say this. automated.ref is unchanged. Read at base feb09db14a. No clause changed", "ref": "#24963" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "automated removed and fixtures.knownGaps[3] added; knownGaps[2] and acceptance[0].verify now point at it. PR #24963's carried finding (round 16's acceptance notes): automated.ref still claimed 'acceptance[0]'s NARROWING half ONLY'. Read at base e4d604efd3 under RUNNER rule 6, as round 15 applied it: acceptance[0] is a log clause, the CLI's printed listing against a served datasource, and its three pins drive the federation route through the real Hono adapter over a fake introspector, the service, and the CLI over a stubbed fetch. None holds a full clause of this item at its layer, so automated goes, and the knownGap names what each pin asserts and when automated may return. No clause changed", + "ref": "#25003" } ] }, @@ -1760,7 +1763,7 @@ "title": "autoMigrate off|safe: a dev boot self-heals ONLY the safe-categorized drift and reports each applied op, `off` (the driver default) and NODE_ENV=production never touch the schema, and a duplicates-blocked tighten is never auto-applied", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "cli", "personas": ["operator (local shell)"], @@ -1773,8 +1776,7 @@ ], "knownGaps": [ "the duplicates-BLOCKED leg (ADR-0120 D4 tightenNullSafeOnly recreate: duplicate rows found by the pre-flight probe → op blocked with a row report, old index left in place) needs planted duplicate rows under a legacy platform-wide unique index — stageable only by writing rows directly into the scratch sqlite file before boot. The leg is unit-pinned (sql-driver-unique-tenancy.test.ts 'auto-tightens at boot … when the probe is clean' + its duplicates sibling); a run may cite the pin per RUNNER rule 6 instead of hand-staging, and must record WHICH it did", - "`autoMigrate` is honoured by the native sqlite / postgres / mysql drivers only — the sqlite-wasm driver is constructed without it (the guidance entry on SqliteWasmConfigSchema says writing it there changes nothing), and turso's config declares no such key (standalone-stack.ts). Do not stage the wasm/turso arms expecting behavior", - "DEFECTIVE PIN, scores no clause (QA run #24840; routed to the wave anchor #24579 as a test fix, not repaired by a ledger edit): sql-driver-schema-drift.test.ts 'is force-disabled under NODE_ENV=production (warns, does not alter)' stages relax_not_null, which ADR-0113 made needs_confirm. A safe reconcile never applies needs_confirm, so the column stays NOT NULL with or without the production guard: its sibling case 'does NOT auto-relax a stray NOT NULL — needs_confirm survives dev auto-reconcile' asserts that same outcome with no NODE_ENV set. The case also asserts no warning. Its green says nothing about the production arm of reconcileAndWarnDrift (packages/drivers/driver-sql/src/sql-driver.ts). Score acceptance[2] only from steps[3]'s live boot, which re-plants the legacy index: replace_unique_index is the safe-category drift that arm must hold back" + "`autoMigrate` is honoured by the native sqlite / postgres / mysql drivers only — the sqlite-wasm driver is constructed without it (the guidance entry on SqliteWasmConfigSchema says writing it there changes nothing), and turso's config declares no such key (standalone-stack.ts). Do not stage the wasm/turso arms expecting behavior" ] }, "variants": [ @@ -1840,7 +1842,7 @@ "an authored `autoMigrate` on a sqlite-wasm datasource appearing to DO something is a FAIL of the declared boundary (the wasm driver is constructed without it — the guidance entry says writing it changes nothing); conversely, do not file the wasm no-op as a missing capability" ], "traps": ["stale-dist", "destructive-in-place"], - "automated": { "kind": "unit", "ref": "packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts — acceptance[4] and negative[2], at the driver: its 'bare-composite tightening + duplicate pre-flight (ADR-0120 D4)' describe, where 'auto-tightens at boot under autoMigrate: safe when the probe is clean' converges with zero drift, and 'BLOCKS the tightening on duplicate NULL-organization rows — old index kept, rows reported' keeps the old index through initObjects, reports the op BLOCKED naming the duplicate group, and refuses even an allowDestructive apply until the duplicate is gone; + packages/drivers/driver-sql/src/sql-driver-schema-drift.test.ts — negative[0] ONLY ('does NOT auto-apply destructive drift (orphan column kept; warned instead)'). Its 'is force-disabled under NODE_ENV=production (warns, does not alter)' case scores NOTHING (knownGaps[2]). acceptance[0]–[3] and [5], and negative[1] and [3], are boot-log and schema-dump clauses: they need the scratch boots" }, + "automated": { "kind": "unit", "ref": "packages/drivers/driver-sql/src/sql-driver-unique-tenancy.test.ts — acceptance[4] and negative[2], at the driver: its 'bare-composite tightening + duplicate pre-flight (ADR-0120 D4)' describe, where 'auto-tightens at boot under autoMigrate: safe when the probe is clean' converges with zero drift, and 'BLOCKS the tightening on duplicate NULL-organization rows — old index kept, rows reported' keeps the old index through initObjects, reports the op BLOCKED naming the duplicate group, and refuses even an allowDestructive apply until the duplicate is gone; + packages/drivers/driver-sql/src/sql-driver-schema-drift.test.ts — negative[0] ('does NOT auto-apply destructive drift (orphan column kept; warned instead)'), and acceptance[2] at the driver layer only. Its 'is force-disabled under NODE_ENV=production (warns, does not alter)' case, re-staged by #24903 (PR #24967) with a safe drift, sits beside its development control 'applies the safe drop_column_default drift outside production (control for the guard below)'. Under NODE_ENV=production with autoMigrate 'safe', SqlDriver.initObjects over in-memory better-sqlite3, with a stub logger, keeps the column's current_user DEFAULT, still reports that drift safe / drop_column_default, and warns with the prefix \"[schema-drift] autoMigrate='safe' is ignored under NODE_ENV=production\"; the control applies the same drift under development. The case reads no boot log and leaves the warning's remedy, the text after its em dash, unasserted, so under RUNNER rule 6 it scores acceptance[2] partial, never pass, and steps[3]'s production boot is that clause's live leg. acceptance[0], [1], [3] and [5], acceptance[2]'s boot log, and negative[1] and [3] are boot-log and schema-dump clauses: they need the scratch boots" }, "source": [ "packages/spec/src/data/driver/common.zod.ts#SqlAutoMigrateSchema (SqlAutoMigrateSchema z.enum(['off','safe']) — 'Dev-only, loosen-only schema self-heal (#2186) … force-disabled under NODE_ENV=production') + sqlite.zod.ts (the authored key; the wasm not-honoured guidance)", "packages/drivers/driver-sql/src/sql-driver.ts#SqlDriverConfig (SqlDriverConfig.autoMigrate docblock: non-destructive alters only), (default 'off'), (reconcileAndWarnDrift — the managed+safe gate, the production ignore warning verbatim, safe-only applyMigrationEntries allowDestructive:false, per-op auto-reconciled info line, post-reconcile re-detect, warn-once loop)", @@ -1854,7 +1856,8 @@ { "revision": 2, "date": "2026-10-17", "change": "fixture and steps re-pointed (checklist-accuracy finding, #23878 close-out): a config-declared datasource named `default` is refused by defineStack, `required: true` no longer makes a NOT NULL column (storage.notNull, ADR-0113), and relax_not_null is needs_confirm, so the step-0 drift the safe leg relied on was never auto-appliable. The fixture now names a datasource routed by datasourceMapping default:true; step 0 stages one drift per category (storage.notNull relax = needs_confirm, a planted legacy platform-wide unique index = safe, a removed field = destructive); the steps read a schema dump instead of `os migrate plan`, which composes no driver for a config-declared primary and says its plan is PARTIAL; the safe variant lists the real safe ops; clauses 0, 1 and 3's verify follow. No clause's ruling changed. Re-run on main c91af8ed6 on a minimal scratch stack: datasource named default → build exit 2 with the reserved-name refusal; qa_main + mapping built. off and omitted boots each warned all three drifts and left the dump byte-identical; `os migrate plan` (also with --database-url) reported 'Examined 0 managed table(s)' and a PARTIAL plan. safe boot: '[schema-drift] auto-reconciled replace_unique_index on qa_thing.code', the legacy qa_thing_code_unique gone from the dump, the relax and orphan warnings still printed; the second safe boot printed no reconcile line. NODE_ENV=production with safe: the named ignore warning verbatim, all three warnings, dump unchanged. Keyless os dev over a planted legacy index: auto-reconciled replace_unique_index; non-dev os start over a re-planted one: the warning only, the index kept. Duplicates leg on the pin: driver-sql sql-driver-unique-tenancy.test.ts (incl. 'auto-tightens at boot under autoMigrate: safe when the probe is clean' and 'BLOCKS the tightening on duplicate NULL-organization rows') + sql-driver-schema-drift.test.ts, 38 passed", "ref": "#24036" }, { "revision": 3, "date": "2026-10-19", "change": "steps[4], acceptance[5] (clause, verify) and source[3] re-pointed to the injection site that exists — checklist-accuracy finding of the 18.0 QA sweep (QA run #24551, cli.dev-automigrate-policy acceptance[5] / steps[4]; wave anchor #24579 §3): the cited storage-driver.ts `isDev ? { autoMigrate: 'safe' } : {}` and serve.ts telemetry branch moved. Re-derived at base 9a7838edf5: since #21733 the decision lives in packages/runtime/src/dev-auto-migrate.ts (devAutoMigrateConfig), read by createStandaloneStack's default datasource, resolveStorageDefinition's fallback and the telemetry sibling. The run found the behaviour matching the clause; the clause now names the one decision and the three readers", "ref": "#24649" }, { "revision": 4, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[4] cited lines 322-342 of sql-driver-index-drift.test.ts in the comma-range spelling (authored in 45397edf53 as the list 112-113 and 322-342). Those lines are unchanged at this base: the describe block \"dev auto-reconcile (autoMigrate: 'safe')\", which source[4] now names. A test file declares no symbol worth anchoring, so the block is named in prose. The count of passed tests that history[1] records after a file name is not a citation, and that row is a dated record, so it is left. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, - { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause, fixtures.knownGaps[2] added — checklist-accuracy findings of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4), round 15 of wave anchor #24579. (1) The bare ref named sql-driver-schema-drift.test.ts alone. By RUNNER rule 6 (a pin scores only the layer it drives) that pin scores negative[0] only. (2) Its production force-off case stages relax_not_null, needs_confirm since ADR-0113, so it passes with or without the guard and asserts no warning. It scores no clause, and the knownGap records it for the anchor to route as a test fix. (3) sql-driver-unique-tenancy.test.ts, which acceptance[4].verify and knownGaps[0] already named, was missing from the ref. Its ADR-0120 D4 cases assert acceptance[4] and negative[2] at the driver, the layer that test-oracle clause names. Read at base b1676441ba. No clause changed", "ref": "#24891" } + { "revision": 5, "date": "2026-10-20", "change": "automated.ref narrowed per clause, fixtures.knownGaps[2] added — checklist-accuracy findings of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4), round 15 of wave anchor #24579. (1) The bare ref named sql-driver-schema-drift.test.ts alone. By RUNNER rule 6 (a pin scores only the layer it drives) that pin scores negative[0] only. (2) Its production force-off case stages relax_not_null, needs_confirm since ADR-0113, so it passes with or without the guard and asserts no warning. It scores no clause, and the knownGap records it for the anchor to route as a test fix. (3) sql-driver-unique-tenancy.test.ts, which acceptance[4].verify and knownGaps[0] already named, was missing from the ref. Its ADR-0120 D4 cases assert acceptance[4] and negative[2] at the driver, the layer that test-oracle clause names. Read at base b1676441ba. No clause changed", "ref": "#24891" }, + { "revision": 6, "date": "2026-10-20", "change": "automated.ref re-adds sql-driver-schema-drift.test.ts's production force-off case for acceptance[2] at the driver layer, and fixtures.knownGaps[2] is removed. #24903's follow-through (PR #24967, merged as f32d795ce5): knownGaps[2] recorded that the case staged relax_not_null, needs_confirm since ADR-0113, so it passed with or without the guard and asserted no warning. Read at base e4d604efd3, it now stages a safe drop_column_default drift (a current_user column DEFAULT) beside a development control that applies it, and under NODE_ENV=production it asserts the DEFAULT kept, the drift still safe, and the warning's stable prefix on the driver's stub logger (the guard and the full warning: reconcileAndWarnDrift in packages/drivers/driver-sql/src/sql-driver.ts). It drives the driver and reads no boot log, so under RUNNER rule 6 it scores acceptance[2] partial, and steps[3]'s production boot stays that clause's live leg. No clause changed", "ref": "#25003" } ] }, { diff --git a/docs/qa/platform-checklist/areas/dashboards.json b/docs/qa/platform-checklist/areas/dashboards.json index 22dd0263c6..83d5c74252 100644 --- a/docs/qa/platform-checklist/areas/dashboards.json +++ b/docs/qa/platform-checklist/areas/dashboards.json @@ -1154,7 +1154,7 @@ "title": "Scheduled report dispatch: the dispatcher ticks on a stock boot, a due schedule runs under the OWNER's authority or FAILS CLOSED with the refusal recorded on the row — never an RLS-bypassed run — and the delivery/bookkeeping contract (format fork, subject template, next-run advance) holds", "since": "v15.1", "status": "retired", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -1175,7 +1175,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); read the boot log for the dispatcher registration line — 'dispatcher registered with job service' or 'dispatcher registered (setInterval fallback)', with its intervalMs — and for the email arm (LogTransport line, or the 'no email service — schedules will fire without delivery' warn)", + "boot the showcase on its own port and DB (dogfood §0); read the boot log for the dispatcher registration line — 'dispatcher registered with job service' or 'dispatcher registered (setInterval fallback)', with its intervalMs — and for the email arm (LogTransport line, or the 'no email service — schedules will fire without delivery' warn)", "as a fresh member: POST /api/v1/reports (a report over an object the member can read, e.g. showcase_task) then POST /api/v1/reports/:id/schedule with { recipients: [''], intervalMinutes: 1, format: 'csv', subjectTemplate: '{{name}}: {{rows}} on {{date}}' } — record the receipt's next_run_at (first fire = create time + interval; an invalid cronExpression is refused eagerly at create, not left to fall back silently at 3am)", "wait past next_run_at + one dispatcher tick, then as the member GET /api/v1/reports/:id/schedules and read the row's last_status / last_error / last_sent_at / next_run_at", "capture the server log lines for the tick (ReportService.dispatchDue warn/error lines)", @@ -1252,6 +1252,12 @@ "date": "2026-09-25", "change": "retired — the saved-report stack it exercised was removed (#20102); see retiredReason", "ref": "#20102" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ], "retiredReason": "Capability removed (ADR-0049 enforce-or-remove; maintainer ruling 2026-09-25, #20102): the saved-report schedule dispatcher this item drove left with the rest of the saved-report stack (sys_report_schedule, the /api/v1/reports schedule routes, @objectstack/plugin-reports), which had zero consumers. The platform has no scheduled delivery of a report today; scheduled delivery for `report` metadata is explicitly NOT built by that ruling — a real need is a new card on the metadata kind, and a new item with it." diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index 6e883b503e..a2341c680d 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -1123,7 +1123,7 @@ "title": "A declared external datasource federates: its objects query in place over REST, read-only", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -1136,7 +1136,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as admin", + "boot the showcase on its own port and DB; sign in as admin", "GET /api/v1/meta and confirm the federated objects are registered", "GET /api/v1/data/ and read fixture rows", "attempt a write (POST/PATCH) against the read-only external object; capture the refusal", @@ -1182,7 +1182,8 @@ }, { "revision": 2, "date": "2026-08-18", "change": "re-worded clause 3 and step 5 for this item's declared api surface. Both named the Setup → Datasources console page inside an item whose surface is api, so the clause could only ever be scored by a substitution the item did not authorize. The datasources admin API is what the page renders from, so it is now the oracle and the page is the optional substitution — surface left as api rather than widened to mixed, since every other clause here is a server read (#9417)", "ref": "#9386" }, { "revision": 3, "date": "2026-10-16", "change": "acceptance[0].verify corrected (checklist-accuracy finding of QA run #23922, wave #23878): 'filters/$top work as on a native object' — OData $filter is refused 400 INVALID_FILTER on /data (the filter must be JSON), while the JSON filter param and top work, which is what #23922 used. The verify text names the working spellings and the refusal. Not loosened: a filter that returns the unfiltered set still FAILS", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-20", "change": "checklist-accuracy findings of QA run #24575 (18.0 sweep, 5b14b7f3; wave anchor #24579 §3). acceptance[2]'s oracle still read 'screenshot' and its evidence 'screenshot', although revision 2 made the datasources API the oracle; both now name the API reads. steps[4] names the console route for the substitution (…/setup/datasources is not one). fixtures.requires[0] says where the fixture DB is provisioned. Not loosened: the clause and its verify are unchanged", "ref": "#24717" } + { "revision": 4, "date": "2026-10-20", "change": "checklist-accuracy findings of QA run #24575 (18.0 sweep, 5b14b7f3; wave anchor #24579 §3). acceptance[2]'s oracle still read 'screenshot' and its evidence 'screenshot', although revision 2 made the datasources API the oracle; both now name the API reads. steps[4] names the console route for the substitution (…/setup/datasources is not one). fixtures.requires[0] says where the fixture DB is provisioned. Not loosened: the clause and its verify are unchanged", "ref": "#24717" }, + { "revision": 5, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1346,7 +1347,7 @@ "title": "External-datasource schema introspection happy path: remote-tables lists live tables with columnCount (?schema= honoured since #7955), object-draft renders a reviewable *.object.ts, both route spellings answer as one operation behind the platform auth floor", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -1365,7 +1366,7 @@ ] }, "steps": [ - "boot showcase isolated via os dev; sign in as the admin holding manage_platform_settings; also mint an authenticated session holding no capability", + "boot the showcase on its own port and DB via os dev; sign in as the admin holding manage_platform_settings; also mint an authenticated session holding no capability", "GET /api/v1/datasources/showcase_external/remote-tables — record the table set with per-table columnCount", "repeat with ?schema=main and with a repeated ?schema=a&schema=b — record status + set for each (see knownGaps for what SQLite can and cannot show here)", "GET /api/v1/datasources/showcase_external/external/tables (the twin spelling) — compare the set against step 2", @@ -1452,6 +1453,12 @@ "date": "2026-10-20", "change": "fixtures.knownGaps[0] corrected — checklist-accuracy finding of QA run #24615 (18.0 sweep round 2, ba4afcc2; wave anchor #24579 §3, round 2 part 2). It said a schema-qualified Postgres fixture would make ?schema= narrowing live-scoreable; measured on PostgreSQL 16, the SQL driver's Postgres introspection returns bare names from current_schemas(false) too, so narrowing stays unobservable live. That is the product defect #24664, split from #24619 row (c); acceptance[1] states the #7955 contract and stays as written, scored by its pin. No clause changed", "ref": "#24717" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1588,7 +1595,7 @@ "title": "A recipient preference muting the inbox channel for a topic suppresses delivery (no inbox row, no dead-letter fake); flipping it back resumes; a sys_notification_template renders through a delivery", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P2", "surface": "mixed", "personas": [ @@ -1608,7 +1615,7 @@ ] }, "steps": [ - "boot showcase isolated; personas admin + assignee; establish the happy path first (or lean on notify-inbox-delivery): reassign a showcase_task to the assignee → showcase_task_assigned_notify fires topic 'task.assigned' on channel 'inbox' → one sys_inbox_message for the assignee, visible in GET /api/v1/notifications", + "boot the showcase on its own port and DB; personas admin + assignee; establish the happy path first (or lean on notify-inbox-delivery): reassign a showcase_task to the assignee → showcase_task_assigned_notify fires topic 'task.assigned' on channel 'inbox' → one sys_inbox_message for the assignee, visible in GET /api/v1/notifications", "author a suppression row via POST /api/v1/data/sys_notification_preference: { user_id: , topic: 'task.assigned', channel: 'inbox', enabled: false } — this is the 'category' the task names; the model's field is `topic` (most-specific-wins over the built-in default-ON)", "re-fire the notify flow (reassign the task to the same assignee again); GET /api/v1/notifications as the assignee and confirm NO new inbox row landed", "read the messaging outbox / dead-letter surface and confirm there is no dead-letter row for the muted (recipient, 'inbox') — a deliberate mute is a DROP by the preference filter, not a dead-letter (dead-letter is reserved for unimplemented channels, #3197)", @@ -1683,6 +1690,12 @@ "date": "2026-10-16", "change": "steps[6] and acceptance[4].clause corrected (checklist-accuracy finding of QA run #23922, wave #23878). 'with locale fallback' overstated the ladder: the store walks only DOWN (en-US → en → en), so a recipient with no locale resolves the deployment default 'en' and an en-US-only template row is never used — #23922 authored its row at 'en'. The step now says where to author the row, and the clause names the ladder. Not loosened: the render must still carry the template's substituted subject and body", "ref": "#24036" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -1944,7 +1957,7 @@ "title": "Setup → Datasources federation UI (ADR-0015 §6.4): SchemaBrowser mirrors the remote-table listing, ImportObjectDialog lands a REAL object through the metadata channel, Refresh catalog and ValidationPanel drive the three previously-uncovered federation routes (import / refresh-catalog / validate) behind their capability floors", "since": "v16", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -1966,7 +1979,7 @@ ] }, "steps": [ - "boot showcase isolated via os dev with the console; sign in as the entitled admin; pre-capture the API ground truth: GET /api/v1/datasources/showcase_external/external/tables and POST .../external/validate", + "boot the showcase on its own port and DB via os dev with the console; sign in as the entitled admin; pre-capture the API ground truth: GET /api/v1/datasources/showcase_external/external/tables and POST .../external/validate", "type the datasource's engine EDIT route directly, /_console/apps/setup/metadata/datasource/showcase_external — no entry from the Setup list reaches it: Setup → Datasources (…/metadata/datasource, which the left-nav names, objectui#3660/register.ts) is the custom DatasourceResourcePage list, whose rows offer Test / Sync objects / Edit / Delete and neither open the item nor render the panel (objectui DatasourceResourcePage.tsx, at the sweep pin 4016370a and the shipped pin 12dc7b47c340; QA run #24615). On the edit route the ExternalDatasourcePanel renders (DatasourcePreview.tsx keys it off schemaMode !== 'managed') with the schemaMode badge and the read-only/writes-allowed pill; screenshot FIRST, then read the DOM", "Tables tab: SchemaBrowser loads the remote tables (mount + explicit Refresh only — never a timer); compare the rendered list name-by-name and columnCount-by-columnCount against the pre-captured listing", "header 'Refresh catalog': click it and capture the POST /api/v1/datasources/showcase_external/external/refresh-catalog on the network; the snapshot timestamp renders from the returned catalog.snapshotAt", @@ -2070,6 +2083,12 @@ "date": "2026-10-20", "change": "checklist-accuracy findings of QA run #24615 (18.0 sweep round 2, ba4afcc2, driven by hand at the console pin 4016370a; wave anchor #24579 §3, round 2 part 2), re-read in objectui at the shipped pin 12dc7b47c340. steps[1]: no entry from the Setup list reaches the panel (DatasourceResourcePage rows never open the item), so the step names the engine edit route. acceptance[0] (clause, verify, evidence): the save-first guidance is unreachable (datasource is not in CREATE_MODE_CANVAS_TYPES), so the clause scores the observable half, no name-less call, and records the guidance half not-run. steps[7]: the quoted refusal was POST /meta/_migrate-stored's; the item save door answers 403 FORBIDDEN 'Saving a metadata item requires…'. knownGaps[3] carries #24615's drift recipe that needs no scratch copy; knownGaps[4] (new) records that a repeated import answers 201 and overwrites, which no clause scores. acceptance[3].verify's comma-range line citation names the handler instead (the line-citation rot class). Not loosened: every other clause and negative is unchanged", "ref": "#24717" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/platform-core.json b/docs/qa/platform-checklist/areas/platform-core.json index 433083be70..54283fb80b 100644 --- a/docs/qa/platform-checklist/areas/platform-core.json +++ b/docs/qa/platform-checklist/areas/platform-core.json @@ -1407,7 +1407,7 @@ "title": "The package manifest is enforced at the install boundary: an incompatible `engines` range is refused before the registry write, the namespace gate holds ownership, and a namespace-less runtime package has one derived", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "api", "personas": [ @@ -1428,7 +1428,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as admin. Read the runtime's protocol version from the running server (the boot banner / GET /api/v1/meta), take its leading major as RT, and derive every range below from RT — never from a literal", + "boot the showcase on its own port and DB; sign in as admin. Read the runtime's protocol version from the running server (the boot banner / GET /api/v1/meta), take its leading major as RT, and derive every range below from RT — never from a literal", "BASELINE: POST /api/v1/packages with { manifest: { id: 'com.example.qa-manifest-probe-a', name: 'Manifest Probe A', version: '1.0.0', scope: 'project', type: 'app', namespace: 'qa_mfp', engines: { protocol: '^RT' } } }; capture status + body and GET it back — this is the compatible control every refusal below is judged against. Every probe manifest below carries the same scope 'project' (the manifest's scope enum is cloud | system | project, default project — packages/spec/src/kernel/manifest.zod.ts; a scope such as 'custom' is refused 400 VALIDATION_ERROR before any gate this item scores is reached)", "INCOMPATIBLE: POST id 'com.example.qa-manifest-probe-old' with engines: { protocol: '^(RT-1)' }; capture the refusal IN FULL (status, code, message, rangeSource, targetMajor, migrateCommand). Then GET /api/v1/packages/com.example.qa-manifest-probe-old and GET /api/v1/packages and confirm NO row exists for that id", "PRECEDENCE, both directions: POST 'com.example.qa-manifest-probe-prec-ok' with engines: { protocol: '^RT', platform: '^(RT-1)' } → expect install (protocol is consulted first, so the incompatible platform range is never reached); POST 'com.example.qa-manifest-probe-prec-no' with engines: { protocol: '^(RT-1)', platform: '^RT' } → expect refusal whose diagnostic names `engines.protocol` as the range source", @@ -1521,6 +1521,12 @@ "date": "2026-10-19", "change": "fixtures.knownGaps[3] and negative[2] corrected — a checklist-accuracy finding of the 18.0 QA sweep (QA run #24557, platform-core.manifest-install-contract; wave anchor #24579 §3): both said NamespaceConflictError has no `code`, so its wire mapping was unpinned. Since commit df657d9df the class carries code NAMESPACE_CONFLICT and status 422 (packages/objectql/src/registry.ts), and the run observed 422 NAMESPACE_CONFLICT on POST /api/v1/packages. knownGaps[3] is now marked CLOSED with that reading, and negative[2] scores a 5xx as a FAIL instead of a finding to record. No acceptance clause changed", "ref": "#24632" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index a88c818871..211d546c5a 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -66,7 +66,7 @@ "title": "Create → read → update → delete a record through the console UI", "since": "v15", "status": "active", - "revision": 12, + "revision": 13, "priority": "P0", "surface": "browser", "personas": [ @@ -82,7 +82,8 @@ "why": "supplies the persona clause 7 (clone is RLS-gated) rests on, and the non-owner cloner clause 6's ownership half needs (cloning a seeded showcase_field_zoo row, acceptance[6].verify) — a reader for whom a SOURCE row is invisible. showcase_account, the object the rest of this item drives, is public_read_write, so no persona can fail to read a row on it and the clause is unscoreable there; clause 7 is driven on showcase_invoice against INV-1003 (linus@example.com's) instead. Without the recipe the clause scores blocked(fixture) — which is what the 17.1.0 sweep recorded after reaching for the seeded demo personas and cloning showcase_account as Mei, who reads it fine (#10236)." }, "knownGaps": [ - "Clause 7 (clone is RLS-gated) cannot be scored on showcase_account, the object every other clause here drives: it is public_read_write, so there is no persona for whom a source row is invisible. It needs showcase_invoice plus a contributor-bound member — the recipe qa-contributor-bound-member, which areas/search.json owns. That reference now lives in fixtures.provisioning as the AREA-QUALIFIED `search:qa-contributor-bound-member`, and check:platform-checklist RESOLVES it (#10593 gap 2, maintainer ruling 2026-08-22 — option A): both spellings are valid, an own-area key and `:` for one another area owns. So this is a real opt-in rather than a prose pointer — rename or move the recipe and this item fails the gate by name, instead of leaving a sentence aimed at nothing. ⛔ Replay it from areas/search.json; do not fork a second copy into this area." + "Clause 7 (clone is RLS-gated) cannot be scored on showcase_account, the object every other clause here drives: it is public_read_write, so there is no persona for whom a source row is invisible. It needs showcase_invoice plus a contributor-bound member — the recipe qa-contributor-bound-member, which areas/search.json owns. That reference now lives in fixtures.provisioning as the AREA-QUALIFIED `search:qa-contributor-bound-member`, and check:platform-checklist RESOLVES it (#10593 gap 2, maintainer ruling 2026-08-22 — option A): both spellings are valid, an own-area key and `:` for one another area owns. So this is a real opt-in rather than a prose pointer — rename or move the recipe and this item fails the gate by name, instead of leaving a sentence aimed at nothing. ⛔ Replay it from areas/search.json; do not fork a second copy into this area.", + "no pin scores a full clause of this item at its layer (RUNNER rule 6, a pin scores only the layer it drives), so the item carries no automated ref. packages/metadata-protocol/src/protocol.readonly-insert.test.ts ('cloneData forwards the copied row AND the caller overrides whole — and reports the engine’s verdict on both') drives cloneData over a stub engine: the source's readonly column travels to engine.insert whole, the 201 body's droppedFields names it, and the clone's record does not carry it, which pins the protocol's delegation. packages/objectql/src/engine-insert-static-readonly-strip.test.ts drives a real ObjectQL engine over a recording driver, with no HTTP: a non-system caller's readonly key is stripped on create and falls back to the field's defaultValue, which pins the engine's strip. That is evidence for acceptance[6]'s readonly half at those two layers, never its pass. automated may return when a pin clones a row carrying a readonly value through the HTTP clone door and reads the clone back" ] }, "steps": [ @@ -125,7 +126,7 @@ { "clause": "record History reflects the create and the update with display values (option labels, localized dates, real actor), not raw audit payloads or phantom value→null rows", "oracle": "dom", - "verify": "after a screenshot confirms the History tab rendered, read its entries — same contract objectui e2e/live/record-history-display.spec.ts pins on showcase_account, the showcase's enable.trackHistory object (its option labels, the localized Customer Since date and the resolved owner; objectui blob 7ebc45106cff at the console pin 616df6598b42)", + "verify": "after a screenshot confirms the History tab rendered, read its entries — same contract objectui e2e/live/record-history-display.spec.ts pins on showcase_account, the showcase's enable.trackHistory object (its option labels, the localized Customer Since date and the resolved owner; objectui blob 7ebc45106cff at the console pin cf153eb2e535)", "evidence": "screenshot + entries" }, { @@ -135,10 +136,10 @@ "evidence": "re-read JSON + search response" }, { - "clause": "clone (POST /data/:object/:id/clone, gated by enable.clone default-on) returns 201 with a NEW id and the source's field VALUES copied, but engine-owned columns (id, audit, autonumber, formula, summary) and readonly columns RE-DERIVED not carried, and the clone is owned by the CLONER — not the source's owner. ⚠️ The readonly half is UNOBSERVABLE on the stock seed: showcase_account declares no readonly column, no showcase object has an approval_status, and the showcase's one readonly field (showcase_contact.lead_score) is null on every seeded row, so carried and re-derived read identically. Score that half from the pins in `automated`, never as blocked(fixture) and never off two reads that agree only because the column is null: no live run can stage it, because the server drops a readonly column set over HTTP (`x-objectstack-dropped-fields: lead_score;reason=readonly`, QA run #21782)", + "clause": "clone (POST /data/:object/:id/clone, gated by enable.clone default-on) returns 201 with a NEW id and the source's field VALUES copied, but engine-owned columns (id, audit, autonumber, formula, summary) and readonly columns RE-DERIVED not carried, and the clone is owned by the CLONER — not the source's owner. ⚠️ The readonly half is UNOBSERVABLE on the stock seed: showcase_account declares no readonly column, no showcase object has an approval_status, and the showcase's one readonly field (showcase_contact.lead_score) is null on every seeded row, so carried and re-derived read identically. Drive that half at this clause's own layer, the clone door, on a source row whose readonly value the run planted in its own file DB (verify). A run that does not plant one records that half blocked(fixture), never pass, and never off two reads that agree only because the column is null. The pins fixtures.knownGaps[1] names are evidence of their own layers only (RUNNER rule 6). The value has to be planted, because the server drops a readonly column a caller sets over HTTP (`x-objectstack-dropped-fields: lead_score;reason=readonly`, QA run #21782)", "oracle": "api", - "verify": "the 201 result carries {id (new ≠ sourceId), sourceId, record}; field-by-field diff shows business values copied and system/readonly columns re-derived (#3043 CLONE_STRIP_FIELDS + the engine's create-side static-readonly strip, #14147); owner_id resolves to the signed-in cloner (the clone is a create in the caller's context — packages/metadata-protocol/src/protocol.ts cloneData). ⚠ The owner half needs a cloner who is NOT the source's owner: the admin cloning a row the admin created reads the same owner_id either way (QA run #24572). Drive it as the provisioned contributor-bound member cloning a seeded showcase_field_zoo row (QA run #24572's suggestion, source-derived and not yet run — record the substitution): showcase_contributor grants allowCreate there (not on showcase_account, which it may only read; examples/app-showcase/src/security/permission-sets.ts), the zoo is public_read_write, and the same clone shows f_autonumber and f_formula re-derived", - "evidence": "clone response + source-vs-clone field diff + owner_id read" + "verify": "the 201 result carries {id (new ≠ sourceId), sourceId, record}; field-by-field diff shows business values copied and system/readonly columns re-derived (#3043 CLONE_STRIP_FIELDS + the engine's create-side static-readonly strip, #14147); owner_id resolves to the signed-in cloner (the clone is a create in the caller's context — packages/metadata-protocol/src/protocol.ts cloneData). ⚠ The owner half needs a cloner who is NOT the source's owner: the admin cloning a row the admin created reads the same owner_id either way (QA run #24572). Drive it as the provisioned contributor-bound member cloning a seeded showcase_field_zoo row (QA run #24572's suggestion, source-derived and not yet run — record the substitution): showcase_contributor grants allowCreate there (not on showcase_account, which it may only read; examples/app-showcase/src/security/permission-sets.ts), the zoo is public_read_write, and the same clone shows f_autonumber and f_formula re-derived. The readonly half: with the server stopped, write a value into one seeded showcase_contact row's lead_score in the run's own file DB with node:sqlite (no sqlite3 CLI in this container), `UPDATE showcase_contact SET lead_score = 42 WHERE id = 'ID'`, which reports one changed row; restart, read the row back over GET /api/v1/data/showcase_contact/ID (lead_score 42), then POST /api/v1/data/showcase_contact/ID/clone as the admin. The 201's droppedFields names lead_score with reason readonly (cloneData forwards the copy whole to engine.insert and reports the engine's onFieldsDropped verdict, packages/metadata-protocol/src/protocol.ts), and a GET of the clone carries no lead_score value: the field declares no defaultValue to fall back to (examples/app-showcase/src/data/objects/contact.object.ts). Derived from source, not replayed here", + "evidence": "clone response + source-vs-clone field diff + owner_id read; for the readonly half, the planted source read, the 201's droppedFields and the clone's lead_score read (or that half's blocked(fixture), with the knownGaps[1] pins beside it)" }, { "clause": "clone is RLS-gated: the source is fetched in the caller's context (engine.findOne with context), so cloning a record the caller cannot SEE is refused with 404 RECORD_NOT_FOUND — never a silent duplicate of an invisible row", @@ -157,10 +158,6 @@ "hydration-race", "automation-input" ], - "automated": { - "kind": "test", - "ref": "acceptance[6]'s READONLY half ONLY. packages/metadata-protocol/src/protocol.readonly-insert.test.ts ('cloneData forwards the copied row AND the caller overrides whole — and reports the engine’s verdict on both'): the source's readonly column travels to engine.insert whole, the 201 body's droppedFields names it, and the clone's record does not carry it (the engine there is a stub, so this pins the delegation). packages/objectql/src/engine-insert-static-readonly-strip.test.ts pins the enforcement on a REAL engine: a non-system caller's readonly key is stripped on create and falls back to the field's defaultValue. Every other clause still needs the run" - }, "source": [ "dogfood-verification skill §3", "examples/app-showcase/src/data/objects/account.object.ts (requiredness + format/conditional validations)", @@ -237,6 +234,12 @@ "date": "2026-10-20", "change": "acceptance[4].verify names the object the objectui spec drives. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said the spec pins the History contract 'on showcase Project'. Read at objectstack's .objectui-sha pin 616df6598b42 (objectui blob 7ebc45106cff): e2e/live/record-history-display.spec.ts creates and patches a showcase_account, the showcase's enable.trackHistory object, and asserts its option labels, the localized signed_on date and the resolved owner. The live verdict at the pin is not measured here. No clause changed", "ref": "#24963" + }, + { + "revision": 13, + "date": "2026-10-20", + "change": "acceptance[6] (clause, verify, evidence) drives its readonly half at its own layer; automated is removed and fixtures.knownGaps[1] added. PR #24963's carried finding (round 16's acceptance notes): the P0 api clause said to score its readonly half from the pins in automated, never as blocked(fixture). Read at base e4d604efd3 under RUNNER rule 6: protocol.readonly-insert.test.ts drives cloneData over a stub engine and engine-insert-static-readonly-strip.test.ts a real engine over a recording driver, neither through HTTP, and no pin holds a full clause of this item. The live leg can be staged: a lead_score value written into the run's own file DB travels in the clone's copy, and cloneData reports the engine's readonly drop in the 201's droppedFields (packages/metadata-protocol/src/protocol.ts). So the half is driven at the clone door on a planted row, or recorded blocked(fixture) with the pins beside it as evidence of their own layers. Also PR #24982's pin finding: acceptance[4].verify now names the console pin cf153eb2e535, where e2e/live/record-history-display.spec.ts keeps blob 7ebc45106cff (it named 616df6598b42). acceptance[6].clause changed how its readonly half is scored and staged; what the clause asserts did not change", + "ref": "#25003" } ] }, @@ -245,7 +248,7 @@ "title": "Field-type matrix: every FieldTypeSchema member renders its widget, accepts a valid value, round-trips over HTTP, and rejects invalid input", "since": "v15", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -315,7 +318,7 @@ ], "steps": [ "run the pinned HTTP round-trip matrix: pnpm --filter @objectstack/dogfood exec vitest run test/field-zoo-roundtrip.dogfood.test.ts (write vectors + expected read shapes live in test/field-zoo.matrix.ts)", - "boot showcase isolated; open Field Zoo (/_console/apps/com.example.showcase/showcase_field_zoo) and open 'Specimen — Full' in edit mode; screenshot the form before reading DOM", + "boot the showcase on its own port and DB; open Field Zoo (/_console/apps/com.example.showcase/showcase_field_zoo) and open 'Specimen — Full' in edit mode; screenshot the form before reading DOM", "enumerate the rendered control for every f_* field and build a variant→widget table (date→date input, color→color input, richtext / markdown / html / code→their dedicated field widgets, select/multiselect→pickers, image/file→upload, location/address→structured inputs, rating/slider/progress→their own controls, formula→read-only). f_autonumber is NOT on the form at all: the field is readonly by construction (Field.autonumber sets readonly: true, packages/spec/src/data/field.zod.ts) and objectui's auto-form drops schema-readonly fields in create AND edit mode (plugin-form autoLayout.ts filterSystemFields) — read it on the record detail page instead. At the console pin the richtext, markdown, html and code widgets are textarea-based BY DESIGN (objectui packages/fields/src/widgets/RichTextField.tsx / CodeField.tsx: a Textarea inside the dedicated widget, with its own label and controls): record that as the dedicated widget, not as a generic text input", "through the form, change one representative value per family (a select, an array type, a temporal, a structured JSON), Save, and capture the PATCH", "re-read the record via GET /api/v1/data/showcase_field_zoo/ and diff against the submitted values (arrays compared as sets)", @@ -412,6 +415,12 @@ "date": "2026-10-19", "change": "steps[2], acceptance[1] (clause, evidence) and fixtures.knownGaps[2] corrected from QA run #24616 (18.0 sweep round 2, ba4afcc2), re-read at objectstack f89d29e382 and the console pin 4016370af3c9. (1) acceptance[1] asked for all 49 members on the Full edit form, which cannot hold: summary has no zoo carrier (knownGaps[0]) and autonumber is readonly by construction (field.zod.ts) and dropped from create and edit forms by plugin-form autoLayout.ts filterSystemFields; it is read on the detail page. (2) knownGaps[2] said f_secret writes fail closed on stock; the CLI wires a LocalCryptoProvider on every boot (serve.ts), and the run's PATCH answered 200 and read back masked. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[1] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ], "enumSource": { @@ -1728,7 +1737,7 @@ "title": "Action buttons surface at exactly their declared locations — list toolbar, list row, record header/more/related/section — and each dispatches for real", "since": "v15", "status": "active", - "revision": 13, + "revision": 14, "priority": "P1", "surface": "browser", "personas": [ @@ -1756,7 +1765,7 @@ "empty-locations semantics probe — an action with NO locations key renders on NO surface, and `locations: []` likewise (objectui#3142 collapsed the renderers onto one membership predicate, `actionRendersAt`; placement is a declaration, which is why recalc_selection has to name record_more)" ], "steps": [ - "boot the showcase isolated; sign in as admin; open the showcase_task list view", + "boot the showcase on its own port and DB; sign in as admin; open the showcase_task list view", "for each location variant, navigate to its surface (list header / row menu / a not-done task's Task Detail title bar / that title bar's ⋯ menu / the rows of the Tasks related list on a project's Project Detail Related tab, not the Tasks tab's line-items grid / the Task Detail quick-actions bar; fixtures.knownGaps names each record host), screenshot AFTER render settles, then read the rendered action buttons from the DOM", "record for every fixture action WHERE it rendered — building the full placement matrix (rendered locations vs declared locations)", "dispatch one action per location with a ref-targeted click: bulk_reassign from the list toolbar — select a row FIRST: with none selected it toasts 'This action runs on a single record — select a row first' and dispatches nothing — (screen-flow wizard opens), quick_view (modal opens), mark_done (script executes), log_time (a `form` action opens its FormView as a ROUTED PAGE, not a dialog: /_console/forms/showcase_task.form?recordId=…&recordObject=showcase_task — objectui core ActionRunner.ts, \"open a FormView as a routed page (/forms/:name)\"; QA run #24616), open_docs (url navigation), recalc_selection from the ⋯ menu (api POST); capture each network trace", @@ -1891,6 +1900,12 @@ "date": "2026-10-20", "change": "automated dropped and fixtures.knownGaps[0] rewritten — §2 theme \"pins that cover no full clause\" of wave anchor #24579 (18.0 sweep). Tier 1 run #24598 scored every clause not-run on list-row-action-cel.spec.ts and action-modal.spec.ts, both green at the sweep's console pin 4016370af3c9 and both adjacent (knownGaps[0] says what each asserts); QA run #24616 drove the item by hand after round 1's #24572 handed it over. The item is now Tier 2 (P1, no pin). No clause changed", "ref": "#24697" + }, + { + "revision": 14, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ], "enumSource": { @@ -1904,7 +1919,7 @@ "title": "All six validation-rule types enforce on the write path with their exact per-type error codes — and advisory severities flag without blocking", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "api", "personas": [ @@ -1932,7 +1947,7 @@ "severity — 'error' (the default, validation.zod.ts) blocks; 'warning'/'info' rules that MATCH do not block: the write lands and the message is logged server-side (rule-validator.ts 'only error blocks')" ], "steps": [ - "boot showcase isolated; sign in as admin", + "boot the showcase on its own port and DB; sign in as admin", "for each variant: POST/PATCH the violating payload over /api/v1/data/; capture status + error body", "for each variant: send the happy-path twin (same shape, satisfying values) and capture success", "after every rejection, re-read the row set to confirm nothing persisted", @@ -2024,6 +2039,12 @@ "date": "2026-10-20", "change": "acceptance[5], steps[6], negative[3], fixtures.requires[2] and knownGaps[1] added for #24782, which QA run #24779 found beside the clauses (round 3 of the 18.0 sweep, 580fd4ed): an unevaluable warning rule lets the write land, as negative[1] requires, but its log says the write was rejected, and no clause read the log of the unevaluable path. The clause stays red until #24782's fix lands. fixtures.requires[2] is round 3's recipe — a scratch object authored through PUT /api/v1/meta/object carries an unevaluable script rule — and closes round 2's fixture gap (QA run #24616: negative[0] blocked(fixture), 'an unevaluable CEL validation cannot be authored through any door'); negative[0] now names its scratch object. No existing clause changed", "ref": "#24802" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -2692,12 +2713,12 @@ "title": "Record discussion: an @mention comment reconciles optimistically, persists to sys_comment, interleaves with activity, pings the mentioned user's bell — and the feed filter shows exactly each mode's subset without dropping author-extended activity", "since": "v16", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "browser", "personas": [ "seeded admin (admin@objectos.ai / admin123) — the comment author", - "a second signed-up user — the @mention TARGET (sys_user rows come from sign-up, not seeds)" + "a second user, minted by the admin with POST /api/v1/auth/admin/create-user — the @mention TARGET. A stock showcase boot is invite_only, so a fresh POST /api/v1/auth/sign-up/email answers 403 SELF_REGISTRATION_CLOSED (steps[13]; RUNNER.md, 'A stock showcase boot refuses self-registration')" ], "fixtures": { "app": "showcase", @@ -2711,7 +2732,7 @@ "why": "unblocks acceptance clauses 6 and 7 — the create-side and re-thread-side FEEDS_DISABLED probes. Without the recipe there is no feeds-disabled parent anywhere in showcase (feeds is opt-out, default true), so both clauses score blocked(fixture) and FEEDS_DISABLED survives in this item's `negative` list alone: named, never driven. That is the exact gap #12118 records, one area over from the files-side twin #12116 repaired." }, "knownGaps": [ - "the @mention TARGET and the bell/notification clauses need a SECOND signed-up user — a fresh single-user boot cannot exercise them; record those clauses blocked(fixture) and run the optimistic-reconcile + sys_comment + activity-interleave clauses as the single author", + "the @mention TARGET and the bell/notification clauses need a SECOND user. A stock showcase boot is invite_only, so POST /api/v1/auth/sign-up/email answers 403 SELF_REGISTRATION_CLOSED: the admin mints the user with POST /api/v1/auth/admin/create-user, which answers mustChangePassword true, and the user calls POST /api/v1/auth/change-password and signs in again before it drives anything (steps[13]; RUNNER.md, 'A stock showcase boot refuses self-registration'). ⛔ Do not record those clauses blocked(fixture) on the 403: the refusal is the posture working, and the admin door mints the user on the same boot. The optimistic-reconcile, sys_comment and activity-interleave clauses need only the author", "CLOSED by the qa-feeds-disabled recipe (#12118) for the two FEEDS_DISABLED clauses: before it, no showcase object declared enable.feeds:false and there was no scratch recipe for this area, so the code appeared only in `negative`. Fall back to the pinned engine/API suites named in `automated` ONLY if the recipe cannot be replayed on the boot under test — and when you do, record WHICH of the two the verdict rests on, because the pins run against their own fixtures (cmt_nofeeds / lead_walled) and not against the showcase surface this item otherwise drives.", "acceptance[9]'s cross-organization leg runs live only on a walled boot (isolated or group), which the stock showcase does not provision. A run without one records that leg blocked(fixture) citing RUNNER.md's walled entry. Its pin (automated.ref, packages/plugins/plugin-audit/src/mention-recipient-gate.integration.test.ts) reaches the ENGINE layer, not HTTP: a real kernel, ObjectQL engine and SQLite driver with AuditPlugin and the real core grant resolver, while plugin-security's read layer is played by one middleware composing the posture's tenant clause and an owner-only rule, and messaging is played by an emit() recorder. Under RUNNER rule 6 the pin scores the clause partial at most, naming the HTTP layer and the real plugin-security as missing; the stock `single` leg is driven live" ] @@ -2865,6 +2886,12 @@ "date": "2026-10-20", "change": "acceptance[9], steps[13], negative[6], fixtures.requires[2] and knownGaps[2] added, and automated.ref names the recipient-gate pin and the layer it drives — the clause #24802 deferred under RUNNER rule 2 until #24785's fix merged. #24785 (found by the walled pass of round 3, QA run #24783) let a collab.mention notification reach a mentioned user who could not read the comment's record; its fix #24794 (merged e5b3de06a6) gates recipients on read access at the producer. The clause asserts that gate: read access decides, by id or email alike, and the author is never notified. Its live legs are the stock `single` sharing-excluded member and, on a walled boot, the cross-organization user; the pin reaches the engine layer only (RUNNER rule 6). acceptance[4] is unchanged: it scores who else's bell gains the row, this clause who among the mentioned may", "ref": "#24864" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "personas[1] and fixtures.knownGaps[0] mint the second user through the admin door. Anchor comment 6290946648 (round 16's carried finding, from #24864 / PR #24869): both said 'a second signed-up user', and knownGaps[0] recorded the mention and bell clauses blocked(fixture) without one. Read at base e4d604efd3: the showcase declares no auth.audience, so resolveAudience answers invite_only and an uninvited sign-up answers 403 SELF_REGISTRATION_CLOSED (packages/plugins/plugin-auth/src/audience-posture.ts), while runAdminCreateUser mints the user with mustChangePassword true by default (admin-user-endpoints.ts), as steps[13] (revision 5) already does. personas[1]'s 'sys_user rows come from sign-up, not seeds' goes too: the showcase seeds its demo personas (examples/app-showcase/src/security/demo-personas.ts), though none of them is this item's mention target. No clause changed", + "ref": "#25003" } ] }, @@ -3506,7 +3533,7 @@ "title": "Object lifecycle hooks fire on the write path with the right timing, gate, async and error semantics — driven over /api/v1/data/*, oracled by the record effect and the log line", "since": "v15", "status": "active", - "revision": 8, + "revision": 9, "priority": "P1", "surface": "mixed", "personas": [ @@ -3548,7 +3575,7 @@ "condition fails CLOSED (#4775) — SCRATCH: an unevaluable/uncompilable condition ABORTS the operation and is NOT softened by onError:'log' nor fire-and-forgotten (the gate runs OUTSIDE both); add a scratch hook with a broken condition (unit-pinned in hook-wrappers.ts)" ], "steps": [ - "boot showcase isolated WITH OS_LOG_LEVEL=info exported (or --log-level info): the audit oracle is a ctx.log.info line, and `os dev` defaults the kernel logger to warn (packages/cli/src/utils/log-level.ts DEFAULT_LOG_LEVEL), so on a default boot a correct product prints no audit line and every 'fires' clause reads as a no-fire; sign in as seeded admin; confirm the four hooks this item drives actually registered before asserting any no-fire — GET /api/v1/meta/hook (or the boot log; ⛔ not /meta/types/hook — /meta/types is the type listing and /meta/types/hook answers 404) lists showcase_normalize_task_title / showcase_stamp_inquiry_defaults / showcase_audit_task_completion / showcase_warn_over_budget (a hook that never bound fakes every no-fire — seed-data-thin)", + "boot the showcase on its own port and DB WITH OS_LOG_LEVEL=info exported (or --log-level info): the audit oracle is a ctx.log.info line, and `os dev` defaults the kernel logger to warn (packages/cli/src/utils/log-level.ts DEFAULT_LOG_LEVEL), so on a default boot a correct product prints no audit line and every 'fires' clause reads as a no-fire; sign in as seeded admin; confirm the four hooks this item drives actually registered before asserting any no-fire — GET /api/v1/meta/hook (or the boot log; ⛔ not /meta/types/hook — /meta/types is the type listing and /meta/types/hook answers 404) lists showcase_normalize_task_title / showcase_stamp_inquiry_defaults / showcase_audit_task_completion / showcase_warn_over_budget (a hook that never bound fakes every no-fire — seed-data-thin)", "stamp defaults (beforeInsert): POST /api/v1/data/showcase_inquiry {name:'os-qa-', email, message} — email and message are required, so a name-only body answers 400 VALIDATION_FAILED before any hook runs — OMITTING status AND source; re-read via GET and confirm source == 'web' — the hook is the SOLE producer of that value (the source field carries no default), so a correct read proves the hook fired; status == 'new' corroborates but is NOT hook-attributable alone (the 'new' status option is default:true)", "title trim (beforeInsert + beforeUpdate): POST /api/v1/data/showcase_task with title ' os-qa- ' (leading/trailing spaces) and valid required fields; re-read → title == 'os-qa-' (insert trim); then PATCH the same row's title to another padded value and re-read → trimmed again (update trim); ONE multi-event hook covered both", "completion audit — FIRES (transition): PATCH /api/v1/data/showcase_task/ {done:true} (previous done:false); after the HTTP response resolves, settle briefly (the audit is async fire-and-forget, it runs after the write returns), then read the server log for 'task completed: Build homepage'", @@ -3697,6 +3724,12 @@ "date": "2026-10-20", "change": "fixture roster only, no clause moved — the showcase gained three hooks, the paid-invoice line guards on showcase_invoice_line (beforeInsert, beforeUpdate, beforeDelete; each throws a user-facing refusal when the line's invoice is paid). Three stated facts they made false ('the five seeded showcase hooks', 'beforeDelete has NO seeded showcase hook', 'the one seeded hook that throws') now name them, and say why they do not close this item's beforeDelete variant or abort-rollback clause: this item does not drive them, and the clauses' verify steps name scratch hooks.", "ref": "#24684" + }, + { + "revision": 9, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -3705,7 +3738,7 @@ "title": "Per-type CONSTRAINT enforcement on the write path (not just widget render): length/range/option/reference/computed limits, and where the platform deliberately does NOT enforce", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "mixed", "personas": [ @@ -3749,7 +3782,7 @@ "summary" ], "steps": [ - "boot showcase isolated; sign in as the seeded admin; capture an API token for direct /api/v1/data/* POST/PATCH/GET", + "boot the showcase on its own port and DB; sign in as the seeded admin; capture an API token for direct /api/v1/data/* POST/PATCH/GET", "STRING length: POST /api/v1/data/showcase_field_zoo with name = a 201-char string (maxLength 200) and capture the refusal; then POST a 200-char name and capture success — confirm the long value was NOT silently truncated-and-stored", "NUMBER range: POST f_number = -1, then = 1001, then a non-number ('abc'); capture each refusal; POST f_number = 500 and re-read verbatim", "SCALE (gap probe): POST f_currency = 1234.567 (declared scale 2); GET the row and record whether the stored value is rounded (expected: stored 1234.567 verbatim — NOT enforced)", @@ -3848,6 +3881,12 @@ "date": "2026-10-20", "change": "acceptance[7].verify corrected — round 8's carried finding for wave anchor #24579 (18.0 sweep; round 8's report 6281776167, out_of_scope_findings[1], carried by the seat's ACCEPT 6281811723). It cited objectui e2e/live/summary-rollup.spec.ts for showcase_invoice.total, which that spec never reads: it rolls showcase_task up into showcase_project (byte-identical at the sweep's console pin 4016370af3c9 and at the shipped pin 12dc7b47c340). Tier 1 run #24598 scored acceptance[7] not-run (its summary half unpinned), and round 1's QA run #24572 scored the item pass on its API pins. The verify now says that no pin reads invoice.total. The spec was never in automated.ref, which names API pins only, so the ref is unchanged. Beside it, fixtures.knownGaps[0]'s line citation 'createColumn ~7354-7368', a tilde range no gate reads, became the sql-driver.ts#createColumn anchor that its own text already named. No clause changed", "ref": "#24752" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -3856,7 +3895,7 @@ "title": "Cascade clear-semantics and the server boundary: stale-child clear on parent change, the WRITTEN-value server gate, and where cascade scope is client-only", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": [ @@ -3875,7 +3914,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as the seeded admin; keep an API token for direct /api/v1/data/* writes", + "boot the showcase on its own port and DB; sign in as the seeded admin; keep an API token for direct /api/v1/data/* writes", "SELECT client clear: open a New Cascading Select (/_console/apps/showcase_app/showcase_cascade — the app name is the canonical segment; the console also accepts the package id com.example.showcase there); pick country=cn, province=zj; screenshot; switch country=us; screenshot; enumerate province options (expect {California,Texas}) and confirm zj CLEARED and not carried into the us submit", "SELECT server gate (written value): direct POST /api/v1/data/showcase_cascade {country:'cn', province:'ca'} → capture the refusal; then {country:'cn', province:'zj'} → capture success", "SELECT stale-child EDGE (gap probe): create {country:'cn', province:'zj'}; PATCH only {country:'us'} (omit province); GET the row and record whether province='zj' (now invalid for us) is still stored and the PATCH returned 200 — expected: accepted, stale value kept (server does not revalidate an unwritten child)", @@ -3980,6 +4019,12 @@ "date": "2026-10-20", "change": "automated.ref and acceptance[1].verify re-scored per RUNNER rule 6 — the sweep-wide rule-6 contradiction QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4) names at cascading-multilevel acceptance[1], round 15 of wave anchor #24579. acceptance[1] scores the direct POST's answer. Its verify and the ref (round 14 moved the objectql pin's scope into it) scored it, and acceptance[2], from rule-validator.option-visibility.test.ts, which calls evaluateValidationRules directly. By the rule (a pin scores only the layer it drives) that pin scores neither clause. Re-read at the console pin 616df6598b42 (blob 5f65228948b7 of cascading-options.spec.ts, unchanged since round 14), the ref's own objectui spec carries a second case, 'the server rejects an out-of-set option value, and accepts an in-set one', which POSTs to the live backend and asserts the out-of-set 400 VALIDATION_FAILED with invalid_option on province and the in-set 201. That is acceptance[1] at the HTTP door, so the ref and the verify cite it; no live run exists at that pin, so its verdict there is NOT MEASURED. acceptance[2] needs the run. No clause changed", "ref": "#24891" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -3988,7 +4033,7 @@ "title": "Secret/encrypted field: ciphertext at rest, masked on every read, no-op mask re-submit, and fail-CLOSED without an ICryptoProvider (ADR-0100)", "since": "v16", "status": "active", - "revision": 3, + "revision": 4, "priority": "P1", "surface": "mixed", "personas": [ @@ -4007,7 +4052,7 @@ ] }, "steps": [ - "boot showcase isolated on a provider-wired boot (`os serve`, LocalCryptoProvider); sign in as the seeded admin; keep an API token", + "boot the showcase on its own port and DB, on a provider-wired boot (`os serve`, LocalCryptoProvider); sign in as the seeded admin; keep an API token", "WRITE ciphertext: POST /api/v1/data/showcase_field_zoo {name:'sec-1', f_secret:'topsecret-value'}; GET the row over the API and confirm f_secret reads back as the SECRET_MASK, never 'topsecret-value'; where DB access exists, confirm the business-row column holds a 'secret:' ref and the plaintext lives only (encrypted) in sys_secret", "MASK on read for f_password too: POST f_password='p@ssw0rd!'; GET and confirm f_password reads back as the mask (generic password: plaintext-at-rest but masked-on-read)", "ECHOED-MASK no-op: PATCH the row's f_secret back as the SECRET_MASK sentinel; GET and confirm the stored secret is UNCHANGED (a form round-trip re-submitting the mask does not wipe it)", @@ -4088,6 +4133,12 @@ "date": "2026-10-20", "change": "automated.ref narrowed per clause; acceptance[3].verify and fixtures.knownGaps[1] rewritten — checklist-accuracy finding of QA run #24840 (18.0 sweep round 4, Tier 1 at 307ce4c4: the acceptance[2] and [4] guards are pinned at the engine only), round 15 of wave anchor #24579. secret-fields.test.ts drives a real ObjectQL engine over a stub driver, with no HTTP. By RUNNER rule 6 (a pin scores only the layer it drives) it scores none of this item's api clauses, so the ref names it engine-layer evidence for acceptance[2]–[4] and negative[1]–[3], and names the dogfood pin's own clauses (acceptance[0]'s API oracle, acceptance[1], negative[0]). The same rule makes acceptance[3]'s 'unit-pin verdict' (the #22017 row 22 disposition) a rule-6 contradiction, located here beyond #24840's list: no stock composition runs without a provider, so the clause is now recorded blocked(fixture) with the pin beside it. Read at base b1676441ba. No clause changed", "ref": "#24891" + }, + { + "revision": 4, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated on ' → 'boot the showcase on its own port and DB, on '. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4096,7 +4147,7 @@ "title": "fieldGroups[].visibleWhen section predicate: both spellings accepted, the parse normalizes to the envelope, retired visibleOn refused with the located prescription, derivation carries it verbatim — the console gates the section on the form AND the detail page (objectui#11630 fixed)", "since": "v17", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -4114,7 +4165,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as the seeded admin; keep an API token", + "boot the showcase on its own port and DB; sign in as the seeded admin; keep an API token", "author the scratch carrier: install a scratch package, then PUT /api/v1/meta/object/qa_group_gate?package= with a `kind` select (options basic|pro, basic default), two member fields declaring group: 'pro_details', fieldGroups: [{ key: 'pro_details', label: 'Pro details', visibleWhen: \"record.kind == 'pro'\" }], and highlightFields: ['kind'] — without highlightFields the derived detail page puts every field in its highlight strip and draws no body sections, so step 5's detail half has nothing to observe — the same call sequence the area's qa-feeds-disabled recipe grounds", "GET the object back over /api/v1/meta and read fieldGroups[0].visibleWhen — expect the body AS AUTHORED: a bare string comes back a bare string, because runtime-authored object metadata persists verbatim (ADR-0005 appendix (c)). Then PUT the envelope spelling and read it back the same way. The string → envelope normalization belongs to the schema parse and is proven by the batch20 pin (step 5), not by this read", "PUT a second draft spelling the key `visibleOn` and capture the refusal body", @@ -4196,6 +4247,12 @@ "date": "2026-10-16", "change": "steps[1] gains highlightFields (checklist-accuracy finding of QA run #23949, wave #23878): with none declared, the derived detail page for qa_group_gate puts every field in the highlight strip and draws no body sections, so the detail half of the console clause could not be observed; #23949 authored highlightFields ['kind']. The clauses are unchanged", "ref": "#24036" + }, + { + "revision": 5, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4204,7 +4261,7 @@ "title": "Field-level `unique` on the write path: colliding writes refused 409 UNIQUE_VIOLATION on both driver families, NULL-distinct, organization-scoped by default, whole-batch refusal, autonumber resync converges", "since": "v17", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "api", "personas": [ @@ -4224,7 +4281,7 @@ ] }, "steps": [ - "boot showcase isolated on the stock SQLite store (the default — do not try to select the in-memory engine, every boot door refuses it); author qa_unique via the package-install + PUT /api/v1/meta/object/qa_unique pattern", + "boot the showcase on its own port and DB, on the stock SQLite store (the default — do not try to select the in-memory engine, every boot door refuses it); author qa_unique via the package-install + PUT /api/v1/meta/object/qa_unique pattern", "POST /api/v1/data/qa_unique {name:'r1', sku:'A-1'} → 2xx; POST {name:'r2', sku:'A-1'} → capture the refusal; count rows", "POST two rows with sku absent → both land (NULL-distinct)", "PATCH r2 to sku:'A-1' → capture the refusal and re-read r2; PATCH r1 touching only `nick` → 2xx (no self-collision)", @@ -4326,6 +4383,12 @@ "date": "2026-10-20", "change": "acceptance[4].clause states what its oracle measures, and automated.ref's sentence quoting the old wording follows it. PR #24891's carried finding (round 15 left the clause text, 'the same 409 UNIQUE_VIOLATION over REST', under a test oracle). Read at base feb09db14a: the SQL pins drive SqlDriver over better-sqlite3 and map each refusal through a test-local createAsApi helper with isUniqueViolationError (@objectstack/types), the predicate envelopeUniqueViolation (objectql duplicate-record-error.ts) builds the 409 on; no pin drives a REST door. acceptance[0]–[3] drive the REST door live, so no knownGaps line is owed. The scope matrix the clause asserts is unchanged", "ref": "#24963" + }, + { + "revision": 7, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated on ' → 'boot the showcase on its own port and DB, on '. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4334,7 +4397,7 @@ "title": "deleteBehavior matrix: set_null clears / member-removes, master_detail cascades, restrict answers 409 DELETE_RESTRICTED with the cascade remedy, required-FK set_null escalates to restrict", "since": "v15", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -4363,7 +4426,7 @@ "NOT enumSource-pinnable: deleteBehavior is an inline z.enum(['set_null','cascade','restrict']) inside FieldSchema (field.zod.ts) with no named export — 3 members hand-pinned here; a fourth member will not trip a pin and must be caught by re-reading the enum" ], "steps": [ - "boot showcase isolated; admin API token; work on rows created BY THIS RUN, not the seeds (the cascade probes destroy their parents)", + "boot the showcase on its own port and DB; admin API token; work on rows created BY THIS RUN, not the seeds (the cascade probes destroy their parents)", "set_null scalar: create an account + a contact pointing at it; DELETE the account; re-read the contact", "multi-value: create two accounts + a field-zoo row with f_lookups = [both]; DELETE one account; re-read the row", "cascade: create an invoice with two lines; DELETE the invoice; list the lines", @@ -4440,6 +4503,12 @@ "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[2] cited line 184 of the showcase invoice.object.ts in the comma-range spelling (authored in 45397edf53 as the list 69-71 and 184). At that commit 184 was deleteBehavior 'cascade' on the invoice line's invoice master_detail, and 69-71 the invoice's required account lookup, the required-FK escalation arm fixtures.requires names; at this base both have moved. source[2] now anchors invoice.object.ts#InvoiceLine and #Invoice. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" + }, + { + "revision": 3, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4448,7 +4517,7 @@ "title": "Clone contract edges: enable.clone gate both ways (403 CLONE_DISABLED), overrides win but cannot forge readonly, bare-201 body per CloneDataResponseSchema, internal fields stripped from the response", "since": "v15", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -4467,7 +4536,7 @@ ] }, "steps": [ - "boot showcase isolated; admin token; author qa_noclone ({ enable: { clone: false } }, one text field) and create one row in it", + "boot the showcase on its own port and DB; admin token; author qa_noclone ({ enable: { clone: false } }, one text field) and create one row in it", "POST /api/v1/data/qa_noclone//clone → capture the refusal; then the control: clone a showcase_account row → 201", "clone WITH overrides: POST /api/v1/data/showcase_account//clone {overrides:{name:'os-qa-clone-override'}} — and include a readonly/system key in overrides (e.g. created_by, or a readonly field where one exists)", "diff the 201 body against CloneDataResponseSchema expectations: top-level {object, id, sourceId, record} with NO envelope wrapper", @@ -4530,6 +4599,12 @@ "date": "2026-08-30", "change": "initial — sweep gap (docs-claims angle): crud-roundtrip covers the clone happy path and RLS gate, but the CLONE_DISABLED refusal was only ever NAMED there, and the overrides-vs-readonly ordering, bare-201 body contract and #7823 response strip had no coverage anywhere. Also records that the docs' 'one-click cloning' has no console caller — API-only surface at the sweep pins", "ref": "#sweep-2026-08-30" + }, + { + "revision": 2, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4538,7 +4613,7 @@ "title": "Per-field trackHistory: a tracked edit lands the ADR-0052 §5b old→new timeline summary with display values, an untracked edit does not, and object-level enable.trackHistory gates the History tab and derived history op both ways", "since": "v15", "status": "active", - "revision": 5, + "revision": 6, "priority": "P1", "surface": "mixed", "personas": [ @@ -4555,7 +4630,7 @@ ] }, "steps": [ - "boot showcase isolated; sign in as admin; open a seeded account record", + "boot the showcase on its own port and DB; sign in as admin; open a seeded account record", "edit the TRACKED select `industry` (e.g. Technology → Finance); save; read the record's sys_activity rows over the API and open the timeline", "edit an UNTRACKED field (e.g. website) alone; save; re-read sys_activity for the record", "open the account's History tab; then open a seeded task's detail and look for a History tab", @@ -4643,6 +4718,12 @@ "date": "2026-10-20", "change": "automated.stale removed — round 14 of wave anchor #24579 (18.0 sweep), on the objectui pointer 6284006658. objectui#13132 (objectui 0b36ffa) repaired record-history-display.spec.ts, the one spec the stale record named, and the console pin moved to 616df6598b42 (PR #24829), which contains 0b36ffa. Its blob is 2c4d8a674f81 at the old pin 12dc7b47c340 and 7ebc45106cff at both 0b36ffa and 616df6598b42: it now targets showcase_account, the showcase's enable.trackHistory object, with no app-todo skip guard. objectui measured its whole live suite at 0b36ffa as 61 passed (11.0m) on the shipped showcase; no live run exists at 616df6598b42, so the verdict there is NOT MEASURED. Re-read at the new pin, it asserts the History tab's display values on showcase_account (crud-roundtrip's contract, per negative[3]) and opens no task detail, so it still claims no clause here, and the ref now says so. The two plugin-audit pins keep their scope. No clause changed", "ref": "#24857" + }, + { + "revision": 6, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, @@ -4949,7 +5030,7 @@ "title": "A script action whose server-side hook refuses the write with a user-facing sentence answers a 4xx, the console shows exactly one error toast carrying that sentence, and the record is left unchanged", "since": "v17.5", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "browser", "personas": [ @@ -4970,7 +5051,7 @@ ] }, "steps": [ - "re-read examples/app-showcase/src/data/hooks/index.ts and copy TASK_REOPEN_REFUSAL verbatim — the oracle string; boot the showcase isolated (RUNNER canonical boot line: own port, own file DB, --seed-admin) with the console built at .objectui-sha; sign in through the form as the seeded admin", + "re-read examples/app-showcase/src/data/hooks/index.ts and copy TASK_REOPEN_REFUSAL verbatim — the oracle string; boot the showcase with RUNNER's canonical checklist boot (own port, own file DB, --seed-admin) with the console built at .objectui-sha; sign in through the form as the seeded admin", "pick a finished task: GET /api/v1/data/showcase_task?where=... (or the list) and record its id, done, progress and modified timestamp — this is the before-image", "open the showcase_task list in the console (/_console/apps/showcase_app/showcase_task); screenshot after render settles; open the finished task's row menu ('Open menu' on its row) and confirm 'Reopen' is offered there", "start capturing the network log and, before clicking, count toast nodes: document.querySelectorAll('[data-sonner-toast]:not([data-removed=\"true\"])').length — the baseline (expected 0)", @@ -5033,6 +5114,12 @@ "date": "2026-10-03", "change": "initial — gives objectui#9151's re-observation a home: the status half was fixed (#17265 / PR #17679) and the console chain pinned correct with the transport stubbed (objectui#9252), but the symptom was never re-observed live on a current build. The showcase had no hook that refuses a write, so the fixture pair (showcase_guard_task_reopen + showcase_reopen_task) lands with it.", "ref": "#21596" + }, + { + "revision": 2, + "date": "2026-10-20", + "change": "steps[0] respelled: 'boot the showcase isolated (RUNNER canonical boot line: own port, own file DB, --seed-admin)' → 'boot the showcase with RUNNER's canonical checklist boot (own port, own file DB, --seed-admin)'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", + "ref": "#25003" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index c2a192472a..6567274494 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": 5, + "revision": 6, "priority": "P0", "surface": "mixed", "personas": ["admin (seeded admin@objectos.ai)", "end user (same account driving the published app)"], @@ -39,7 +39,7 @@ { "clause": "package create round-trips and the package switcher lists the new writable package without a page reload", "oracle": "network", - "verify": "the create POST succeeds and the switcher shows the package in the same session. objectui e2e/live/studio-object-designer.spec.ts test F1 pins it at the pinned objectui (.objectui-sha 616df6598b42, blob 58a7f1cbf024): it creates a package through the switcher's New package dialog by the testids studio-pkg-chip, package-form-id-input and package-form-submit, all declared in that tree's app-shell (StudioDesignSurface.tsx, PackageFormDialog.tsx), and asserts that the surface moved into the new package and that the switcher lists it by name with no reload. From this checkout it is not runnable (RUNNER, the objectui-ref fact): drive the clause by hand, or run F1 in an objectui checkout at the pinned revision and cite its output naming the revision. Note the new-package wizard's own state, objectui#11327", + "verify": "the create POST succeeds and the switcher shows the package in the same session. objectui e2e/live/studio-object-designer.spec.ts test F1 pins it at the pinned objectui (.objectui-sha cf153eb2e535, blob 58a7f1cbf024): it creates a package through the switcher's New package dialog by the testids studio-pkg-chip, package-form-id-input and package-form-submit, all declared in that tree's app-shell (StudioDesignSurface.tsx, PackageFormDialog.tsx), and asserts that the surface moved into the new package and that the switcher lists it by name with no reload. From this checkout it is not runnable (RUNNER, the objectui-ref fact): drive the clause by hand, or run F1 in an objectui checkout at the pinned revision and cite its output naming the revision. Note the new-package wizard's own state, objectui#11327", "evidence": "the create request/response + switcher screenshot" }, { @@ -89,7 +89,8 @@ { "revision": 2, "date": "2026-08-18", "change": "corrected the ?id= spelling in step 8 and clause 4, which said in an item whose step 2 creates the package com.example.repairs — inviting the package-id spelling that answers with an empty items array (#9334's class). Found by grounding rather than from a report: this item was not on the #9334 list. ⚠️ The card's other instruction for this item — drop any inherited 'Studio is not installed' assumption (#9453 CF-4) — needed NO edit: the item never carried one. It has no blocked marker, no such knownGap, and its steps drive the App Builder directly; the assumption lived in the wave briefing and the run records, not here", "ref": "#9386" }, { "revision": 3, "date": "2026-10-01", "change": "checklist-accuracy findings of QA run #21056, each grounded in objectui at the pin: the object is named repairs_repair_ticket (the dialog prefixes the package namespace), and every step and clause now uses that name; the New object dialog carries a pre-selected OWD choice; the zero-nav-items knownGap is superseded (the Create-app scaffold checkbox defaults on); steps[8]'s probe is a forged request because the console hides authoring on a read-only package; acceptance[0]'s cited objectui pin targets testids absent at the pin", "ref": "#21060" }, { "revision": 4, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[1], steps[2], steps[3] and steps[6] corrected from QA run #24571 (18.0 sweep, 5b14b7f3; carried by #24604 at ba4afcc2) and the P0 run #24556's continuation, re-read at the console pin 4016370af3c9. (1) knownGaps[0] said publish has no review step; Publish now opens the Pending changes sheet with per-item diff and a separate 'Publish all' (DraftChangesPanel.tsx). (2) A CJK-only display name derives no package id (PackageFormDialog.tsx toIdSegment). (3) There is no Save draft button for a field: the designer autosaves, and a picklist is held until it has an option ('Not saved yet: … needs at least one option'). (4) The publish toast names the count (objectui#11807). (5) The Studio house icon goes to the Studio landing, not the console Home. No clause changed. Wave anchor #24579 (round 3 named this item)", "ref": "#24671" }, - { "revision": 5, "date": "2026-10-20", "change": "acceptance[0].verify follows F1's testids. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said F1 targets package-id-input / package-name-input, which no source declares. Read at the console pin 616df6598b42 (objectui blob 58a7f1cbf024): F1 targets studio-pkg-chip, package-form-id-input and package-form-submit, each declared in that tree's app-shell source, and the two old testids appear nowhere in it. So F1 is citable when run at the pinned revision; the live verdict there is not measured here. No clause changed", "ref": "#24963" } + { "revision": 5, "date": "2026-10-20", "change": "acceptance[0].verify follows F1's testids. Round 14's out-of-scope finding (report 6289612504 on PR #24857, wave anchor #24579): it said F1 targets package-id-input / package-name-input, which no source declares. Read at the console pin 616df6598b42 (objectui blob 58a7f1cbf024): F1 targets studio-pkg-chip, package-form-id-input and package-form-submit, each declared in that tree's app-shell source, and the two old testids appear nowhere in it. So F1 is citable when run at the pinned revision; the live verdict there is not measured here. No clause changed", "ref": "#24963" }, + { "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" } ] }, { @@ -273,7 +274,7 @@ "title": "Record-page authoring round-trip: created bound to its object, seeded from the default layout, block-edited, published, rendered", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "mixed", "personas": ["admin (authors)", "end user (opens the record). On stock no non-admin persona reads showcase_contact (auditor.demo is refused 'You don't have access to Contact records', QA run #24604), so the viewer is the admin unless a grant is provisioned — e.g. a member bound to the `manager` position, whose showcase_manager set reads showcase_contact (examples/app-showcase/src/security/permission-sets.ts); record which"], @@ -304,7 +305,7 @@ { "clause": "the page editor exposes block authoring: the picker offers schema-backed block kinds (Card, Section, Record details)", "oracle": "dom", - "verify": "after a screenshot confirms the editor rendered, the Add-block dialog lists the three kinds. No pin in automated.ref covers this clause: objectui e2e/live/studio-editor.spec.ts asserts the same picker (Card, Section, Record details) on an editable copy of the shipped showcase_project_workspace page, and is not in the ref. Until objectui 0b36ffa that spec was stale: it looked for a 'Layout' section heading the page form no longer renders (the layout is edited on the canvas) and opened a shipped page the editor correctly locks", + "verify": "after a screenshot confirms the editor rendered, the Add-block dialog lists the three kinds. objectui e2e/live/studio-editor.spec.ts pins this clause carrier-neutrally (automated.ref): it asserts the same picker (Card, Section, Record details) on an editable copy of the shipped showcase_project_workspace page. Until objectui 0b36ffa that spec was stale: it looked for a 'Layout' section heading the page form no longer renders (the layout is edited on the canvas) and opened a shipped page the editor correctly locks", "evidence": "screenshot + dialog DOM read" }, { @@ -326,7 +327,7 @@ "traps": ["stale-console-bundle", "hydration-race", "automation-input"], "automated": { "kind": "e2e", - "ref": "objectui: e2e/live/studio-record-page.spec.ts — acceptance[0] ONLY, carrier-neutrally: it creates a record page bound to showcase_invoice (not this item's showcase_contact) and asserts the captured PUT body's type 'record', its object, and regions seeded with a record:highlights block. It covers no other clause. objectui e2e/live/studio-editor.spec.ts, which drives the Add-block picker acceptance[1] scores, is cited in source[1] and has never been in this ref" + "ref": "objectui: e2e/live/studio-record-page.spec.ts — acceptance[0], carrier-neutrally: it creates a record page bound to showcase_invoice (not this item's showcase_contact) and asserts the captured PUT body's type 'record', its object, and regions seeded with a record:highlights block. + objectui: e2e/live/studio-editor.spec.ts — acceptance[1], carrier-neutrally: 'Add block opens a block-type picker with configurable block kinds' opens the live page editor on an environment-authored copy of the shipped showcase_project_workspace (an app page, not this item's showcase_contact record page) and asserts that the dialog offers Card, Section and Record details. The picker offers one static catalogue whatever the page (AddBlockButton in objectui's PageBlockCanvas.tsx reads TYPES_BY_CATEGORY from block-types.ts), so the carrier does not change the answer. Both were read at the console pin cf153eb2e535 (blobs 3844a60ccd79 and 19f9cc131654). Neither covers acceptance[2] or [3]" }, "source": [ "objectui: e2e/live/studio-record-page.spec.ts (#1541, ADR-0034 — create bound + seeded regions, asserted off the PUT payload)", @@ -364,6 +365,12 @@ "date": "2026-10-20", "change": "automated.stale removed, automated.ref scoped to acceptance[0], and the stale notes in acceptance[0].verify, acceptance[1].verify, steps[3], source[0] and source[1] rewritten — round 14 of wave anchor #24579 (18.0 sweep), on the objectui pointer 6284006658. objectui#13132 (objectui 0b36ffa) repaired both specs the stale record named, and the console pin moved to 616df6598b42 (PR #24829), which contains 0b36ffa. studio-record-page.spec.ts: blob 181b2562c18d at the old pin 12dc7b47c340, 3844a60ccd79 at both 0b36ffa and 616df6598b42; it now sets Page type to record and picks the object from the Object combobox. studio-editor.spec.ts: blob e442b4c1a29e, then 19f9cc131654 at both; it now edits a copy of showcase_project_workspace and reads the layout on the canvas. objectui measured its whole live suite at 0b36ffa as 61 passed (11.0m) on the shipped showcase; no live run exists at 616df6598b42, so the verdict there is NOT MEASURED. Re-read at the new pin, studio-record-page.spec.ts asserts acceptance[0], the one clause the ref claims (carrier-neutrally, on showcase_invoice). studio-editor.spec.ts was never in the ref, so acceptance[1] stays driven by hand rather than widening the ref. No clause changed", "ref": "#24857" + }, + { + "revision": 8, + "date": "2026-10-20", + "change": "automated.ref widened to acceptance[1], and acceptance[1].verify says so. Round 14's widening candidate (PR #24857's acceptance notes, carrier #24579): objectui e2e/live/studio-editor.spec.ts, read at the console pin cf153eb2e535 (blob 19f9cc131654, the same at 616df6598b42). Its case 'Add block opens a block-type picker with configurable block kinds' drives the live page editor and asserts that the dialog offers Card, Section and Record details: acceptance[1]'s whole dom oracle, at its layer. It edits an environment-authored copy of showcase_project_workspace, an app page, while this item authors a showcase_contact record page, and the picker offers one static catalogue whatever the page (AddBlockButton reads TYPES_BY_CATEGORY, objectui block-types.ts), so the carrier does not change the answer. The spec's live verdict at the pin is not measured here. No clause changed", + "ref": "#25003" } ] }, @@ -1353,7 +1360,7 @@ "title": "The metadata governance sweep: GET /meta/diagnostics reports every STORED item failing spec validation with located errors, refuses outages and bad spellings loudly instead of answering '0 problems', and the Studio diagnostics page + directory tile badges paint the same numbers", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P2", "surface": "mixed", "personas": ["admin (the governance surface is Studio's)"], @@ -1370,7 +1377,7 @@ ] }, "steps": [ - "boot showcase isolated (dogfood §0); sign in as the dev admin", + "boot the showcase on its own port and DB (dogfood §0); sign in as the dev admin", "GET /api/v1/meta/diagnostics on the live REST server and record the BARE body — { entries, total, scannedTypes, scannedItems, stats } with NO { success, data } envelope on this surface (rest-route-ledger.ts); ⛔ the route is REST-server-only, so a simulated dispatch is not an oracle here", "stats parity probe: for two or three types (object, view, flow), GET /api/v1/meta/ and compare stats[].count against the list's item count and stats[].packages against the sorted distinct _packageId set of those items", "package scoping: GET /api/v1/meta/diagnostics?package=com.example.showcase and record how stats narrows; then open Studio's metadata directory (/apps//metadata?package=com.example.showcase) and compare the tile counts against the scoped stats (DirectoryPage feeds its tiles from the package-scoped sweep)", @@ -1457,7 +1464,8 @@ { "revision": 3, "date": "2026-10-13", "change": "acceptance[1].verify compares stats.locked against the served envelope lock (stale spec, #23166 A1; parked on #23121, carried by #23169). getMetaDiagnostics counts an item as locked when its read envelope reports a lock other than 'none' — the same derivation getMetaItem publishes (#21694) — not when its declared _lock is set. QA run #23166 compared against the envelope lock and passed. Read from source at 6d7f4df3c (protocol.ts getMetaDiagnostics docblock and loop). The A6 row of the same run, the unplantable invalid stored row, is already recorded in full in knownGaps[0] and needs no edit", "ref": "#23169" }, { "revision": 4, "date": "2026-10-16", "change": "acceptance[1].verify names the read that carries the envelope lock (checklist-accuracy finding 2 of QA run #23942, wave #23878). It told the runner to compare stats.locked against the lock GET /api/v1/meta/TYPE/NAME publishes, but that default read is the cached arm and carries no `lock` by design; the envelope lock is on GET /api/v1/meta/TYPE/NAME/layers, which is where #23942 read it (object 107, view 0, flow 32 locked, matching stats). The comparison itself is unchanged", "ref": "#24036" }, { "revision": 5, "date": "2026-10-19", "change": "fixtures.knownGaps[0], steps[6], steps[8] and acceptance[6] (clause, verify) corrected from QA run #24604 (18.0 sweep round 2, ba4afcc2), the round-2 §3 note that knownGaps[0] is wrong. The invalid stored row is plantable in-run on an isolated scratch file DB: save a valid view through the door with ?package=, rewrite its sys_metadata.metadata, and the next sweep lists it — the run scored acceptance[6] pass that way, deep link and badge included. The directory always repairs ?package= to a project package, so the red button is judged per package scope; rootless union-parent entries may ride beside the located leaf. Wave anchor #24579 round 2 §3 (6272314386)", "ref": "#24671" }, - { "revision": 6, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[5] cited lines 242-254 of objectui DirectoryPage.tsx and line 982 of app-shell's console AppContent.tsx in the comma-range spelling (authored in 45397edf53 as the lists 133-158 and 242-254, and 882 and 982). At the objectui pin that commit shipped, 9602dc820450, 242-254 was the diagnostics link that renders only when diagSummary.total is above 0, 133-158 the package-scoped useGlobalDiagnostics sweep, and 982 the nested metadata route block that holds _diagnostics; at the shipped pin 12dc7b47c340 all have moved. source[5] now names the MetadataDirectoryPage component's sweep and link, and the AppContent component's metadata/_diagnostics route. objectui is read at the shipped pin 12dc7b47c340 and cited in prose, because no objectui anchor resolves in this gate. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" } + { "revision": 6, "date": "2026-10-20", "change": "line citations re-pointed — the comma-range spelling that round 7 of wave anchor #24579 named (18.0 sweep; round 7's report 6281433885, out_of_scope_findings[1]). source[5] cited lines 242-254 of objectui DirectoryPage.tsx and line 982 of app-shell's console AppContent.tsx in the comma-range spelling (authored in 45397edf53 as the lists 133-158 and 242-254, and 882 and 982). At the objectui pin that commit shipped, 9602dc820450, 242-254 was the diagnostics link that renders only when diagSummary.total is above 0, 133-158 the package-scoped useGlobalDiagnostics sweep, and 982 the nested metadata route block that holds _diagnostics; at the shipped pin 12dc7b47c340 all have moved. source[5] now names the MetadataDirectoryPage component's sweep and link, and the AppContent component's metadata/_diagnostics route. objectui is read at the shipped pin 12dc7b47c340 and cited in prose, because no objectui anchor resolves in this gate. The spelling is what #13786's strip left when it removed only the first range of a ':A,B' list, and the line-citation limb reads only the colon spellings, so no gate caught it. Where the surrounding words still describe the range the strip removed, the new citation names that code too. No clause's assertion changed", "ref": "#24752" }, + { "revision": 7, "date": "2026-10-20", "change": "steps[0] respelled: 'boot showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1465,7 +1473,7 @@ "title": "A doc on the app menu follows the reader's audience: an admin authors a doc and puts it (or its book) on an app menu; a member who may read it sees it and opens it in its book; a member outside its audience does not see the entry and gets a refusal — never \"not found\", never a blank page — on the direct route", "since": "v17.5", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "mixed", "personas": [ @@ -1489,7 +1497,7 @@ ] }, "steps": [ - "boot the showcase isolated (dogfood §0) with the console named above; as admin, verify the stock corpus first (knownGaps), then author the doc in the writable base package over the metadata API (PUT /api/v1/meta/doc/DOC?package=PKG, then publish) — knownGaps[2]'s recipe, so the doc shares the books' package, rather than through the Studio doc editor; author the remaining fixture books/docs and the permission set the same way, and assign SET to the reader", + "boot the showcase on its own port and DB (dogfood §0) with the console named above; as admin, verify the stock corpus first (knownGaps), then author the doc in the writable base package over the metadata API (PUT /api/v1/meta/doc/DOC?package=PKG, then publish) — knownGaps[2]'s recipe, so the doc shares the books' package, rather than through the Studio doc editor; author the remaining fixture books/docs and the permission set the same way, and assign SET to the reader", "as admin, add the three doc entries to the app navigation (Studio app designer, or PUT /api/v1/meta/app/APP) over the gated and the open corpus", "GET /api/v1/meta/app?id=APP as each member in that member's own session (prove the identity with GET /api/v1/auth/get-session first); record which doc entries each receives", "as each member, open the app and capture the rendered menu after a screenshot confirms it settled; diff the rendered doc entries against that member's /meta/app answer (UI mirror)", @@ -1546,7 +1554,8 @@ { "revision": 1, "date": "2026-09-30", "change": "new — the 清单项待写 row of docs/NORTH-STAR.md beside platform-core.docs-audience-gate and platform-core.docs-portal-render; drafted with objectui#10188 and corrected by the at-tier review of objectui PR 11258 (the UI-mirror clause scoped to entries naming an installed doc or book; no signed-out persona — /docs sits behind ProtectedRoute)", "ref": "#20939" }, { "revision": 2, "date": "2026-10-13", "change": "knownGaps[2]'s fixture recipe re-pointed (knownGap stale, #23166 A0; parked on #23121, carried by #23169). The Studio doc editor no longer answers the read-only showcase package with a 422: it saves an env-wide draft and offers no package picker, so the recipe's reason for a writable base changed, not its need. The books and docs must share one package, so the recipe drives the writable base over the metadata API with ?package=. QA run #23166 did exactly that and passed", "ref": "#23169" }, { "revision": 3, "date": "2026-10-16", "change": "knownGaps[1] closed (checklist-accuracy finding 9 of QA run #23942, wave #23878): the .objectui-sha pin has been past the fork window since daeb77fc84dd — the console carries the refusal page and the menu pruning — so the knownGap no longer asks for a console built elsewhere. Wording only: no step, request or oracle changed", "ref": "#24036" }, - { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#24671" } + { "revision": 4, "date": "2026-10-19", "change": "steps[0] aligned with fixtures.knownGaps[2] from QA run #24571 (18.0 sweep, 5b14b7f3): the step said to create the doc in the Studio doc editor while knownGaps[2] prescribes authoring every book, doc and the app over the metadata API with ?package= so they share one package; the run drove the API recipe. No clause changed. Wave anchor #24579 §3", "ref": "#24671" }, + { "revision": 5, "date": "2026-10-20", "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, { @@ -1554,7 +1563,7 @@ "title": "A picklist authored in a package is used by two objects' select fields from the designer: both forms offer its options, the bound field refuses inline options, and a GET → edit → PUT of an object with a bound field saves", "since": "v17.7", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "mixed", "personas": [ @@ -1569,7 +1578,7 @@ ] }, "steps": [ - "boot the showcase isolated with the console under test; sign in as the seeded admin", + "boot the showcase on its own port and DB with the console under test; sign in as the seeded admin", "GET /api/v1/meta/picklist and GET /api/v1/meta/picklist/NAME — the list is served with name, label, its own options and _packageId", "GET /api/v1/meta/object/OBJ_A/layers for the object whose field is already bound — record the bound field as served (picklist + resolved options, the extension's option included)", "metadata admin (Setup → Metadata → Object → OBJ_A): select an UNRELATED field, rename its label, let the autosave run; record the PUT body and status", @@ -1643,7 +1652,8 @@ "the fixture's spelling: packages/spec/src/data/picklist.zod.ts#definePicklist, the defineStack keys packages/spec/src/stack.zod.ts#picklists and packages/spec/src/stack.zod.ts#picklistExtensions; an extension in the list's own package merges (packages/objectql/src/engine-picklist.test.ts, the nested-plugin case)" ], "history": [ - { "revision": 1, "date": "2026-10-06", "change": "new — drafted with objectui#10202 against the measured run of its PR; filed by the seat. Authored from that draft with these changes: since v17.7, because 17.6.0 shipped before objectui PR 11669 merged; the picker clause split in two, one oracle each; the picklist fixture spelled with its export, its stack keys and the reload it needs; the console fixture naming the merge it must carry; the spec citations written as anchors the gate resolves", "ref": "#21957" } + { "revision": 1, "date": "2026-10-06", "change": "new — drafted with objectui#10202 against the measured run of its PR; filed by the seat. Authored from that draft with these changes: since v17.7, because 17.6.0 shipped before objectui PR 11669 merged; the picker clause split in two, one oracle each; the picklist fixture spelled with its export, its stack keys and the reload it needs; the console fixture naming the merge it must carry; the spec citations written as anchors the gate resolves", "ref": "#21957" }, + { "revision": 2, "date": "2026-10-20", "change": "steps[0] respelled: 'boot the showcase isolated' → 'boot the showcase on its own port and DB'. Round 12's out-of-scope finding (landing record 6287983829 on wave anchor #24579): the phrase 'showcase isolated' named the dogfood §0 own-port boot, and a runner read it as the `isolated` tenancy posture (OS_TENANCY_POSTURE=isolated). The new spelling names that boot by what it is, in RUNNER.md's own words for it (the trap table's counter: own port/DB, skill §0), and names no posture. Read at base e4d604efd3. No clause changed", "ref": "#25003" } ] }, {