From 49a03e92c902327b2c8044febf37029724a9dcbe Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 16:40:35 +0000 Subject: [PATCH] =?UTF-8?q?docs(qa):=20wave-3=20checklist=20accuracy=20sli?= =?UTF-8?q?ce=20S10=20=E2=80=94=20the=2018=20W3-RT=20findings=20at=206e11a?= =?UTF-8?q?929?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-verified each W3-RT (QA run #28656) checklist-accuracy row against source before editing; ten items across five area files, each with revision +1 and one history row. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2gLghEqeDKTbDTC7Zd --- .../platform-checklist/areas/api-backend.json | 9 ++- .../platform-checklist/areas/approvals.json | 9 ++- .../areas/integration-system.json | 12 ++- .../areas/records-forms.json | 12 ++- .../areas/studio-authoring.json | 79 +++++++++++++------ 5 files changed, 82 insertions(+), 39 deletions(-) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 6f53c3ad174..4d20716b4d8 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -2165,7 +2165,7 @@ "title": "CEL formula stdlib: all 30 bare-callable stdlib functions give known answers against the BUILT package — calendar-day timezone contract, addMonths month-end clamp, floor(-1.2)==-2, null-dropping joins, mixed int/double arithmetic, isoDate/isoDatetime text — and a formula calling an unregistered function is REFUSED at the authoring door (422), never stored to read null", "since": "v16", "status": "active", - "revision": 6, + "revision": 7, "priority": "P1", "surface": "mixed", "personas": [ @@ -2189,7 +2189,7 @@ "calendar-day timezone contract (ADR-0053 Phase 2 D1): pin now to an instant whose calendar day DIFFERS between UTC and the reference zone (e.g. 2026-08-30T22:00:00Z with timezone Asia/Shanghai — locally already 08-31) and assert today() == 2026-08-31T00:00:00Z (the reference-tz calendar day expressed as UTC MIDNIGHT — never an instant carrying wall-clock time), daysFromNow/daysAgo offset from that same day; an unknown zone falls back to the UTC calendar day, never throws", "mixed-arithmetic overloads (registerNumericCoercions, stdlib.ts): a double-typed record field divided/multiplied by an int literal (record.amount / 100) evaluates to the promoted double instead of faulting 'no such overload' to silent null (#1928), while pure int 7/2 == 3 keeps integer-division semantics untouched", "live-door parity: GET a showcase_project row and confirm budget_remaining materializes (read-path evaluation); author a scratch object through the metadata channel, declaring its sharingModel (fixtures.requires[1]), whose formula field composes two stdlib calls (e.g. joinNonEmpty + upper) and read back the known answer — the stdlib reaching the LIVE read path, not just the harness", - "the unregistered-function probe: on a scratch object in the scratch package (fixtures.requires[1]), PUT /api/v1/meta/object/qaformula_NAME?package=com.objectstack.qa.formula at the ACTIVE door (no ?mode=draft) with a formula field calling an UNregistered function (e.g. bad_f = sqrt(4)), its body declaring a sharingModel (fixtures.requires[1]) — without one the 422 is security-owd-unset's refusal, not the expression's. Capture the 422 INVALID_METADATA and its expression-invalid issue, then GET the object and confirm the field was not stored. A ?mode=draft save of the same body is admitted (draft saves defer the gate to promotion, #4463 D1), and its draft→active promotion is refused with the same expression-invalid finding. Promoted through POST /api/v1/packages/PACKAGE/publish-drafts, that refusal is an entry with outcome 'refused' inside the batch envelope, not the request's own 422 (packages/metadata-protocol/src/protocol.ts, the publish-drafts outcome union). What the door cannot see is unchanged underneath: a formula that compiles but faults at EVALUATE time, or a row stored before the gate, still reads null (applyFormulaPlan's `r.ok ? … : null`, packages/objectql/src/engine.ts), now with one warn line per (object, field) (#22031)" + "the unregistered-function probe: on a scratch object in the scratch package (fixtures.requires[1]), PUT /api/v1/meta/object/qaformula_NAME?package=com.objectstack.qa.formula at the ACTIVE door (no ?mode=draft) with a formula field calling an UNregistered function (e.g. bad_f = sqrt(4)), its body declaring a sharingModel (fixtures.requires[1]) — without one the 422 is security-owd-unset's refusal, not the expression's. Capture the 422 INVALID_METADATA and its expression-invalid issue, then GET the object and confirm the field was not stored. A ?mode=draft save of the same body is admitted (draft saves defer the gate to promotion, #4463 D1), and its draft→active promotion is refused with the same expression-invalid finding at BOTH promotion doors: the single-item POST /api/v1/meta/object/qaformula_NAME/publish answers 422 INVALID_METADATA itself (publishMetaItem), while through POST /api/v1/packages/PACKAGE/publish-drafts that refusal is an entry with outcome 'refused' inside the batch envelope, not the request's own 422 (packages/metadata-protocol/src/protocol.ts, the publish-drafts outcome union). What the door cannot see is unchanged underneath: a formula that compiles but faults at EVALUATE time, or a row stored before the gate, still reads null (applyFormulaPlan's `r.ok ? … : null`, packages/objectql/src/engine.ts), now with one warn line per (object, field) (#22031)" ], "acceptance": [ { @@ -2223,7 +2223,7 @@ "evidence": "the responses vs the expectation" }, { - "clause": "a formula calling an UNREGISTERED function is refused at the authoring door, before it is stored: an active-state object save (PUT /api/v1/meta/object/NAME, no ?mode=draft) whose formula field calls sqrt(...) answers 422 INVALID_METADATA carrying the build's located finding (rule expression-invalid, at that field's expression, naming `sqrt` as a NAME fault), and nothing lands; a ?mode=draft save is admitted (#4463 D1) and its promotion is refused with the same finding (through publish-drafts, as an outcome 'refused' entry of the batch envelope, steps[6]). This is the loud refusal the earlier EXPECTED-FAIL wording waited for (#22031, fixing #22019)", + "clause": "a formula calling an UNREGISTERED function is refused at the authoring door, before it is stored: an active-state object save (PUT /api/v1/meta/object/NAME, no ?mode=draft) whose formula field calls sqrt(...) answers 422 INVALID_METADATA carrying the build's located finding (rule expression-invalid, at that field's expression, naming `sqrt` as a NAME fault), and nothing lands; a ?mode=draft save is admitted (#4463 D1) and its promotion is refused with the same finding at BOTH promotion doors (the single-item POST /api/v1/meta/object/NAME/publish answers 422 INVALID_METADATA itself, and publish-drafts reports an outcome 'refused' entry of the batch envelope, steps[6]). This is the loud refusal the earlier EXPECTED-FAIL wording waited for (#22031, fixing #22019)", "oracle": "api", "verify": "the PUT answers 422 with code INVALID_METADATA and an issue whose rule is expression-invalid, located at the field's expression and naming the function; a follow-up GET /api/v1/meta/object/NAME shows the field was not stored. Pinned through the real saveMetaItem / publishMetaItem by packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the 'runtime authoring gate on OBJECT writes — the formula field verdict (#22019)' block: sqrt(record.amount) refused 422 on an active save and on a draft's promotion, nothing lands, floor(record.amount) still saves), and at the rule layer by packages/lint/src/runtime-gate.object-formula-writes.test.ts. A row stored BEFORE the gate still reads null on the read path, now with one warn line per (object, field) (packages/objectql/src/engine-formula-fault-log.test.ts). That is the residual, not this clause", "evidence": "the 422 response (code + the expression-invalid issue) + the post-refusal metadata read" @@ -2281,7 +2281,8 @@ "change": "steps[6] and acceptance[5] say how the draft's promotion is refused. Checklist-accuracy finding of the 18.2.0 wave 2 (QA run #26456, release-18.2.0-w2-manual-browser, at 756220c4; wave anchor #25807, slice A): the run promoted through publish-drafts and met the refusal as an outcome:'refused' entry inside the batch envelope, while 'refused the same way' read as a second HTTP 422. Re-read at base e4ba846f (metadata-protocol protocol.ts: outcome 'published' | 'refused' | 'nothing_to_publish'). The clause's assertion (refused, nothing lands) is unchanged", "ref": "#25807" }, - { "revision": 6, "date": "2026-10-31", "change": "The function count is 30, not 29, and the scratch-object legs name their writable package. Re-point of the red row QA run #27441 (18.3.0 sweep, wave 3, manual set, at c4c515ad) parked on sweep anchor #27377 §1, plus its fixture finding. (1) The registry gained label(record.f), a macro bindOptionLabel registers beside the permission predicates, and the docs table lists it, so the title, steps[0], steps[2], knownGaps[0], acceptance[0] (clause and evidence) and source[0]/[1] count 30, steps[2] adds the label() known-answer rows, and the receiver-only names outside the count are `can` and `hasCapability` (registerPermissionPredicate is now bindPermissionPredicate). (2) fixtures.requires[1] and steps[6]: a runtime object save needs a writable package and its namespace prefix as well as a sharingModel. Re-read at base 488b3ed0: packages/formula/src/stdlib.ts registerBoundStdLib and bindOptionLabel, packages/formula/src/validate.ts CEL_STDLIB_FUNCTIONS docblock ('30 added by registerStdLib'), content/docs/data-modeling/formulas.mdx, packages/metadata-protocol/src/protocol-authoring-gate.ts (NAMESPACE_PREFIX). The clause still fails on any count drift in either direction", "ref": "#27441" } + { "revision": 6, "date": "2026-10-31", "change": "The function count is 30, not 29, and the scratch-object legs name their writable package. Re-point of the red row QA run #27441 (18.3.0 sweep, wave 3, manual set, at c4c515ad) parked on sweep anchor #27377 §1, plus its fixture finding. (1) The registry gained label(record.f), a macro bindOptionLabel registers beside the permission predicates, and the docs table lists it, so the title, steps[0], steps[2], knownGaps[0], acceptance[0] (clause and evidence) and source[0]/[1] count 30, steps[2] adds the label() known-answer rows, and the receiver-only names outside the count are `can` and `hasCapability` (registerPermissionPredicate is now bindPermissionPredicate). (2) fixtures.requires[1] and steps[6]: a runtime object save needs a writable package and its namespace prefix as well as a sharingModel. Re-read at base 488b3ed0: packages/formula/src/stdlib.ts registerBoundStdLib and bindOptionLabel, packages/formula/src/validate.ts CEL_STDLIB_FUNCTIONS docblock ('30 added by registerStdLib'), content/docs/data-modeling/formulas.mdx, packages/metadata-protocol/src/protocol-authoring-gate.ts (NAMESPACE_PREFIX). The clause still fails on any count drift in either direction", "ref": "#27441" }, + { "revision": 7, "date": "2026-11-01", "change": "QA run #28656 (W3-RT) finding: acceptance[5] and steps[6] named only publish-drafts as the draft's promotion door. The single-item POST /api/v1/meta/object/NAME/publish refuses too, 422 INVALID_METADATA with the same expression-invalid finding: #28656 read it live at 6e11a929, publishMetaItem's refusal is pinned in packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts (the formula field verdict block, on a draft's promotion), and the route is meta.publishItem in packages/rest/src/rest-route-ledger.ts. Both now name the two promotion doors. The clause asks for both refusals, so it is stronger, never weaker. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", "ref": "#28656" } ] }, { diff --git a/docs/qa/platform-checklist/areas/approvals.json b/docs/qa/platform-checklist/areas/approvals.json index b73e8e7b19b..7e368738a80 100644 --- a/docs/qa/platform-checklist/areas/approvals.json +++ b/docs/qa/platform-checklist/areas/approvals.json @@ -780,7 +780,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": 5, + "revision": 6, "priority": "P2", "surface": "mixed", "personas": ["dev admin (org-membership owner — the stage-1 approver of the dynamic-approval demo)"], @@ -791,7 +791,7 @@ "trigger: retitle a showcase_announcement (an otherwise approval-free object, so this demo never collides with the expense/invoice/project approval dedupe)" ], "knownGaps": [ - "CLOSED by #7639: a paused run's variables ARE readable now. Both suspend sites take one snapshot and hand it to the continuation and to the `paused` log entry that run-detail serves — the initial suspend in packages/services/service-automation/src/engine.ts#recordLog's `status: 'paused'` call, and the re-suspend in packages/services/service-automation/src/run-resume.ts, which is where this flow re-pauses at stage 2 — and GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID publishes it (measured live in #27134 on a stage-1-paused run: status paused, `variables` present). An accepted decision's outputs land in that map as FLAT `.` keys (packages/services/service-automation/src/engine.ts#applyResumeSignal), so the stage-2-paused run carries the lead's pick as `variables['lead_review.next_reviewers']`. That key on the stage-2 snapshot is read from source at base 74133d98, not yet driven live: #27134 read a stage-1 snapshot. So acceptance[3] now reads the snapshot directly AND keeps the resolution leg (stage-2 pending_approvers); a snapshot without the key, or one that disagrees with the decision POST body, is a FAIL of that clause, never a PARTIAL. Revision 2's reason for the functional-only oracle (#7517 scored PARTIAL on an unreadable snapshot) no longer holds" + "CLOSED by #7639: a paused run's variables ARE readable now. Both suspend sites take one snapshot and hand it to the continuation and to the `paused` log entry that run-detail serves — the initial suspend in packages/services/service-automation/src/engine.ts#recordLog's `status: 'paused'` call, and the re-suspend in packages/services/service-automation/src/run-resume.ts, which is where this flow re-pauses at stage 2 — and GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID publishes it (measured live in #27134 on a stage-1-paused run: status paused, `variables` present). An accepted decision's outputs land in that map as FLAT `.` keys (packages/services/service-automation/src/engine.ts#applyResumeSignal), so the stage-2-paused run carries the lead's pick as `variables['lead_review.next_reviewers']`. That key on the stage-2 snapshot was read from source at base 74133d98 and has now been read live: QA run #28656 (W3-RT, at 6e11a929) read variables['lead_review.next_reviewers'] on the stage-2-paused run, equal to the picks, in order (#27134 had read only a stage-1 snapshot). So acceptance[3] now reads the snapshot directly AND keeps the resolution leg (stage-2 pending_approvers); a snapshot without the key, or one that disagrees with the decision POST body, is a FAIL of that clause, never a PARTIAL. Revision 2's reason for the functional-only oracle (#7517 scored PARTIAL on an unreadable snapshot) no longer holds" ] }, "steps": [ @@ -799,7 +799,7 @@ "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's page (its inbox row navigates to /apps/APP/sys_approval_request/record/ID) and click Approve in its decision panel; screenshot the decision dialog's next_reviewers control — it must be a sys_user MULTI-SELECT picker, not a free-text box", - "gate probe: attempt to approve WITHOUT filling next_reviewers; record the refusal", + "gate probe: attempt to approve WITHOUT filling next_reviewers; record the refusal and the door that answered it. The decision outputs travel under the wire key `outputs`: on the approvals route POST /api/v1/approvals/requests/ID/approve the body is {\"outputs\":{\"next_reviewers\":[…]}} (packages/rest/src/approvals-routes.ts reads body.outputs), and on the action door POST /api/v1/actions/sys_approval_request/approval_approve they are the params `outputs.next_reviewers` (packages/runtime/src/action-execution.ts folds the outputs.KEY family into the body's outputs). An output under any other key is no output at all, so its required-output refusal (VALIDATION_FAILED naming next_reviewers, packages/plugins/plugin-approvals/src/approval-service.ts) says nothing about the gate: spell the key right in the control of steps[5]", "approve WITH next_reviewers=[the admin] via the dialog; capture the decision POST body", "GET the requests list again — the stage-2 co-sign request must now exist; read its pending_approvers", "cardinality/order probe (the vars oracle): re-run the whole chain on a SECOND announcement picking TWO reviewers. For EACH chain, once stage 1 is approved and the run has re-paused at stage 2, read GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID (RUNID = the stage-2 request's flow_run_id) and record its status and variables['lead_review.next_reviewers'], then read the stage-2 request's pending_approvers — both must equal the picked ids: 1 pick yields 1, 2 picks yield 2, in the picked order (see knownGaps for where the snapshot comes from)", @@ -853,7 +853,8 @@ { "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" }, { "revision": 4, "date": "2026-10-28", "change": "steps[3] opens the stage-1 decision on the request's served record page, not the retired request drawer: the inbox row navigates to /apps/APP/sys_approval_request/record/ID (ui/apps/console/src/pages/system/ApprovalsInboxPage.tsx requestPagePath; objectui#12484 deleted the inbox's side sheet (ruling C on objectui#12438); it came into this tree with the ui/ import, PR #25825), and that page's decision panel draws Approve through DeclaredActionsBar, whose dialog takes the node's typed outputs from decisionOutputParams (source[4], added). Read from source at base f2fdac5d. Wave anchor #25807, slice C; lead from slice B's report 6457925443. No clause changed", "ref": "#25807" }, - { "revision": 5, "date": "2026-10-28", "change": "knownGaps[0], steps[7], acceptance[3] and source[3]: a paused run's variables are readable now, so the vars clause reads them. Since #7639 both suspend sites (engine.ts recordLog's paused call, and run-resume.ts's re-suspend, where this flow re-pauses at stage 2) hand one snapshot to the continuation and to the paused log entry run-detail serves, and QA run #27134 measured it live at 07d18e692: GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID on a paused run answered status paused with variables present. Decision outputs land as flat . variables (engine.ts applyResumeSignal), so acceptance[3] gains a snapshot leg: the stage-2-paused run's variables['lead_review.next_reviewers'] equals the submitted pick, ids and order, on both chains; the resolution leg (stage-2 pending_approvers 1 → 1, 2 → 2, same order) is kept, and both must hold. The clause is stronger, never weaker: revision 2's '⛔ Do NOT prescribe reading the run's variables snapshot' rested on a paused run carrying no variables key, which no longer holds. The knownGap is rewritten as CLOSED-by-#7639 rather than deleted. The stage-2 key is read from source at base 74133d98, not yet driven live (#27134 read a stage-1 snapshot), so a prior verdict on acceptance[3] does not carry. Wave anchor #25807, wave-2 accuracy slice D; seat note 6464451167", "ref": "#27134" } + { "revision": 5, "date": "2026-10-28", "change": "knownGaps[0], steps[7], acceptance[3] and source[3]: a paused run's variables are readable now, so the vars clause reads them. Since #7639 both suspend sites (engine.ts recordLog's paused call, and run-resume.ts's re-suspend, where this flow re-pauses at stage 2) hand one snapshot to the continuation and to the paused log entry run-detail serves, and QA run #27134 measured it live at 07d18e692: GET /api/v1/automation/showcase_dynamic_approval/runs/RUNID on a paused run answered status paused with variables present. Decision outputs land as flat . variables (engine.ts applyResumeSignal), so acceptance[3] gains a snapshot leg: the stage-2-paused run's variables['lead_review.next_reviewers'] equals the submitted pick, ids and order, on both chains; the resolution leg (stage-2 pending_approvers 1 → 1, 2 → 2, same order) is kept, and both must hold. The clause is stronger, never weaker: revision 2's '⛔ Do NOT prescribe reading the run's variables snapshot' rested on a paused run carrying no variables key, which no longer holds. The knownGap is rewritten as CLOSED-by-#7639 rather than deleted. The stage-2 key is read from source at base 74133d98, not yet driven live (#27134 read a stage-1 snapshot), so a prior verdict on acceptance[3] does not carry. Wave anchor #25807, wave-2 accuracy slice D; seat note 6464451167", "ref": "#27134" }, + { "revision": 6, "date": "2026-11-01", "change": "Two QA run #28656 (W3-RT) findings. steps[4] names the decision outputs' wire key, so a refusal can be attributed: `outputs` on the approvals route ({\"outputs\":{\"next_reviewers\":[…]}}; approvals-routes.ts reads body.outputs) and the params outputs.next_reviewers on the action door (action-execution.ts folds the outputs.KEY family). The line carried in anchor comment 6539452860 (acceptance[3]'s snapshot leg, written from source in #27164, with no live read) is retired: #28656 read the stage-2-paused run's variables['lead_review.next_reviewers'] live at 6e11a929, equal to the picks in order, and found the key spelling accurate. knownGaps[0]'s 'not yet driven live' sentence now records that read. The carried line itself is a comment on #27377, closed by anchor comment 6557904764; it is not in this file. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", "ref": "#28656" } ] }, { diff --git a/docs/qa/platform-checklist/areas/integration-system.json b/docs/qa/platform-checklist/areas/integration-system.json index e58b669c790..9d1c2dc1342 100644 --- a/docs/qa/platform-checklist/areas/integration-system.json +++ b/docs/qa/platform-checklist/areas/integration-system.json @@ -897,7 +897,7 @@ "title": "Email templates resolve from the registry (no sys_email_template row since ADR-0131 D6/D10), resolve (name, locale) with en-US fallback, render {{path}} holes, gate on required variables and active:false, survive admin edits across redeploys — and the raw POST /api/v1/email/send door authenticates, requires manage_platform_settings, bounds sentBy, from, headers, organizationId and identityMail, refuses anonymous, and 400s malformed input", "since": "v15", "status": "active", - "revision": 11, + "revision": 12, "priority": "P2", "surface": "mixed", "fixtures": { @@ -910,7 +910,7 @@ ], "knownGaps": [ "no stock example exercises an email template end-to-end; needs a small fixture flow with a notify/email step", - "refinement of the gap above: the TEMPLATE fixture exists (showcase_task_done_email) and the send path is pinned by packages/qa/dogfood/test/email-template-materialization.dogfood.test.ts, but no shipped FLOW invokes sendTemplate. The workable door, measured by QA run #23922: a SCRATCH notify flow authored through POST /api/v1/automation (a runtime flow registers on stock showcase), whose notify node names channels ['email'], template 'showcase_task_done_email' and templateData (CEL value envelopes for title / project), fired through its api trigger — the email channel calls sendTemplate per recipient, and each delivery lands in sys_email (admin GET /api/v1/data/sys_email) with the rendered subject and body" + "refinement of the gap above: the TEMPLATE fixture exists (showcase_task_done_email) and the send path is pinned by packages/qa/dogfood/test/email-template-materialization.dogfood.test.ts, but no shipped FLOW invokes sendTemplate. The workable door, measured by QA run #23922 and corrected by QA run #28656 at 6e11a929: a SCRATCH flow authored through POST /api/v1/automation (a runtime flow registers on stock showcase) of type 'api', whose start node's config is { triggerType: 'api', hookId, secret } — an api flow needs a non-blank config.secret or a config.secretRef, and without one its registration is refused (service-automation flow-registration-checks.ts validateApiTriggerSecret; 400 when #28656 met it) — and whose notify node names channels ['email'], template 'showcase_task_done_email' and templateData (CEL value envelopes for title / project) and carries NO inline title or message: template and inline title/message are mutually exclusive (NotifyConfigSchema, packages/spec/src/automation/io-node-config.zod.ts; 400). Fire it as the admin with POST /api/v1/automation/NAME/trigger. Not type 'autolaunched': #28656's autolaunched flow was refused at that door, 403 (the door refuses an elevated, runAs: 'system', self-triggered type with PERMISSION_DENIED, packages/runtime/src/flow-start-admission.ts) — the email channel calls sendTemplate per recipient, and each delivery lands in sys_email (admin GET /api/v1/data/sys_email) with the rendered subject and body" ] }, "variants": [ @@ -929,7 +929,7 @@ "author the zh-CN twin; send to a zh-CN-locale recipient and to an unmatched-locale recipient; capture which row rendered each", "author a template with fromOverride + replyTo and verify both on the outbound message. fromOverride is an object { name?, address } with address an email (EmailAddressInlineSchema, packages/spec/src/system/email-template.zod.ts) and replyTo a plain address string; a string fromOverride is refused at save, 422 INVALID_METADATA 'expected object'", "edit the template wording as an admin in Studio (an environment overlay over the declared item), redeploy/reboot, and re-read the served item over GET /api/v1/meta/email_template/showcase_task_done_email — the admin's wording must still be served", - "on the WRITABLE showcase copy (fixtures.requires[3]), edit the DECLARED source template and metadata-reload WITHOUT a reboot, then GET /api/v1/meta/email_template/ — the registry serves the edited item (email_template is allowRuntimeCreate:true; the plugin subscribes to metadata changes). On the stock read-only showcase this half is not drivable: record it partial, as QA run #23166 did", + "on the WRITABLE showcase copy (fixtures.requires[3]), edit the DECLARED source template and metadata-reload WITHOUT a reboot, then GET /api/v1/meta/email_template/ — the registry serves the edited item (email_template is allowRuntimeCreate:true; the plugin subscribes to metadata changes). Keep the reboot out of it: under os dev's default watcher a declared-source save recompiles AND restarts the server (packages/cli/src/commands/dev.ts, --restart defaults on), which proves nothing about a reload. Compile into the running copy's artifact with objectstack compile --output COPY/dist/objectstack.json, or boot the copy with os dev --no-restart (the watcher then only rebuilds the artifact), and read the listener's PID before and after (lsof -ti tcp:PORT): an unchanged PID is part of the evidence, and a changed one means this half was not driven. On the stock read-only showcase this half is not drivable: record it partial, as QA run #23166 did", "build the two rejection probes (stray key `body`; name 'BadName') and capture the errors", "drive the raw send door POST /api/v1/email/send four ways: (a) as an admin (holding manage_platform_settings) with a well-formed message { to, subject, html } (or `text`; SendEmailInput's body keys are `html`/`text` — `bodyHtml`/`bodyText` are the TEMPLATE authoring fields, not wire keys) → capture status + the dev-transport landing; (b) anonymous (no session) → capture status; (c) a non-object / malformed body → capture the envelope; (d) as a member WITHOUT manage_platform_settings (mint one through admin create-user), and as the admin with a `from` other than the configured sender, with a `sentBy` naming another user, with a `headers` value that is not a plain object of single-line strings (a list of entries, say), with an `organizationId` other than the admin's active organization, and with `identityMail: true` → capture each refusal and confirm no new sys_email or sys_identity_email row; then send once more as the admin with no `organizationId` and confirm the new sys_email row's organization_id is the admin's active organization", "while you are at the raw door, send the wrong-vocabulary body { to, subject, bodyHtml } deliberately and capture the refusal — 400 VALIDATION_FAILED 'at least one of text or html is required'. This is the shape the checklist itself used to name (run #7690); keeping the probe pins the two vocabularies apart" @@ -1072,6 +1072,12 @@ "date": "2026-10-31", "change": "three checklist-accuracy findings of QA run #27438. acceptance[7]: the admin's 200 body is the SendEmailResult at top level, {id, status:'sent', messageId}, with no result wrapper or success envelope. email-routes.ts answers res.status(200).json(result), and its 400s are flat {code, error}. steps[5]: fromOverride is an object {name?, address} (EmailAddressInlineSchema), and a string is refused 422 INVALID_METADATA 'expected object'. acceptance[1] and [2]: the failure is recorded on sys_notification_delivery (status 'dead', error 'MISSING_VARIABLES: title' or 'TEMPLATE_INACTIVE: NAME'), not in the served log. email-channel.ts grades both permanent, so the row is dead-lettered. Their oracle moves from log to api. The assertions are unchanged, read where the evidence lives. Read from source at base 22ce3d13fe. Wave anchor #27377, wave-3 accuracy slice S7 (#28341)", "ref": "#27438" + }, + { + "revision": 12, + "date": "2026-11-01", + "change": "Two QA run #28656 (W3-RT) findings. knownGaps[1]'s recipe was incomplete. #28656 met three refusals on the way: an inline title/message beside template (400), type 'autolaunched' at POST /api/v1/automation/NAME/trigger (403), and type 'api' with no config.secret or secretRef (400). It now spells a type:'api' flow with start config {triggerType:'api', hookId, secret}, a notify node with channels ['email'], template and templateData and no inline title/message, fired as the admin. Source: NotifyConfigSchema's template/inline exclusivity (io-node-config.zod.ts) and validateApiTriggerSecret (flow-registration-checks.ts). The 403's rule is read from flow-start-admission.ts, which refuses a runAs: 'system' self-triggered type. That #28656's flow declared runAs: 'system' is not confirmed from source, and the 400 and 403 statuses are #28656's readings. steps[7] said 'metadata-reload WITHOUT a reboot' on a copy whose os dev watcher restarts on every save (dev.ts --restart, default on). It now names objectstack compile --output into the running copy's dist/objectstack.json or --no-restart, and requires an unchanged listener PID as evidence. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", + "ref": "#28656" } ] }, diff --git a/docs/qa/platform-checklist/areas/records-forms.json b/docs/qa/platform-checklist/areas/records-forms.json index ad265bf6dbd..9a06968d0a6 100644 --- a/docs/qa/platform-checklist/areas/records-forms.json +++ b/docs/qa/platform-checklist/areas/records-forms.json @@ -5277,7 +5277,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 record page's sys_audit_log field-change read and derived history op both ways", "since": "v15", "status": "active", - "revision": 10, + "revision": 11, "priority": "P1", "surface": "mixed", "personas": [ @@ -5298,7 +5298,7 @@ "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", - "record the network log from page load, open the account record and select its timeline stream's Changes chip (变更). Record page v3 has no History tab at any width (#27055). The chips are on the stream's whole face, the 动态 (Activity) tab: open it from the header of the 动态 panel, the first panel of the record page's right column at every width (below the details in one column under 760 px, ui/packages/plugin-detail/src/synth/recordPageColumns.ts#recordPageTierFor), or by ?related=timeline (#27524). No width draws a tab strip on the synthesized page. Then open a seeded task's detail and read the network log again", + "record the network log from page load, open the account record and select its timeline stream's Changes chip (变更). Record page v3 has no History tab at any width (#27055). The chips are on the stream's whole face, the 动态 (Activity) tab: open it from the Activity tab of the record page's tab row (Overview · Activity · the related panels, ui/packages/plugin-detail/src/RecordPageColumns.tsx), from the header of the 动态 panel, the first panel of the record page's right column at every width (below the details in one column under 760 px, ui/packages/plugin-detail/src/synth/recordPageColumns.ts#recordPageTierFor), or by ?related=timeline (#27524). QA run #28656 read the tab row at 1440, 1000 and 700 px. Then open a seeded task's detail and read the network log again", "GET /api/v1/auth/me/permissions and compare apiOperations for showcase_account vs showcase_task" ], "acceptance": [ @@ -5344,7 +5344,7 @@ ], "automated": { "kind": "unit", - "ref": "packages/plugins/plugin-audit/src/audit-lookup-summary.test.ts (reference titles + read plan); audit-option-label-summary.test.ts (option labels); ui/e2e/live/record-history-display.spec.ts (the record history's display contract, which the cross-ref in source[4] assigns to records-forms.crud-roundtrip. At 1440×900, a column tier whatever the sidebar does, it opens the timeline stream's Changes chip on showcase_account in a browser, the enable.trackHistory object, so it holds acceptance[2]'s flag-true DOM half; it reads no network log and never opens the task detail, so the read gate both ways is driven by hand). packages/qa/dogfood/test/field-history-tracking-activity.dogfood.test.ts, at the REST doors of the showcase with AuditPlugin added as objectstack serve composes it: acceptance[0] and negative[2] (PATCH industry retail → finance lands a sys_activity row whose summary is exactly 'Industry: Retail → Finance', message key fieldChanges), acceptance[1] and negative[0] (a website-only PATCH lands the generic frame naming the Website label with no arrow and neither URL, message key activityUpdatedFields) and acceptance[3] (GET /api/v1/auth/me/permissions: showcase_account's apiOperations carry history, showcase_task's carry get and not history). acceptance[4] stays the unit pin's. acceptance[2]'s absence half and negative[1] (no sys_audit_log read from the task detail) stay live browser legs: they are judged on the network log of the rendered detail page, which no HTTP pin reaches, and the card's ruling adds no Playwright lane (#26573)" + "ref": "packages/plugins/plugin-audit/src/audit-lookup-summary.test.ts (reference titles + read plan); audit-option-label-summary.test.ts (option labels); ui/e2e/live/record-history-display.spec.ts (the record history's display contract, which the cross-ref in source[4] assigns to records-forms.crud-roundtrip. It runs only against the vite console dev server, LIVE_APP_URL, default http://localhost:5180 (ui/playwright.live.config.ts), never a built /_console boot: its page.goto paths are root-relative (/apps/…). At 1440×900, a column tier whatever the sidebar does, it opens the timeline stream's Changes chip on showcase_account in a browser, the enable.trackHistory object, so it holds acceptance[2]'s flag-true DOM half; it reads no network log and never opens the task detail, so the read gate both ways is driven by hand). packages/qa/dogfood/test/field-history-tracking-activity.dogfood.test.ts, at the REST doors of the showcase with AuditPlugin added as objectstack serve composes it: acceptance[0] and negative[2] (PATCH industry retail → finance lands a sys_activity row whose summary is exactly 'Industry: Retail → Finance', message key fieldChanges), acceptance[1] and negative[0] (a website-only PATCH lands the generic frame naming the Website label with no arrow and neither URL, message key activityUpdatedFields) and acceptance[3] (GET /api/v1/auth/me/permissions: showcase_account's apiOperations carry history, showcase_task's carry get and not history). acceptance[4] stays the unit pin's. acceptance[2]'s absence half and negative[1] (no sys_audit_log read from the task detail) stay live browser legs: they are judged on the network log of the rendered detail page, which no HTTP pin reaches, and the card's ruling adds no Playwright lane (#26573)" }, "source": [ "packages/plugins/plugin-audit/src/audit-writers.ts#planTrackedLookupReads (planTrackedLookupReads), (renderTrackedChangeSummary — ADR-0052 §5b, #7230/#7289 localization), (zero-reads-by-default plan)", @@ -5413,6 +5413,12 @@ "date": "2026-10-29", "change": "acceptance[2], negative[1], steps[3] and the title re-pointed from the History tab to the record page v3 surface, from QA run #27438 (18.3.0 sweep, wave 3). Record page v3 (#27055) removed the History tab at every width, and the gate now lives on the sys_audit_log read: the account's read fills the timeline stream's Changes chip, and the task makes none. acceptance[2]'s oracle moves from dom to network, because the Changes chip also lists sys_activity rows and is no absence oracle. Measured at base 882d4e4a5c on a booted showcase with the console dev server. Account: one audit read at load, no tab strip, at 1440×900 (record view 1184 px) and 1000×800 (952 px, the sidebar as its icon rail). Only 700×800 drew tabs (Details / Related / Timeline, no History), and its read went out when Timeline opened. Task: the authored showcase_task_detail page, no stream, no audit read at any of the three widths. So that half is over-determined (new knownGaps entry). negative[0]'s exemption now names the stream's audit-log lines instead of the History tab. negative[3], a cross-reference that could only score skipped, moved to source[4]. source[2] and automated.ref point at ui/ and at the re-pointed live spec, record-history-display.spec.ts, which now reads the Changes chip at 1440×900 (passed once on that boot)", "ref": "#27492" + }, + { + "revision": 11, + "date": "2026-11-01", + "change": "Two QA run #28656 (W3-RT) findings. steps[3] said 'No width draws a tab strip on the synthesized page', stale since #28469: the record page draws a tab row, Overview · Activity · the related panels (ui/packages/plugin-detail/src/RecordPageColumns.tsx, the tab row), which #28656 read at 1440, 1000 and 700 px. The step now opens the stream from the Activity tab, the 动态 panel header, or ?related=timeline. automated.ref names a requirement of ui/e2e/live/record-history-display.spec.ts: it runs only against the vite console (LIVE_APP_URL, default port 5180, ui/playwright.live.config.ts; its page.goto paths are root-relative), not a built /_console boot. acceptance[2] is unaffected. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", + "ref": "#28656" } ] }, diff --git a/docs/qa/platform-checklist/areas/studio-authoring.json b/docs/qa/platform-checklist/areas/studio-authoring.json index eca3e702379..1a47f9ca655 100644 --- a/docs/qa/platform-checklist/areas/studio-authoring.json +++ b/docs/qa/platform-checklist/areas/studio-authoring.json @@ -714,7 +714,7 @@ "title": "The Studio CEL editors reach the SAME verdict as the engine: formula result-type inference, previous. completion in conditional rules, and RLS lint + test-run — the editor UI, not a second grammar", "since": "v16", "status": "active", - "revision": 7, + "revision": 8, "priority": "P1", "surface": "mixed", "personas": ["admin"], @@ -727,17 +727,16 @@ ], "knownGaps": [ "the CEL editors lazy-load @objectstack/formula and FEATURE-DETECT every entry point (celAuthoring.ts) — a missing/older engine degrades to no lint / no suggestions / test-run 'unavailable', never an exception. If the vendored bundle lacks the engine, record the affected clauses blocked(dependency) and verify the graceful-degradation clause instead", - "AUTOMATION LIVES UNDER ui/ — `automated.ref` names objectui specs, and since objectui was imported into this repository they resolve at ui/PATH in this checkout. A jsdom unit pin runs here (`pnpm --filter @object-ui/PKG exec vitest run --maxWorkers=2 FILE`); a ui/e2e/live spec runs from ui/ with `pnpm test:e2e:live SPEC` against a live backend and the console dev server (the prerequisites are in ui/playwright.live.config.ts). Cite the output naming the commit, or drive the item by hand in the browser and score it as a manual run. ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact.", - "acceptance[7] (graceful degradation) is PIN-ONLY BY DESIGN: its verify forbids hand-breaking the bundle. The environment it needs is this checkout with its install done, to run ui/packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (describe 'celAuthoring · graceful degradation') with `pnpm --filter @object-ui/app-shell exec vitest run --maxWorkers=2 FILE`. QA run #21784 had no runnable checkout and recorded blocked(environment) (parked on #22017 row 16), which is the right verdict without one: never a hand-driven verdict, never inferred from the source. CI in this repository now runs the ui/ suites (the Test UI job), which is evidence about the commit CI built, not about the run subject: cite a run of the file at the subject sha", - "acceptance[2] and acceptance[4] score the editor against the ENGINE's tier for a non-lowerable RLS read predicate, which is ERROR (rls-predicate-unenforceable, fail-closed). At the console pin 4016370a the editor still gives it severity 'warning' with fail-open wording (objectui packages/app-shell/src/views/metadata-admin/celAuthoring.ts lintCelPredicate) and autosaves it — the product defect objectstack-ai/objectui#13061 (QA run #24604 negative[0], rule-7 verified). Until that lands, those two clauses FAIL as written: score them fail with that card cited, never 'pass as written'", - "THE FORMULA-FIELD PROBE FAILS TODAY (QA run #26715, Data card, at 3fcafcd3; re-read at 3230bdbf): the Studio RLS editor reads 'Valid CEL' for a USING predicate on a formula field (record.f_formula > 5) and autosaves it, and only the publish refuses it, rls-predicate-unenforceable at INVALID_FIELD — the editor's lint judges a one-clause stack that names no object (celAuthoring.ts rlsPredicateVerdict), so the rule's virtual-field branch cannot fire there. #26707 is closed by 57820085, which fixed the draft half: a draft read's _diagnostics now carries the publish verdict. The editor half has no open card. Until it is fixed, acceptance[2] and acceptance[4] FAIL on this probe: score them fail citing the run" + "AUTOMATION LIVES UNDER ui/ — `automated.ref` names objectui specs, and since objectui was imported into this repository they resolve at ui/PATH in this checkout. A jsdom unit pin runs from ui/, the objectui root, with its path written relative to ui/: `cd ui && pnpm exec vitest run --maxWorkers=1 packages/PKG/src/FILE`. ⛔ Not `pnpm --filter @object-ui/PKG exec vitest run FILE`: that runs in the package directory, and the ui vitest guard (ui/scripts/vitest-invocation-guard.mjs, which ui/vitest.config.mts calls) refuses any run whose Vitest root is not ui/ (its package-cwd verdict). a ui/e2e/live spec runs from ui/ with `pnpm test:e2e:live SPEC` against a live backend and the console dev server (the prerequisites are in ui/playwright.live.config.ts). Cite the output naming the commit, or drive the item by hand in the browser and score it as a manual run. ⛔ Never score it as covered on the strength of the `automated` field alone — an unrun pin is a claim, not evidence. Full protocol: RUNNER.md, the objectui-pinned-automation standing fact.", + "acceptance[7] (graceful degradation) is PIN-ONLY BY DESIGN: its verify forbids hand-breaking the bundle. The environment it needs is this checkout with its install done, to run ui/packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (describe 'celAuthoring · graceful degradation') with `cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts` (the `pnpm --filter @object-ui/app-shell exec vitest run` form is refused by the ui vitest guard, knownGaps[1]). QA run #21784 had no runnable checkout and recorded blocked(environment) (parked on #22017 row 16), which is the right verdict without one: never a hand-driven verdict, never inferred from the source. CI in this repository now runs the ui/ suites (the Test UI job), which is evidence about the commit CI built, not about the run subject: cite a run of the file at the subject sha", + "THE FORMULA-FIELD PROBE FAILS TODAY (QA run #26715, Data card, at 3fcafcd3; re-read at 3230bdbf; read live again by QA run #28656, W3-RT, at 6e11a929): the Studio RLS editor reads 'Valid CEL' for a USING predicate on a formula field (record.f_formula > 5), and only the publish refuses it, rls-predicate-unenforceable at INVALID_FIELD — the editor's lint judges a one-clause stack that names no object (celAuthoring.ts rlsPredicateVerdict), so the rule's virtual-field branch cannot fire there. #26707 is closed by 57820085, which fixed the draft half: a draft read's _diagnostics carries the publish verdict. The editor half is #27482, which is open. Its layers #27612, #27643 and #27644 have landed, so a server error at the clause's path now holds the autosave from the server re-read on: the FIRST autosave of each newly refused text still goes out and answers 200, and the edit after it is held (ui/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.serverVerdictHold-27482.test.tsx, case (b): the refused clause saves once, its re-read holds the next edit). That first PUT is the residual #27482 carries. Until #27482 is ruled, acceptance[2] and acceptance[4] FAIL on this probe as written, acceptance[4]'s 'no PUT issued' included: score them fail citing the run" ] }, "steps": [ "ground the engine truth FIRST (rule 6 parity): for each probe expression, capture the canonical verdict from the engine — via MCP validate_expression (site: formula / flow_condition) or `objectstack build` — so the editor is judged against server truth. validate_expression has NO RLS site (formula | validation | flow_condition | template), so the RLS probes' engine verdict is the author-time gate `os build` and publish run: packages/lint/src/validate-rls-predicate-enforceability.ts (rls-predicate-unenforceable / rls-predicate-unparseable, both error tier). The ENGINE itself is pinned by ai.mcp-validate-expression / api-backend.formula-gates; this item only checks the EDITOR agrees", "F1 formula result-type: open the object field designer (ObjectFieldInspector) on a formula field (type=formula, role=value, scope=record), TYPE a proven-number expression (record.est_hours * 1.1, or record.quantity * record.unit_price) and, separately, one cel-js cannot prove (record.a + record.b); after the 250ms debounce, screenshot the inferred-result-type (Σ) affordance for each", "type an unknown field (record.est_hourz) and a bare field ref in record scope (est_hours, no record. prefix); capture the inline error under the field (aria-invalid, destructive border) and its record. fix text", - "F2 conditional-rule editor: open the same field's visibleWhen editor (scope=record, roots record/previous/parent); type `previous.` and then `record.` and capture the autocomplete listbox — it must offer the object's field catalog; type `current_user.` and confirm it does NOT list this object's fields; accept a suggestion and confirm the insertion", + "F2 conditional-rule editor: open the same field's visibleWhen editor (scope=record, roots record/previous/parent); type `previous.` and then `record.` and capture the autocomplete listbox — it must offer the object's field catalog; type `current_user.` and confirm it does NOT list this object's fields; accept a suggestion and confirm the insertion. The listbox shows at most 8 suggestions (MAX_CEL_SUGGESTIONS, ui/packages/app-shell/src/views/metadata-admin/celAuthoring.ts) in catalog order, and a bare `record.` led with the system fields when QA run #28656 drove it, so the field under test may not be on it: type a member prefix (`record.est`, `previous.s`) and read the listbox for that", "F4 RLS editor: in Studio open /_console/studio/PKG/access, select the permission set, open its 'Row-level security' tab (PermissionAdvancedFacets) and add a policy on the object; enter a USING predicate that is NOT a simple field comparison (blast-radius), one with a parse fault, and — the virtual-field probe — one comparing the object's FORMULA field (e.g. record.FORMULA_FIELD > 5: a formula field has no storage column, so the engine cannot push the filter down and refuses it, rls-predicate-unenforceable, packages/lint/src/validate-rls-predicate-enforceability.ts#RLS_PREDICATE_UNENFORCEABLE); confirm ALL THREE raise a blocking ERROR: the non-pushdownable filter at the engine's tier — rls-predicate-unenforceable refuses it at build and publish (ADR-0056 D4) because the runtime drops the policy and fails CLOSED, so it grants no access — and the parse fault as a parse error; with an error present the save is held (celErrorCount>0): the environment door's Save is disabled with title perm.cel.saveBlocked, and the Studio package door, which autosaves and has no Save button, shows the perm-autosave-held status and issues no PUT", "RLS test-run: open the CelTestRunDialog ('Test this policy against a sample record'), supply a sample record + current_user, Run; capture allow (shield-check) / deny (shield-x) / a non-boolean value (non-bool smell) across three sample+predicate pairs", "parity cross-check: for the SAME expressions, confirm the editor's verdict tier (error/warning/ok, inferred type, allow/deny) equals the engine verdict captured in step 1", @@ -789,7 +788,7 @@ { "clause": "when @objectstack/formula cannot load the editors degrade to no-lint / no-suggestions / test-run 'unavailable', never an exception that breaks the form (feature-detect + swallow)", "oracle": "test", - "verify": "run ui/packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (unavailable-engine cases) and cite its output — do not hand-break the bundle in the browser", + "verify": "run ui/packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts (unavailable-engine cases) from ui/, `cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts`, and cite its output — do not hand-break the bundle in the browser", "evidence": "the test output" } ], @@ -854,6 +853,12 @@ "date": "2026-10-28", "change": "fixtures.knownGaps[4] re-read against main 3230bdbf: #26707 is closed by 57820085 (the draft read's _diagnostics reports the publish verdict), but the RLS editor still lints a one-clause stack with no object and reads 'Valid CEL' on the formula-field probe, so acceptance[2] and [4] still fail on it, now cited to the run. No clause changed", "ref": "#26848" + }, + { + "revision": 8, + "date": "2026-11-01", + "change": "Four QA run #28656 (W3-RT) findings. (1) fixtures.knownGaps[3] is dropped. It said the editor gives a non-lowerable RLS read predicate the warning tier and autosaves it (objectui#13061). The editor now asks the published rule for both RLS clauses and keeps its error severity (celAuthoring.ts lintCelPredicate and rlsPredicateVerdict; celAuthoring.test.ts pins the error for USING and CHECK), and #28656 read it live at 6e11a929: error tier, perm-autosave-held, 0 PUTs. The former knownGaps[4] is now knownGaps[3]. (2) In that knownGap only the factual half changes: the editor half is #27482 (open), not 'no open card'; #27612, #27643 and #27644 landed; the residual is the first autosave of each newly refused text (PermissionMatrixEditor.serverVerdictHold-27482.test.tsx case (b)). acceptance[4]'s 'no PUT issued' is left as it was until #27482 is ruled. (3) knownGaps[1], knownGaps[2] and acceptance[7].verify name the invocation the ui vitest guard accepts, cd ui && pnpm exec vitest run --maxWorkers=1 packages/app-shell/src/views/metadata-admin/celAuthoring.test.ts. The pnpm --filter @object-ui/app-shell exec form runs in the package directory, which the guard refuses (ui/scripts/vitest-invocation-guard.mjs, verdict package-cwd). (4) steps[3] says the completion listbox caps at 8 (MAX_CEL_SUGGESTIONS) and to type a member prefix. That a bare record. leads with the system fields is #28656's live reading. No clause is weakened. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", + "ref": "#28656" } ] }, @@ -2025,7 +2030,7 @@ "title": "Studio asks before an unsent edit is left behind: a held (incomplete) or dirty edit in any pillar survives a pillar switch, a rail switch, the package switcher, Home and a reload — or is discarded only after the author confirms", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P1", "surface": "browser", "personas": [ @@ -2046,7 +2051,7 @@ "Automations, held step: open the flow, add a step whose required input is empty — the strip names '{input} on the step “{step}” needs a value'; repeat departures (a), (b) with another flow in the rail, and (e)", "Interfaces, held widget: open the app's dashboard, add a widget and leave its Dataset empty — the strip names 'Dataset on the widget “…” needs a value'; repeat departures (a) and (e)", "controls: Access — tick one matrix checkbox (dirty, not held); Interfaces — rename a navigation entry with nav editing open; repeat departures (a), (d) and (e) for each. A control is dirty only until its autosave lands: Studio's shared draft autosave fires 1.5 s after the LAST edit (ui/packages/app-shell/src/views/studio-design/useDraftAutoSave.ts), so start each departure inside that window, timed from the edit, and record the gap. A departure after the save landed leaves a clean pillar, where no prompt is correct and not a fail of acceptance[0] or [1]", - "server truth after each answer: GET /api/v1/meta/_drafts?packageId=PKG (the package's pending drafts) and the item's draft read (GET /api/v1/meta/TYPE/NAME?state=draft&package=PKG) — after Leave they carry nothing of the discarded edit; after Stay, finishing the edit (an option with a value, a target, the input, a dataset) lets the autosave PUT carry it" + "server truth after each answer: GET /api/v1/meta/_drafts?packageId=PKG (the package's pending drafts) and the item's draft read (GET /api/v1/meta/TYPE/NAME?state=draft&package=PKG) — after Leave they carry nothing of the discarded edit; after Stay, finishing the edit (an option with a value, a target, the input, a dataset) lets the autosave PUT carry it. The item draft read answers 404 NO_DRAFT when the item has no pending draft (packages/metadata-protocol/src/protocol.ts noPendingDraftError), so for a held edit that never saved, a 404 before AND after Leave is the 'unchanged' reading, not a failed read" ], "acceptance": [ { @@ -2114,6 +2119,12 @@ "date": "2026-11-01", "change": "negative[2] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "QA run #28656 (W3-RT) finding: steps[7] says the item draft read GET /api/v1/meta/TYPE/NAME?state=draft&package=PKG answers 404 NO_DRAFT when no draft exists (metadata-protocol protocol.ts noPendingDraftError; its wire answer is pinned in packages/rest/src/meta-item-absent-404.test.ts), so a 404 before and after Leave is the 'unchanged' reading acceptance[3] asks for, as #28656 read it live at 6e11a929. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", + "ref": "#28656" } ] }, @@ -2926,7 +2937,7 @@ "title": "A summary (roll-up) field authored in Studio is held until it names its child object and function, offers only numeric child fields for sum and avg, and once published computes and recomputes from the children", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -2937,9 +2948,6 @@ "requires": [ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "a writable package with a published parent object and a published child object holding a master_detail field to the parent and a number field (and a text field, for the numeric-only check)" - ], - "knownGaps": [ - "acceptance[0] AND [1] FAIL TODAY (QA run #26715, Data card defect 6, at 3fcafcd3; re-read at 3230bdbf). (1) An unconfigured summary field is not held: the Data pillar saves it, and the publish refuses it. Since 57820085 (#26707, closed) the draft read's _diagnostics carries that publish verdict beforehand, but the field is still saved, not held. (2) For sum and avg the field picker still offers every visible child field, numeric or not (ObjectFieldInspector SummaryConfigFields). No card is open for either; score the two clauses fail citing the run. The computing clauses pass" ] }, "steps": [ @@ -2975,7 +2983,7 @@ } ], "negative": [ - "a summary saved unconfigured instead of held — refused only at publish, rolling back the rest of the package release with it — is the shape this item fails today (its draft verdict now reports it, #26707; the hold is still missing)" + "a summary saved unconfigured instead of held — refused only at publish, rolling back the rest of the package release with it — is a FAIL (acceptance[0])" ], "traps": [ "automation-input", @@ -3012,6 +3020,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "QA run #28656 (W3-RT) finding: fixtures.knownGaps (its one entry, 'acceptance[0] AND [1] FAIL TODAY') is dropped. #27461 is fixed by PR #27720: an unconfigured summary is held (ui/packages/app-shell/src/views/studio-design/DataPillar.summaryHeld-27461.test.tsx), and the sum/avg picker offers only the child fields the spec accepts for the function (ObjectFieldInspector.summaryAggregand-27461.test.tsx). #28656 read both clauses pass live at 6e11a929. negative[0], which called the saved-unconfigured summary 'the shape this item fails today', is reworded as a plain FAIL condition of acceptance[0]. No clause is weakened. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", + "ref": "#28656" } ] }, @@ -3201,7 +3215,7 @@ "title": "A package dashboard renders every widget type the spec declares — those Studio offers and those written through the API — and every global-filter type renders its control and re-scopes the widgets bound to it; Studio's two widget-type entries offer the same set", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -3213,17 +3227,14 @@ "a fresh boot of the showcase on its own port and DB (dogfood §0) with OS_PORT exported, signed in as the seeded admin, and a console built from this tree with `pnpm console:build` — the root `pnpm build` does not refresh packages/console/dist (QA run #26715 needed the console build on every card)", "a writable package with an app whose menu names one dashboard", "a dataset over a package object, with at least one dimension and one measure, written with PUT /api/v1/meta/dataset/NAME?mode=draft&package=PKG and published — Studio has no dataset editor (expected: the Interfaces card read it no-entry), so reports and widgets bind to an API-written dataset. Give it several dimensions (a status-like category and a date) and a few measures over seeded multi-bucket rows — examples/app-showcase/src/ui/dashboards/chart-gallery.dashboard.ts is the model for each type's bindings. Sankey takes no source/target pair: it flows from one root node (the total) to each category of its first dimension, and a second dimension (two or more pivoted series) adds a second column of nodes (ui/packages/plugin-charts/src/AdvancedChartImpl.tsx, the sankey arm and buildSankeyCellFlows; the gallery's sankey_flow binds dimensions ['status'])" - ], - "knownGaps": [ - "acceptance[4] FAILS TODAY (QA run #26715, Interfaces card D11, at 3fcafcd3; source re-read at 3230bdbf): the global-filter editor's Add item autosaves a filter with no field, the whole dashboard draft is then refused, and the other edits in it are lost on reload. Family card #26707 closed with #26856, which makes the draft read's _diagnostics and Problems report the refusal; the Studio form still autosaves the incomplete item, and no open card carries that half" ] }, "steps": [ "open the dashboard in /_console/studio/PKG/interfaces; read the Add-widget menu's type list and, on one widget, the Type picker's list", "add one widget of every type the Add menu offers, bound to the dataset (a dimension and a measure each); the menu offers every ChartTypeSchema member since #26967, so for any type it does not list, record the missing type and write that widget into the same dashboard draft with PUT /api/v1/meta/dashboard/NAME?mode=draft&package=PKG (keep the Studio-authored widgets in the body)", - "global filters: add one filter of each type — text, select (with options or optionsFrom), date, number, lookup (with optionsFrom {object, valueField, labelField}: a lookup filter draws the same select control and, with no options or optionsFrom, offers no choices — ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx) — and bind each to at least one widget through that widget's filter mapping; leave one widget unbound", + "global filters: add one filter of each type — text, select (with options or optionsFrom), date, number, lookup (with optionsFrom {object, valueField, labelField}: a lookup filter draws the same select control and, with no options or optionsFrom, offers no choices — ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx) — and bind each to at least one widget through that widget's filter mapping; leave one widget unbound, which means filterBindings { NAME: false } for EVERY filter on it. A widget with no filterBindings entry is not unbound: it inherits each filter on that filter's own field (ui/packages/core/src/utils/dashboard-filters.ts resolveBoundField: an explicit binding, then targetWidgets, then the filter's field; read from source, not driven live)", "publish; open the board in the app; screenshot it settled", - "change each filter on the rendered board in turn; record which widgets re-query (network) and how their values change; reset", + "change each filter on the rendered board in turn (a text or number filter commits only on Enter or when the input loses focus, ui/packages/plugin-dashboard/src/DashboardFilterBar.tsx TextFilter, so press Enter before reading the network); record which widgets re-query (network) and how their values change; reset", "in Studio add a global filter with Add item and leave it incomplete; watch the autosave and reload" ], "acceptance": [ @@ -3254,7 +3265,7 @@ { "clause": "Studio's global-filter editor never autosaves an incomplete filter: the item is held until complete, and no other edit is lost", "oracle": "network", - "verify": "no dashboard PUT carries the incomplete filter; after a reload, the other edits are present", + "verify": "no dashboard PUT carries the incomplete filter. Three cases, each judged: (a) the edits made BEFORE Add item survive a reload; (b) an edit made while the item is held is carried by the PUT that completes the item, answered 200 (ui/packages/app-shell/src/views/studio-design/InterfacesPillar.heldListItem-27459.test.tsx); (c) a reload while the item is held raises the browser's leave-site prompt, and Leave then discards the held item and the edits kept with it, by design (studio-authoring.held-edit-leave-guard) — a reload with no prompt that loses an edit is a FAIL", "evidence": "the trace + the post-reload read" } ], @@ -3306,6 +3317,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "Three QA run #28656 (W3-RT) findings. fixtures.knownGaps (its one entry, 'acceptance[4] FAILS TODAY') is dropped. #27459 is fixed by PR #27784 (InterfacesPillar.heldListItem-27459.test.tsx: Add item then a Label edit sends nothing, and filling the Field in sends one save carrying the item and the Label edit), and #28656 read acceptance[4] pass live at 6e11a929. The report-panel half is #27785. acceptance[4].verify said only 'after a reload, the other edits are present'. It now judges three cases: edits before Add item survive a reload; an edit made while held lands in the PUT that completes the item; a reload while held must raise the leave-site prompt, and Leave then discards by design. steps[4] says a text or number global filter commits only on Enter or blur (DashboardFilterBar.tsx TextFilter). steps[2] says 'unbound' means filterBindings false for every filter, since a widget with no filterBindings inherits each filter on the filter's own field (dashboard-filters.ts resolveBoundField; read from source, not driven live). No clause is weakened. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); findings parked in anchor comment 6557904764", + "ref": "#28656" } ] }, @@ -3405,7 +3422,7 @@ "title": "App and menu-entry keys reach the running app: branding, areas and context selectors render; an entry's viewName, recordId / recordMode, filters, badge, requiresObject and visible each change what the entry does or who sees it; hidden takes an app off the switcher and nothing else — and Studio's menu-entry inspector edits them", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -3424,7 +3441,7 @@ ] }, "steps": [ - "write the app through PUT /api/v1/meta/app/APP?mode=draft&package=PKG: branding { primaryColor, accentColor, logo }, two areas each holding some entries, a contextSelector, and menu entries: an object entry with viewName (the second list view), one with recordId + recordMode 'edit', one with filters, one with badge + badgeVariant, one with requiresObject naming an object that does not exist, and one with a visible predicate false for the member (e.g. 'org_owner' in current_user.positions: the seeded admin holds org_owner, not org_admin, because an organization's owner membership maps to org_owner alone, packages/spec/src/identity/eval-user.zod.ts, so an 'org_admin' predicate is false for both personas and proves nothing). Make this app the package's FIRST app (steps[4]); publish", + "write the app through PUT /api/v1/meta/app/APP?mode=draft&package=PKG: branding { primaryColor, accentColor, logo } (a runner-environment note: give logo a same-origin URL such as /_console/vite.svg, which the console build ships from ui/apps/console/public; an external logo URL is blocked by the runner container's egress proxy and never renders), two areas each holding some entries, a contextSelector, and menu entries: an object entry with viewName (the second list view), one with recordId + recordMode 'edit', one with filters, one with badge + badgeVariant, one with requiresObject naming an object that does not exist, and one with a visible predicate false for the member (e.g. 'org_owner' in current_user.positions: the seeded admin holds org_owner, not org_admin, because an organization's owner membership maps to org_owner alone, packages/spec/src/identity/eval-user.zod.ts, so an 'org_admin' predicate is false for both personas and proves nothing). Make this app the package's FIRST app (steps[4]); publish", "as admin and as the member (each in its own context, identity proved with GET /api/v1/auth/get-session), open the app: read the chrome colours and logo, the areas switcher, the context selector, and click each entry", "GET /api/v1/meta/app?id=APP as each persona and list the entries each receives", "set hidden: true on a second app of the package (through the API) and publish; as the member read the App Switcher, the Home launcher, the avatar menu and GET /api/v1/meta/app?id=SECOND; open its URL directly", @@ -3507,6 +3524,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[3]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "QA run #28656 (W3-RT) finding: steps[0] says to give branding.logo a same-origin URL such as /_console/vite.svg (ui/apps/console/public/vite.svg ships in the console build). This is a runner-environment note: an external logo URL is blocked by the runner container's egress proxy and never renders, which is #28656's live reading at 6e11a929. No clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", + "ref": "#28656" } ] }, @@ -3806,7 +3829,7 @@ "title": "The Access pillar's org-wide defaults panel and sharing suggestions do what they say: the OWD panel lists every package object, saves only the changed one and offers controlled-by-parent only with a master; a package's suggested set is granted to Everyone, revoked or dismissed from the pillar, with immediate effect", "since": "v18", "status": "active", - "revision": 4, + "revision": 5, "priority": "P2", "surface": "mixed", "personas": [ @@ -3823,7 +3846,7 @@ ], "knownGaps": [ "THE GUEST PROMPT CANNOT BE REACHED ON STOCK (QA run #26715, Access card G5): only Everyone suggestions exist, and accepting one for Guest answers 405; the pillar's Guest wording ('Cannot be granted here: the Guest position takes no sets from accepted suggestions.') is scored only where a guest suggestion exists — record that leg blocked(fixture) otherwise", - "TWO POLISH DEFECTS RECORDED BY THE RUN (Access card D10 and D13): an OWD refusal banner stays after the change is undone, and a dismissed or revoked suggestion offers no way to grant it again from Studio. Neither blocks a clause" + "A POLISH DEFECT RECORDED BY THE RUN (Access card D13): a dismissed or revoked suggestion offers no way to grant it again from Studio, and the door refuses it too: accepting a dismissed suggestion answers 409 SUGGESTION_STATE, 'only pending suggestions can be accepted' (packages/plugins/plugin-security/src/suggested-audience-bindings.ts; read live by QA run #28656 at 6e11a929). It blocks no clause" ] }, "steps": [ @@ -3899,6 +3922,12 @@ "date": "2026-11-01", "change": "negative[1] moved to source[4]: it was a scope note naming the items that own the neighbouring surfaces, which no run can decide, so it could only score skipped. Its text is unchanged except the SCOPE: label, which now reads cross-ref:, the spelling the sibling source rows use. Ruling 6545938831 item ① on #28345 (scope notes leave negative[], which keeps only decidable entries; the #27651 precedent). No clause changed. Wave anchor #27377", "ref": "#28345" + }, + { + "revision": 5, + "date": "2026-11-01", + "change": "QA run #28656 (W3-RT) finding on fixtures.knownGaps[1]: D10 is dropped. After the OWD 422 and an undo the panel reads 'No changes' with no banner, as #28656 read it live at 6e11a929, and ui/packages/app-shell/src/views/studio-design/PackageOwdOverviewPanel.refusalClearsOnRevert-26846.test.tsx pins it (reverting the refused edit clears the banner; CONTROL: an edit still dirty keeps it). D13 still holds, and the knownGap now names its API half: accepting a dismissed suggestion answers 409 SUGGESTION_STATE (suggested-audience-bindings.ts SuggestionStateError). The knownGap names no clause; no clause changed. Re-verified against source at base 1c5b1773ec. Wave anchor #27377, wave-3 accuracy slice S10 (#28701); finding parked in anchor comment 6557904764", + "ref": "#28656" } ] },