Skip to content

QA run · release-18.3.0-w4-c3b1 (36/36) · 7fa9873f · 2026-11-02 · 29 PASS / 0 PARTIAL / 7 FAIL / 0 BLOCKED / 0 NOT-RUN #29002

Open

Description

@objectstack-fleet

domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE
Part of #27377 (wave 4, W4-C3b1)

QA run record for the 18.3.0 release-candidate sweep, card W4-C3b1: the unpinned browser and mixed items of studio-authoring, records-forms, approvals, automation and dashboards, 36 items. Every clause was driven live in a real browser (Playwright chromium) plus its API half, against the Console built in-repo from the subject. Item verdicts are derived from the clause verdicts, never hand-assigned. Run under .claude/skills/checklist-test/SKILL.md and docs/qa/platform-checklist/RUNNER.md. Compared against wave 3 record #27441.

The per-clause verdict table is too long for one issue body (about 130 KB of one-line evidence), so it is posted as the three comments that follow this record, in item order. Everything else is here.

Environment fingerprint

  • Framework: 7fa9873f38f7a0158e3ce17338dcc0f446b113e3 (main, 2026-11-02T02:44:25Z), detached and pinned; it never followed main. ui/ is the same commit. npm_config_nodedir=/opt/node22 pnpm install --frozen-lockfile (34 s), pnpm exec turbo run build --concurrency=2 --filter=!@objectstack/docs under scripts/pm/os-verify-lock.sh (15m57s, exit 0), pnpm console:build --concurrency=2 (cache hit + copy, 3 s), node scripts/check-dev-prereqs.mjs green (106 artifacts). Chromium build 1243 matches playwright-core.
  • Boot (per runner): a tar copy of examples/app-showcase under the runner's own scratch dir, node_modules symlinked to the tree. Command: OS_PORT=PORT OS_STORAGE_LOCAL_ROOT=RUN/uploads OS_HOME=RUN/home setsid node packages/cli/bin/run.js dev --ui --seed-admin --no-watch -p PORT -d file:RUN/data.db. A canary on 41300 measured health 200 in about 12 s, /_console/ 200, and GET /api/v1/meta/app naming showcase_app, setup, account.
  • Ports 41310–41414: one block of five per runner (41310 R1a, 41320 R1b, 41330 R2a, 41340 R2b, 41350 R3, 41360 R4a, 41370 R4b, 41380 R5, 41390 R6, 41400 R7, 41410 R8). Every runner had its own file DB and browser context.
  • Extra boots, each on the runner's own port and DB: walled group boots (OS_TENANCY_POSTURE=group OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only) for approvals.decision-action-matrix A9 (41322) and for an approvals probe (41311); a fresh stock DB for the decision matrix (41321); a fresh DB for an automation clone probe (41341).
  • Seed: --seed-admin (admin@objectos.ai), the stock showcase seed and its demo personas. Members were minted per RUNNER, by invite then sign-up or by admin create-user. Each persona's identity was proved with GET /api/v1/auth/get-session before its cells. Writable scratch packages (POST /api/v1/packages) carried the authoring fixtures; the qaact-placement-specimen and qagroup recipes were installed into the scratch app copies only.
  • Execution: 11 runner subagents, at most 3 browser runners at a time, staggered. The orchestrator read every runner's result, checked every FAIL disposition against source and the ledger, and checked each extracted defect against the board before filing.
  • Hygiene: after the run, git status is clean, examples/app-showcase/.objectstack does not exist, and no listener remains on 41300–41415. Scratch packages, install-local recipes and drafts were deleted or discarded by each runner, or live only in its scratch DB.
  • Time: environment prep about 74 min (22 min orchestrator: install, build, console, canary; 52 min summed across runners: copies, boots and fixtures) against about 256 min of testing plus 67 min of off-clause probes, summed across runners. Prep to testing is about 1 : 3.5 (1 : 4.4 with probes). Wall clock: 03:33Z to 05:54Z for the runs.

Scope

Selector release-18.3.0-w4-c3b1: the unpinned browser/mixed list of anchor #27377 wave 4, card W4-C3b1, as computed at the subject (/mnt/project-files/release-18.3.0/W4-C3b1-items.txt). No planned items in the set. Revisions, read from the ledger at the subject (node scripts/checklist-select.mjs ID --json):

approvals.per-group-signoff r11 · approvals.quorum-m-of-n r9 · approvals.inbox-metadata-actions r8 · approvals.viewer-gating-submitter-side r7 · approvals.decision-action-matrix r10 · automation.flow-run-step-nesting r8 · automation.rollup-summary-filter r7 · automation.flow-node-type-matrix r12 · automation.flow-error-handling r6 · automation.packaged-flow-clone-contract r10 · dashboards.strict-widget-rejects-stray-keys r3 · dashboards.chart-type-matrix r7 · dashboards.global-filters-rescope r5 · records-forms.form-view-gallery r6 · records-forms.action-location-matrix r18 · records-forms.calendar-interactions r4 · records-forms.field-group-visible-when r11 · studio-authoring.view-authoring-live r5 · studio-authoring.draft-publish-lifecycle r10 · studio-authoring.authoring-validation-not-persisted r5 · studio-authoring.packaged-automation-studio-lock r6 · studio-authoring.doc-on-app-menu r9 · studio-authoring.held-edit-leave-guard r5 · studio-authoring.flow-edge-type-authoring r4 · studio-authoring.summary-field-authoring r5 · studio-authoring.api-written-keys-survive-studio-save r3 · studio-authoring.dashboard-widget-and-filter-types r5 · studio-authoring.report-authoring-options r3 · studio-authoring.app-and-nav-properties r5 · studio-authoring.authored-page-slots-and-variables r3 · studio-authoring.access-permission-set-lifecycle r4 · studio-authoring.access-facets-runtime r4 · studio-authoring.access-owd-and-suggestions r5 · studio-authoring.no-entry-property-matrix r4 · studio-authoring.uninstalled-package-refused r3 · studio-authoring.expression-editors r8

Clause ids: A# is acceptance[#] and N# is negative[#], both zero-based.

studio-authoring.expression-editors was run here as the live retest of the #27482 residual: its browser autosave clauses are judged live; its A7 is a pin-only clause, recorded skipped here because the pin belongs to card W4-C2b.

Derived item verdicts

29 PASS / 0 PARTIAL / 7 FAIL / 0 BLOCKED / 0 NOT-RUN (36 of 36 judged).

Deltas against wave 3 (#27441, at c4c515a)

item wave 3 wave 4 note
studio-authoring.held-edit-leave-guard FAIL PASS #27464 holds: a nav rename then reload raises beforeunload (2 of 2)
studio-authoring.summary-field-authoring FAIL PASS #27461 and #27462 hold: an unconfigured rollup is held with no PUT; Sum/Avg offers number and currency only
studio-authoring.app-and-nav-properties FAIL PASS #27457 holds: a bare-string CEL nav visible shows for the admin, hides for the member
studio-authoring.authored-page-slots-and-variables FAIL PASS #27457 holds: size(page.items) > 0 evaluates on first render
studio-authoring.access-owd-and-suggestions FAIL PASS #27458 holds: the 409 opens "Make the relationship required?" and the confirm saves with force
studio-authoring.no-entry-property-matrix NOT-RUN FAIL every variant row driven this time; 5 rows fail, 4 knownGaps[1] keys not judgeable as the item says
studio-authoring.dashboard-widget-and-filter-types FAIL FAIL #27459 holds for the empty item; a different shape fails (partly filled optionsFrom)
studio-authoring.expression-editors FAIL FAIL same stale-spec clauses; #27452 holds; #27482 still open
automation.packaged-flow-clone-contract PASS FAIL stale spec after ADR-0146 D4 (#28149), not a regression
studio-authoring.packaged-automation-studio-lock PASS FAIL stale spec after ADR-0146 D3/D4 (#28572, #28148), not a regression
studio-authoring.access-permission-set-lifecycle PASS FAIL stale spec after ADR-0146 D3 (#28572), not a regression
studio-authoring.draft-publish-lifecycle PASS FAIL stale spec after #28355, not a regression
approvals.viewer-gating-submitter-side, automation.flow-run-step-nesting, dashboards.strict-widget-rejects-stray-keys, records-forms.calendar-interactions (not in wave 3) PASS new to the set
the other 20 items PASS PASS unchanged

Wave-3 off-clause cards re-checked here: #27451 holds (GET /api/v1/approvals/requests/UNKNOWN/actions → 404 REQUEST_NOT_FOUND), #27460 holds (a two-dimension sankey draws 8 links for 8 cells), #27445 holds (an undeclared filterBindings key → 422 at save and at publish), #27452 holds (no stale server error under newly edited RLS text). No PASS turned into a product-defect FAIL.

Reproduction rule for every fail

R-1 · automation.packaged-flow-clone-contract · A1 / A2 / A6 · stale spec (ADR-0146 D4, amended 2026-10-31; #28149; notice reworded by #27886)

  1. Boot the showcase on its own port and DB; sign in admin@objectos.ai.
  2. POST /api/v1/automation/showcase_urgent_task_alert/clone {"name":"qa_urgent_alert_clone","label":"QA urgent alert clone"} → 200 data:{flow:{… status:"obsolete"}, packageId:"com.example.showcase.custom", notice:"The copy is switched off and does not run until you switch it on, and it still uses the same subflows, actions and objects as the original."}.
  3. GET /api/v1/automation/qa_urgent_alert_clone → _packageId:"com.example.showcase.custom", status obsolete.
  4. POST /api/v1/data/showcase_task {title, project, priority:"urgent"} three times; read both flows' runs → base +1 per create, clone 0.
  5. Cold restart on the same DB; /_console/studio/~org/automations → "No flows outside a package…"; /_console/studio/com.example.showcase/automations lists the clone, "In com.example.showcase.custom".
    Expected (clause r10): a package-less clone, status draft, armed (double-fire), listed on ~org. Actual: as above, matching packages/runtime/src/flow-clone.ts (FLOW_CLONE_STATUS, FLOW_CLONE_NOTICE) and resolveCloneTarget. Reproduced with two clones over two processes.

R-2 · studio-authoring.packaged-automation-studio-lock · A0 (New half) / A2 · stale spec (ADR-0146 D3 #28572 "New lands in the pair"; D4 #28148 "the ledger switch, on the item")

  1. Form sign-in as admin; GET /api/v1/packages/com.example.showcase → writable:false.
  2. /_console/studio/com.example.showcase/automations?surface=flow%3Ashowcase_urgent_task_alert (rail, "Read-only" badge, header switch "On for this deployment").
  3. [data-testid=studio-rail-add] ("New automation") is present and opens the New dialog, which targets the .custom pair.
  4. [role=switch][data-testid=auto-ledger-switch] is enabled; clicking it posts POST /api/v1/automation/showcase_urgent_task_alert/toggle {"enabled":false} → 200, then {"enabled":true} → 200. The flow definition stays byte-identical (GET /api/v1/meta/flow md5 unchanged).
    Expected (clause r6): no New; the switch disabled with no request. Actual: as above, matching StudioDesignSurface.tsx (newInPair, ledgerRow → FlowLedgerSwitch). Reproduced on 2 fresh loads each. The lock itself holds (A1, A3, N0, N1 pass: forged writes 403 ITEM_LOCKED / NOT_OVERRIDABLE, inspector inputs disabled).

R-3 · studio-authoring.access-permission-set-lifecycle · A3 · stale spec (ADR-0146 D3, #28572)

  1. Form sign-in → /_console/studio/com.example.showcase/access?surface=permission:showcase_contributor.
  2. getByTestId('studio-rail-add') ("New permission set") is present and enabled beside the "Read-only" chip.
  3. Click → "New permission set" → "QR7 Pair Set" → Save as draft → POST /api/v1/packages/com.example.showcase/ensure-customization 201 {"packageId":"com.example.showcase.custom","created":true}, then PUT /api/v1/meta/permission/qr7_pair_set?mode=draft&package=com.example.showcase.custom&surface=studio 200.
    Expected (clause): a read-only package offers no New set. Actual: New lands in the pair, never in the base; the packaged set's own controls are all disabled (192 matrix checkboxes) and forged writes are refused (A4 passes). Reproduced on 2 fresh loads.

R-4 · studio-authoring.draft-publish-lifecycle · A10 · stale spec (#28355)

  1. As admin: PUT /api/v1/meta/dashboard/qa_lifecycle_probe?mode=draft&package=PKG with a widget carrying a stray key ("object":"showcase_task") → 422 INVALID_METADATA widgets.0 [unrecognized_keys].
  2. GET /api/v1/meta/dashboard/qa_lifecycle_probe/audit.
    Expected (clause r10): a 422 spec refusal writes no row. Actual: one row per refusal, {"source":"protocol.saveMetaItem","operation":"save","outcome":"denied","code":"invalid_metadata","actor":"ADMIN_ID",…}, by design of packages/metadata-protocol/src/protocol-refusal-audit.ts (REFUSAL_AUDIT_CODES = NOT_OVERRIDABLE, NOT_CREATABLE, INVALID_METADATA, DESTRUCTIVE_CHANGE). A NO_DRAFT 404 still writes none, and every row carries the authenticated actor. Reproduced 2 of 2 with two different bodies.

R-5 · studio-authoring.expression-editors · A2 / A4 / A6 / N0 · stale spec (#26858 ruling "no client copy of any publish rule"; #27612's shipped design; re-point waits on #27482)

  1. POST /api/v1/packages {"manifest":{"id":"com.qa.rsx","name":"QA R7 Expr","version":"0.1.0","type":"app","namespace":"qrx"}}.
  2. PUT /api/v1/meta/object/qrx_job?mode=draft&package=com.qa.rsx (sharingModel public_read_write; number est_hours; select status; formula f_formula = record.est_hours * 1.1); publish-drafts.
  3. PUT /api/v1/meta/permission/qrx_job_editor?mode=draft&package=com.qa.rsx with objects.qrx_job read/edit and one select RLS policy using: "record.status == 'open'"; publish.
  4. Form sign-in → /_console/studio/com.qa.rsx/access?surface=permission:qrx_job_editor → Row-level security → USING (read filter) = record.f_formula > 5.
    Expected (clause as written): an error-tier lint and the save held, no PUT. Actual: "Valid CEL" for about 1.7 s, then PUT …/qrx_job_editor?mode=draft&package=com.qa.rsx&surface=studio 200, then the server's INVALID_FIELD verdict at the field; publish → 422 rls-predicate-unenforceable. Each newly refused text autosaves once more; an edit to another clause is held. Non-pushdownable and parse-fault predicates are held with 0 PUTs. Reproduced on 3 fresh loads. This is the residual Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482 describes; the item's own knownGaps[3] says to score these fail until Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482 is ruled.

R-6 · studio-authoring.dashboard-widget-and-filter-types · A4 · product defect (extracted)

  1. A writable package with an app whose navigation names a dashboard with one metric widget (here com.qa.r3, dashboard r3_hold on dataset r3_ticket_metrics).
  2. /_console/studio/PKG/interfaces → the dashboard → Properties → Filters → Global Filters → "Add item" (0 PUTs, held as designed).
  3. Fill Field (region), set Input Type = select (each PUT /api/v1/meta/dashboard/r3_hold?mode=draft&package=PKG&surface=studio 200).
  4. Type r3_ticket into Options From → Object (#mdf-globalFilters.N.optionsFrom.object) and Tab → the PUT carries "optionsFrom":{"object":"r3_ticket"} → 422 INVALID_METADATA, globalFilters.N.optionsFrom.valueField [invalid_type]; …labelField [invalid_type].
  5. Edit the dashboard Description, then its Label → each PUT carries the same incomplete item and is refused 422; the banner reads "Changes not saved: the server refused this draft…".
  6. Reload → no leave prompt (new Event('beforeunload',{cancelable:true}) → defaultPrevented false); the Description and Label edits are gone.
    Expected (clause): the editor never autosaves an incomplete filter and no other edit is lost. Actual: as above. Reproduced 3 times on fresh loads. The Studio: "Add item" on a dashboard list property autosaves an empty item (422), and every later edit is refused and lost until reload #27459 shape (an empty Add item) holds. Suspected source: listItemHeldInput() in ui/packages/app-shell/src/views/studio-design/metadataError.ts holds an item only when the unset input sits directly below the item.

R-7 · studio-authoring.no-entry-property-matrix · A1 / A2 / N0 · product defect (extracted, one card per key)
Each row: author the key on a writable package (PUT /api/v1/meta/TYPE/NAME?mode=draft&package=app.qar8 → POST /api/v1/packages/app.qar8/publish-drafts → published; the live read carries the key verbatim), then read its runtime effect.

  • object.titleFormat '{code} @ {city}' on qar8_tf2 with no authored nameField: the served object carries a stamped nameField:"code"; record {code:'ZX-42', city:'Lyon'} → detail H1 and list primary read "ZX-42", not "ZX-42 @ Lyon". 3 repros.
  • field.searchable true on a code field and an html field of qar8_fsrch2: GET /api/v1/data/qar8_fsrch2?$search=QQCODE and ?$search=QQHTML → []; ?$search=QQCODE&$searchFields=snippet → 400 INVALID_FIELD "Field 'snippet' on object 'qar8_fsrch2' is not searchable…". A field with searchable absent is scanned exactly like one with true. 3 repros.
  • view.list sharing: {lockedBy: 'admin@objectos.ai'} (no type) on qar8_vl.v_sharing: the view opened from the switcher shows no lock indicator and no "Locked by". 3 repros. Mechanism (orchestrator, from source): ui/packages/plugin-list/src/ListView.tsx renders the indicator only when schema.sharing.type is set, while ViewSharingSchema.type defaults to collaborative in the spec.
  • view.form drawerSide:'left', drawerWidth:'480px' on qar8_fdr.form: New and /_console/forms/qar8_fdr.form render exactly as the control (no drawer); a bad enum is refused 422, so the key is validated and never read. 2 repros. The spec describe says the drawer half "stays open on spec(ui): FormViewSchema declares drawerSide / drawerWidth and buttons.reset, but no console entry point renders a drawer form or a reset button; implement or retire them (ADR-0049) #27153", which is closed.
  • dataset dimension nullLabel:'No region (R8)' with report qar8_report (summary, rows region): /_console/apps/com.example.showcase/report/qar8_report shows the null row as "—"; the page's own POST /api/v1/analytics/dataset/query answer carries fields.region.nullLabel "No region (R8)". 3 repros.
    Expected (clause): every key has its declared runtime effect, or is refused. Actual: as above. The four knownGaps[1] keys (mailAssociation, ackPlaintextMasking, lifecycle.ttl, dimensions) were left unjudged as the item says. Every other row of the matrix passed; see the clause comments.

Fixture gaps, partial coverage and handover

  • No item is partial, blocked or not-run. studio-authoring.app-and-nav-properties A5 leaves its requiresObject cell unjudged as its knownGaps[0] says; the clause's other keys passed. studio-authoring.expression-editors A7 is skipped (pin-only, run by W4-C2b).
  • studio-authoring.view-authoring-live: end-user surfaces were scored as the admin, which the item allows (no non-admin grant on a runtime-authored object).
  • records-forms.field-group-visible-when: the qa_group_gate recipe as written is refused 422 security-owd-unset (no sharingModel); provisioned with sharingModel: 'public_read_write' added.
  • records-forms.calendar-interactions: the week/day time-grid move and the resize-end variants cannot be exercised on the stock fixture (date-only start field, no end field).
  • dashboards.global-filters-rescope D2: the field-override combo offers only the dataset's declared dimensions, so the dateRange re-target needs a dataset with two date dimensions; region → status was re-targeted instead.
  • records-forms.action-location-matrix A2: open_docs targets https://objectstack.ai, which this container's proxy refuses; the url leg is scored on the issued navigation.
  • The checklist-accuracy findings (about 45 lines) go to the anchor [sweep] v18.3.0 release test sweep #27377 in the final report, not here.

Off-clause probes (RUNNER rule 8)

About 67 minutes in total across 11 runners, on the same boots and as the same personas (admin, demo personas, minted members; a walled group boot for one approvals probe).

Doors probed: approvals decision, action-route, reassign, recall, remind and comment doors; automation runs, trigger, resume, clone and toggle; the actions dispatcher; metadata save, draft read, history, audit, diff, rollback and publish-drafts; analytics dataset query; share-links and export; the named form page; dashboard and report save; flow publish and registration; book tree and doc read; the avatar menu.

Boundary classes tried: empty string, blank id, missing id, unknown id, wrong type, inverted or non-numeric versions, limit=0, partial nested objects, duplicate names, undeclared cycles, opposite posture (member on admin doors, draft vs active, walled vs single), and readers outside the asserted scope.

Findings that pass the filing gate are extracted as cards and listed in the close-out comment. One more:

  • approvals.per-group-signoff · observed beside the clauses · security finding — detail withheld, reported to PM

Observations not filed, judged below the filing gate or by design:

  • POST /api/v1/actions/sys_approval_request/approval_approve with recordId missing → 404 ENDPOINT_NOT_FOUND rather than 400.
  • POST …/requests/areq_unknown/approve → 404 whose error text is the bare id, while the GET says "Approval request 'areq_unknown' not found".
  • every POST/PUT/DELETE on /automation logs "[Registry] Overwriting flow" once per org-scope flow.
  • GET …/history?limit=0 → 200 empty; /diff?from=abc → 400 VALIDATION_FAILED while /rollback {"toVersion":"abc"} → 400 INVALID_REQUEST; /history and /audit on an unknown name → 200 empty while /diff → 404.
  • the ITEM_LOCKED remedy sentence still names the clone route while NOT_OVERRIDABLE names the pair; both are true remedies.
  • RLS "Add tab" autosaves an empty-key row tabPermissions: {"": "visible"}, which save and publish accept.
  • /_console/studio/PKG/access?surface=permission:does_not_exist opens the first set with no notice.
  • duplicate widget ids on a dashboard are accepted; the runtime draws both tiles correctly.
  • a form view for a nonexistent object saves and publishes 200 and renders nowhere.
  • a summary rolling up a nonexistent child object saves valid and stays null (dangling references are tolerated as for lookups).
  • an app whose navigation holds only doc entries opens on a blank main region.
  • field.referenceVia in the header highlight strip shows the raw id text; the details-grid placement renders the card.
  • a Studio RLS CHECK on a select policy reads "Valid CEL" beside a 422 that names it: this is Studio RLS editor: a CHECK on a select policy (or an empty USING) reads "Valid CEL", then the autosave is refused 422 and the banner shows only the path code, not the server's explanation #28664, fixed by PR fix(app-shell): the Studio RLS editor stops offering CHECK on a select or delete policy, and a refused save shows the server's explanation #28884, which merged at 03:29Z, after this subject.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

    Per-clause verdicts, part 1 of 3 (text oracle evidence)

    Continuation of this record. One line per clause, A then N, in the scope order of the record body. Runner tag in the record fingerprint. Double braces inside quoted templates are spaced apart so the write door does not read them as tokens.

    approvals.per-group-signoff r11 — PASS

    • A0 pass: GET /api/v1/approvals/requests/areq_410f30da… (EXP-2001, flow:showcase_expense_signoff, submitter usr_showcase_phone_demo) → status pending; pending_approvers [sWrqdlPd…(admin), usr_showcase_auditor_demo]; pending_approver_groups {admin:["manager"], Ada:["finance"]}; decision_progress per_group got 0/need 2, finance 0/1, manager 0/1.
    • A1 pass: after the admin's dialog approve → status pending; pending_approvers ["usr_showcase_auditor_demo"]; groups {Ada:["finance"]}; progress got 1/2, manager got 1/1 satisfied:true, finance 0/1.
    • A2 pass: stage 0 page: "Sign-off progress — 0 of 2 groups · finance 0/1 · manager 0/1", chips ["Dev Admin · manager","Ada Auditor (demo) · finance"] = API; stage 1 page (admin after approve, and Ada's fresh load): "1 of 2 groups · finance 0/1 · manager 1/1", chips ["Ada Auditor (demo) · finance"] = API; decided (fresh load from All tab): panel shows no progress line and no chips (status chip "Approved"), and the read carries neither decision_progress nor pending_approver_groups.
    • A3 pass: after Ada's approve → request status approved, pending_approvers []; GET /api/v1/automation/showcase_expense_signoff/runs/run_3a918a69… went paused (steps start > dual_signoff) → completed (start > dual_signoff > mark_approved:update_record:success); EXP-2001 status "approved"; no return_to_draft step.
    • A4 pass: GET /:id/actions → submit (Mei) + approve sWrqdlPd… "R1a manager approve via dialog" + approve usr_showcase_auditor_demo "R1a finance approve by Ada via dialog" — two approve rows, distinct actors, both dialog comments round-tripped, via_override false on both.
    • A5 pass: admin POST /api/v1/approvals/requests/areq_410f30da…/approve {actorId:"usr_showcase_auditor_demo"} → 403 FORBIDDEN "cannot act as 'usr_showcase_auditor_demo' — an approval action is recorded against the authenticated caller", /actions unchanged (1 row); with no actorId (dialog) → 200 and row actor = admin; with own actorId (invoice, N0) → 200 and row actor = admin. Note: the dialog's real POST is /api/v1/actions/sys_approval_request/approval_approve {recordId, params:{comment} }, not the /approvals/requests/:id/approve route step [4] names (accuracy finding below).
    • N0 pass: seeded unanimous invoice request areq_44157063… (INV-1008) slate [admin, admin], progress unanimous got 0/need 1; one admin approve → status approved, one approve row (via_override false); per_group EXP-2001 stayed pending after its first approve (A1) — the contrast holds.
    • N1 pass: qa.none (positions org_member/everyone; in neither group) POST …/areq_410f30da…/approve → 403 {"code":"FORBIDDEN","error":"actor 'x5J16y7j…' is not a pending approver"}; /actions unchanged; that persona's GET of the request → 404 REQUEST_NOT_FOUND.

    approvals.quorum-m-of-n r9 — PASS

    Verdict rests on the item's RUNTIME FIXTURE (fixtures.requires[1]), applied on this fresh boot: POST /api/v1/data/sys_user_position {user_id:usr_showcase_auditor_demo, position:finance, organization_id:org_mvyq5oga23juzfjh} → 201; same for qa.legal (KIHGwGl3…) position legal → 201 (pre-list: finance/legal/manager held only by the admin, nothing to unbind); POST /api/v1/automation/showcase_expense_signoff/toggle {enabled:false} → 200 (re-enabled {enabled:true} → 200 after the quorum steps); fresh draft reports QA-R1A-Q1 / QA-R1A-Q2 + one 6000 showcase_expense_line each (total_amount 6000) → PATCH status submitted → showcase_committee_quorum requests areq_68f7fea9… / areq_e58d1503…, slate [admin, Ada, admin, qa.legal, admin] = 3 distinct users. The admin is submitter and third holder (as the fixture says) and never decided either fresh request.

    • A0 pass: clamp CONTRAST only (stock EXP-DEMO areq_9df1b64d…): pending_approvers [admin, admin, admin], decision_progress quorum got 0/need 1; one admin approve → status approved; /actions = submit + exactly one approve (admin, via_override false).
    • A1 pass: QA-R1A-Q1: initial got 0/need 2 → Ada POST …/approve → 200, status pending, got 1/need 2, pending_approvers [admin, qa.legal] (the two remaining holders; Ada dropped) → qa.legal approves via the request page dialog → status approved; /actions = 2 approve rows, actors usr_showcase_auditor_demo + KIHGwGl3… (distinct), via_override false both.
    • A2 pass: QA-R1A-Q2: Ada approve → pending 1/2 (run paused, steps start > committee) → qa.legal POST …/reject → status rejected, pending_approvers [], sys_approval_approver rows 0; later approves by admin and qa.legal → 409 {"code":"INVALID_STATE","error":"request is rejected"}; run paused → completed, steps start > committee > return_to_draft:update_record:success; report QkHDnteH… status "draft".
    • A3 pass: after quorum on Q1 the request lists pending_approvers [] (admin, approver 3, no longer listed), its sys_approval_approver index rows for the request = 0, the admin's GET ?status=pending = [] and the inbox "My Pending" badge went 1 → none (TABS ["My Pending\n1",…] → ["My Pending",…]).
    • A4 pass: request page (opened from the inbox row) at each pending stage = API: stage 0 "Approvals — 0 of 2 · 3 eligible approvers", chips ["Dev Admin · holds 3 approver slots","Ada Auditor (demo)","QA Legal R1a"] vs read got 0/need 2, 3 distinct ids; stage 1 (screenshot confirmed) "Approvals — 1 of 2 · 2 eligible approvers", chips [Dev Admin, QA Legal R1a] vs read got 1/need 2, [admin, qa.legal]; after the final decision (fresh load, All tab) no tally line and no chips, and the read carries no decision_progress. The step text "Committee Sign-off (2 of 3)" was not read as the tally.
    • N0 pass: Ada (plain member holder) repeating her approve on Q1 → 403 {"code":"FORBIDDEN","error":"actor 'usr_showcase_auditor_demo' is not a pending approver"}, tally stayed got 1/need 2, /actions unchanged; a holder of none of the positions (qa.none) → 403 same shape. No platform admin used for this probe.
    • N1 pass: the EXP-DEMO clamp is recorded above only as contrast (A0); the M-of-N clauses A1–A4 rest on the runtime fixture, not on stock seeds.

    approvals.inbox-metadata-actions r8 — PASS

    Personas proven by GET /api/v1/auth/get-session: admin@objectos.ai (hVpCVmor…, org_mvyrb68z…), auditor.demo@example.com (usr_showcase_auditor_demo, same org). Entry path: form sign-in → /_console/apps/com.objectstack.account/component/approvals/inbox → tab → click the Request cell (cell 1 on My Pending after the checkbox; cell 0 on Submitted by me / All) → served page /_console/apps/com.objectstack.account/sys_approval_request/record/‹id›; screenshot confirmed render before every DOM read.

    • A0 pass: Ada (viewer {can_act:true,is_submitter:false,can_override:false}) on EXP-2001 (submitter Mei Phone) via My Pending → Request cell "Expense Report Sign-off" → request page buttons = [Approve, Reject, Reassign, Send back, Request info, Reply]; matches the declared approval_approve/reject/reassign/send_back/request_info/comment set whose visible admits can_act (sys-approval-request.object.ts); "…" menu holds only Refresh/Add to favorites/Share.
    • A1 pass: own request INV-1006 (admin, Submitted by me → Request cell) shows "Send reminder" AND "Recall" ({can_act:true,is_submitter:true,can_override:true}); Ada on EXP-2001 shows neither; dev admin on the SAME pending EXP-2001 shows Recall and no Send reminder — labelled "Recall" while the admin still held its manager slot ({can_act:true,can_override:true}), and "Override Recall" (with "Override Approve/Reject/Reassign", no Send reminder) once the admin had signed its slot and was override-only ({can_act:false,is_submitter:false,can_override:true}, request still pending on Ada). See accuracy finding AF-1 on the label.
    • A2 pass: GET /api/v1/approvals/requests/:id flags predicted every rendered set exactly: Ada/EXP-2001 {T,F,F} → 6 approver buttons, no remind/recall; admin/EXP-2001 {T,F,T} → 6 + Recall (override arm); admin/EXP-DEMO {T,F,T} → same 7; admin/INV-1006 {T,T,T,pending} → 8 (all but Resubmit, which needs status returned); admin/EXP-2001 override-only {F,F,T} → Override Approve/Reject/Reassign/Recall only (no Send back/Request info/Reply, which need can_act). Ada's GET of INV-1006 / EXP-DEMO → 404 (not hers).
    • A3 pass: Approve click (Ada, EXP-2001 page → dialog Confirm) → browser POST /api/v1/actions/sys_approval_request/approval_approve {"recordId":"areq_72d3…","params":{"comment":…} } → 200 {"success":true,"data":{"request":{…} }}, forwarded in-process to the declared target POST /api/v1/approvals/requests/{id}/approve (runtime action-execution.ts forwardApiAction); re-read → status approved, viewer can_act false, page re-renders with no decision buttons. Also admin UI approve of EXP-DEMO → re-read status approved. Send reminder click (admin, INV-1006) → POST /api/v1/actions/sys_approval_request/approval_remind → 200 (forwarded to …/requests/{id}/remind); re-read status=pending. See AF-2 on the verify's literal path.
    • A4 pass: GET /:id/actions before→after: EXP-DEMO [submit(Mei)] → +1 approve hVpCVmor… via_override=false "R1b UI spot-check Approve"; INV-1006 [submit(admin)] → +1 remind hVpCVmor… "R1b UI spot-check Send reminder"; EXP-2001 2 rows → +1 approve usr_showcase_auditor_demo. Exactly one row per action, actor named.
    • N0 pass: no viewer whose flags deny a decision saw one: Ada after her approve (can_act false, can_override false) → page buttons [Details, Timeline] only; the override-only admin (can_act false) saw decisions only under the "Override …" framing its can_override flag admits; Ada never saw Recall/Send reminder.

    approvals.viewer-gating-submitter-side r7 — PASS

    • A0 pass: as Mei (form sign-in), account app Approvals → "Submitted by me" tab lists EXP-2001 + EXP-DEMO; row click → /_console/apps/com.objectstack.account/sys_approval_request/record/areq_410f30da…; screenshot confirmed render; decision panel buttons = ["Send reminder","Recall","Reply"] — no Approve/Reject/Reassign/Send back/Request info (admin on the same page, same moment: ["Approve","Reject","Reassign","Send back","Request info","Recall","Reply"]).
    • A1 pass: GET /api/v1/approvals/requests/areq_410f30da… as Mei → 200 viewer {"can_act":false,"is_submitter":true,"can_override":false}; pending_approvers [admin, usr_showcase_auditor_demo] — Mei's id absent.
    • A2 pass: Mei POST /api/v1/approvals/requests/areq_410f30da…/approve {comment} → 403 {"code":"FORBIDDEN","error":"actor 'usr_showcase_phone_demo' is not a pending approver"} (with actorId=admin → 403 "cannot act as 'sWrqdlPd…' — an approval action is recorded against the authenticated caller"); other side: the entitled admin's dialog approve on the same request → 200 and an approve row (actor admin), then Ada's → 200, request approved.
    • A3 pass: request read and /actions read, as Mei and as admin, taken before and after both forged calls → JSON byte-identical (4/4 comparisons true); actions = 1 row (submit by Mei) before and after; decision_progress got 0/need 2 unchanged.
    • N0 pass: neither forged submitter call answered 2xx, appended an action row, or moved the tally (see A2/A3).

    approvals.decision-action-matrix r10 — PASS

    Boot B2 (A0–A8, N0–N2) and B3 (A9). Personas proven by get-session on B2: admin@objectos.ai (sPoyRDLE…), auditor.demo (usr_showcase_auditor_demo), phone.demo (usr_showcase_phone_demo), all org_mvyrl3hg…. Budget requests raised by PATCH showcase_project budget (admin = submitter AND manager/exec slate holder).

    • A0 pass: one route, one row each — approve POST …/areq_b844…(EXP-DEMO)/approve→200, actions [submit]→[submit, approve:admin]; reject (Mobile App 210000) →200, +reject:admin; revise →200, +revise:admin; resubmit →200, +resubmit:admin; recall (Legacy Sunset 300000) →200, +recall:admin; reassign →200, +reassign:admin; remind (INV-1010) →200, +remind:admin; request-info as Ada (reassign target) →200, +request_info:usr_showcase_auditor_demo; comment as admin →200, +comment:admin. Third revise (maxRevisions) → revise + reject("Auto-rejected: revision limit (2) exceeded"), the designed pair.
    • A1 pass: EXP-DEMO pending→approved, run run_7f88… completed, steps [start, committee, mark_approved]; budget reject → status rejected, run run_a549… completed, steps [start, manager_review] (no needs_exec); budget approve (Website Relaunch 160000) → approved, run run_78a1… completed, steps [start, manager_review, needs_exec, exec_review(skipped by e4 'true')]; exec leg: run run_b227… read paused at exec_review before the exec approve and completed after.
    • A2 pass: revise on R1 (Compliance Audit 120000) → 200, GET status=returned; GET /api/v1/automation/showcase_budget_approval/runs/run_0b8f… → status=paused, last step wait_revision.
    • A3 pass: Ada resubmit R1 → 403 FORBIDDEN "only the submitter may resubmit" (no row); admin resubmit → 200; pending list for the record goes [] → [areq_8de1…(R2), status pending, same flow_run_id run_0b8f…, flow_node_id manager_review, pending_approvers [admin] freshly resolved].
    • A4 pass: GET R1 after R2 opened → status=returned (never pending); final sibling list on run_0b8f…/manager_review = R1 returned, R2 returned, R3 rejected.
    • A5 pass: after two revise/resubmit rounds, revise on R3 (areq_756a…) → 200 with request.status "rejected"; GET R3 → rejected; run completed.
    • A6 pass: Ada (pending approver, non-submitter, non-override) recall on EXP-2001 → 403 FORBIDDEN "Only the person who submitted this approval request can recall it…", status unchanged, no row; admin (submitter) recall → 200, GET status=recalled; GET /approvals/requests?status=pending&approverId=‹admin› (the My Pending query) contains R before = true, after = false.
    • A7 pass: reassign admin→Ada → pending_approvers ['sPoyRDLE…'] → ['usr_showcase_auditor_demo'] (admin's viewer.can_act true→false, Ada's My Pending lists it); request-info / comment / remind → status pending and the whole GET body byte-identical before/after (changed keys = []), exactly one new timeline row each.
    • A8 pass: Data Platform 700000 at manager_review (lockRecord false): PATCH /api/v1/data/showcase_project/4brU… {name} → 200; after manager approve, at exec_review (lockRecord true): same PATCH → 409 RECORD_LOCKED "…is locked while an approval is in progress…", name unchanged.
    • A9 pass (live, on the group-posture boot B3 — not blocked(fixture)): recipe replayed literally (org create 200 → B xnxUsvV0…, set-active B, account 201 org=B, project 201 org=B, PATCH budget 200000 → R areq_5c58… org=B, slate = B's admin via admin_rescue comment row); set-active A → GET R 404 REQUEST_NOT_FOUND; POST /api/v1/approvals/requests/R/approve {comment} → 500 {"code":"READ_BACK_FAILED","error":"the write to approval request '…' was recorded, but the updated row is not visible inside the caller's organization scope… The write is NOT rolled back — read the request back with a system or matching-organization context."}; set-active B → GET R 200 status=approved, pending_approvers []; actions [submit, comment, approve(admin, via_override=false)]. Admitted as R's pending approver (knownGaps note), not via the override arm.
    • N0 pass: revise at exec_review → 400 VALIDATION_FAILED "approval node 'exec_review' has no 'revise' out-edge — the flow does not support send-back for revision"; status stays pending, no row.
    • N1 pass: second remind on INV-1010 immediately → 429 THROTTLED "a reminder was already sent recently"; actions count unchanged (2).
    • N2 pass: wrong-relationship actors all refused with no row and no state change — Mei revise R1 → 403 "not a pending approver"; Ada resubmit R1 → 403; Ada recall EXP-2001 → 403; Mei reassign → 403; admin request-info after reassigning its slot away → 403 "actor '…' is not a pending approver"; Mei approve at exec_review → 403.
    • Off-clause card approvals: GET /api/v1/approvals/requests/UNKNOWN/actions answers 200 [] while GET /requests/UNKNOWN answers 404 #27451 holds: GET /api/v1/approvals/requests/areq_does-not-exist/actions → 404 REQUEST_NOT_FOUND (also for ' ' and for a real request the caller cannot read).

    automation.flow-run-step-nesting r8 — PASS

    • A0 pass: POST /api/v1/automation/showcase_batch_reminders/trigger {params:{tasks:[3 rows with id/title/owner]} } → 200; run run_2e7334be status completed; steps with nodeId=send_reminder = 3.
    • A1 pass: run_2e7334be steps: start, loop_tasks (no parentNodeId), guard_reminder ×3 {parentNodeId:loop_tasks, regionKind:loop-body, iteration 0/1/2}, each followed by send_reminder {parentNodeId:guard_reminder, regionKind:try, iteration} matching its guard (0/1/2); no end step logged.
    • A2 pass: showcase_nested_fan_out_reminders over 2 rows (id/title/owner/watcher) → run run_7ca0abcd completed; parallel-branch steps: notify_owner {iteration:0, branch:0}, notify_watcher {iteration:0, branch:1}, notify_owner {iteration:1, branch:0}, notify_watcher {iteration:1, branch:1} — all four (iteration, branch) pairs exactly once, branch:0 present; containers fan_out_audiences {parentNodeId:each_task, regionKind:loop-body, iteration:0|1} with no branch key. All 8 steps (and runs run_2e7334be, run_a185ed06, run_ef916949) parse under ExecutionStepLogSchema from packages/spec/dist (0 parse failures).
    • A3 pass: Studio /_console/studio/com.example.showcase/automations?surface=flow:showcase_batch_reminders › Runs tab (FlowRunsPanel), expanded run_2e7334be; screenshot shows "For each task LOOP" with indented "↳ ITERATION 1 / 2 / 3" headers, under each "Guarded iteration TRY_CATCH", under that "↳ TRY" and "Send Reminder NOTIFY" indented a further level — a nested tree, 1-based labels.
    • A4 pass: panel renders 8 step rows (Start, loop, 3× try_catch, 3× notify) = API's 8 steps; grouping Iteration 1/2/3 ↔ iteration 0/1/2; Try group under each guard ↔ parentNodeId guard_reminder/regionKind try.
    • A5 pass: trigger {params:{tasks:[]} } → 200; run run_a185ed06 status completed, steps start:success | loop_tasks:loop:success, 0 steps with parentNodeId=loop_tasks; the panel shows the same two rows.
    • N0 pass: the designer Runs panel (not the developer Flow Runs page) rendered the run nested (A3); no flat rendering seen.

    automation.rollup-summary-filter r7 — PASS

    • A0 pass: GET /api/v1/data/showcase_expense_report/tKfw43-rXmwUOHO0 (EXP-2001) → total_amount 1500.5, approved_amount 960, reimbursable_amount 512, line_count 4, rejected_count 0, over_limit_count 2; independent recompute over its 4 showcase_expense_line rows (filter expense_report=id: 28.5 submitted/!billable, 340 approved/!billable, 512 submitted/billable, 620 approved/!billable) → identical six values.
    • A1 pass: PATCH …/showcase_expense_line/UIhz5WO_NTauIt4_ {status:rejected} → 200; parent approved_amount 960→620, rejected_count 0→1 (both = recompute); total 1500.5, line_count 4, reimbursable 512, over_limit 2 unchanged.
    • A2 pass: PATCH …/XUGloZDyGe7uAsm6 {billable:false} → 200; reimbursable_amount 512→0 (= recompute); total 1500.5, approved 620, line_count 4, rejected 1, over_limit 2 unchanged.
    • A3 pass: DELETE …/evjR3SAPJ89lFf8V (620, approved, !billable) → 200; total 1500.5→880.5, line_count 4→3, approved 620→0, over_limit 2→1 (its predicates matched); reimbursable 0 and rejected 1 unchanged; all six = recompute.
    • A4 pass: POST /api/v1/packages {manifest:{id:com.qa.rollup,…,scope:project,type:app} } → 201 namespace "rollup", writable:true; PUT /meta/object/rollup_parent + rollup_child ?package=com.qa.rollup → 200 (open_total summary sum(amount) filter {status:open}); Studio /_console/studio/com.qa.rollup/data › Rollup Parent › Form › Open Total › Properties › "Filter (rows to aggregate)" rendered Field "Status", Operator "= equals", Value "open" (screenshot-confirmed); filled Value "closed" → PUT /meta/object/rollup_parent?mode=draft&package=com.qa.rollup&surface=studio 200; Publish dialog "~ Open Total open_total · Summary Operations" → Publish all → POST /packages/com.qa.rollup/publish-drafts 200 "Published 1 item in this package"; GET /api/v1/meta/object/rollup_parent → summaryOperations.filter {"status":"closed"}; sys_metadata row package_id com.qa.rollup state active filter {"status":"closed"}; live: children 10 open + 20 closed + 5 closed → open_total 25. Contrast: POST /packages/com.example.showcase/duplicate {targetPackageId} → 422 DUPLICATE_SOURCE_NOT_A_BASE, no com.qa.dup row (404).
    • N0 pass: on the billable flip rejected_count stayed 1 and over_limit_count 2; on the status flip reimbursable stayed 512; no untouched-filter roll-up moved on any edit.

    automation.flow-node-type-matrix r12 — PASS

    • A0 pass: GET /api/v1/automation/actions → 200, 20 descriptors decision,assignment,loop,parallel,try_catch,get_record,create_record,update_record,delete_record,screen,script,http,connector_action,notify,save_notice,wait,subflow,map,approval_revise,approval — every variant but start/end, none extra. Palette: cloned showcase_task_completed → qa_r2a_palette (POST /automation/…/clone {name,label} → 200; without label → 400 VALIDATION_FAILED as steps[1] says), opened in Studio, clicked "Add node" after the canvas screenshot confirmed render; the popover lists Create Record, Update Records, Get Records, Delete Records, Decision, Loop, Assignment, Parallel, Try / Catch, Map, Approval, Screen, HTTP, Notify, Connector Action, Script, Subflow, Wait, Save notice, Revise Window (= the 20) plus End (the base palette's structural item).
    • A1 pass: every one of the 21 step-logging variants located on a run (run id · node · status): start run_80071a07 start success · decision run_0c8cc981 (showcase_inquiry_purge) any_found success · assignment run_4ae45609 (showcase_one_task_signoff, the map child) mark_ok success and run_e3eef03b (showcase_closure_signoff) mark_approved success · loop run_2e7334be loop_tasks success · create_record run_a72e373b (inbound webhook, signed t=…,v1=… → 202) create_task success, task XSSyp8gTJetZ7Nrm created · update_record run_c2bbf836 (showcase_reassign_wizard) apply success · delete_record run_0c8cc981 purge success (closed inquiries gone: remaining statuses new,contacted) · get_record run_0c8cc981 purge_check success · http run_1a44faeb (scratch self-ping qa_r2a_http_ping, GET http://127.0.0.1:41330/api/v1/health) ping success · notify run_80071a07 notify success · save_notice run_80071a07 tell_saver success, and the PATCH answer carried notice: "Marked R2a chain task … done; its assignee is being notified" · script run_80071a07 summarize success · screen run_c2bbf836 collect success · wait run_327dcbfe hold success · subflow run_2037c80d (showcase_task_done_notify_owner) call_notify success, child showcase_notify_owner run_79dd0f3c completed · map run_ca21f358 signoffs success · connector_action run_5e0e7744 / run_86708114 / run_a70be9a4 ping/ping/echo success · parallel run_7ca0abcd (showcase_nested_fan_out_reminders) fan_out_audiences success ×2 and run_bb27dbae (showcase_project_escalation) alert success · try_catch run_ef916949 guarded_push success · approval run_6c88cfc5 (seeded EXP-2001) dual_signoff success · approval_revise run_3872c091 wait_revision success. end: run_80071a07 status completed with no end step logged (taken end logs none); the end:skipped row of run_0c8cc981 is not cited for end. showcase_fan_out_notify run_1d861105 failed only at slack_post http: fetch failed (no egress, knownGaps — parallel and http proven on the runs above instead).
    • A2 pass: screen run_c2bbf836 paused (collect:screen) → POST …/runs/:id/resume {inputs:{new_assignee} } 200 → completed, task 5E0h8xnfNJhS69th assignee rewritten (the browser leg, Tasks row "Open menu" › "Reassign…" › Submit, also completed run_4eb88166 and wrote assignee auditor.demo@example.com) · wait run_327dcbfe paused at hold → completed 60.04 s later (startedAt 04:41:27.435, completedAt 04:42:27.475) · approval run_6c88cfc5 paused → admin approve + auditor.demo approve (both 200) → completed with mark_approved · approval_revise run_3872c091 paused at manager_review → revise 200 → paused at wait_revision → resubmit 200 → paused at manager_review (round 2) → approve 200 → completed · map run_ca21f358 paused → approve item 1 → still paused → approve item 2 → completed with notify_done · subflow (child pauses) showcase_project_closure run_993231fa paused at signoff with child run_e3eef03b paused at ask_signoff → approve → both completed.
    • A3 pass: sweep of every run of all 36 registered flows (36 runs, 123 steps) → 0 steps with an empty/missing nodeType.
    • A4 pass: loop body run_2e7334be guard_reminder {parentNodeId:loop_tasks, regionKind:loop-body, iteration:0..2}; parallel run_7ca0abcd notify_owner {parentNodeId:fan_out_audiences, regionKind:parallel-branch, iteration:0, branch:0}; try_catch run_ef916949 push {parentNodeId:guarded_push, regionKind:try, retryAttempt:0} and record_failure {regionKind:catch}.
    • A5 pass: (1) POST /api/v1/automation qa_r2a_bogus (start → bogus_node → end) → 200, read-back GET /api/v1/meta/flow/qa_r2a_bogus node types ["start","bogus_node","end"]; (2) boot.log WARN Flow 'qa_r2a_bogus' references node type(s) with no registered executor or descriptor — … {"unknownTypes":["bogus_node"],"knownTypes":[start,end,decision,assignment,loop,parallel,try_catch,get_record,create_record,update_record,delete_record,screen,script,http,connector_action,notify,save_notice,wait,subflow,map,approval_revise,approval]} (22 = the 20 descriptors + start/end; lists in meta, not message); (3) POST …/qa_r2a_bogus/trigger → 400 {"success":false,"error":{"code":"FLOW_FAILED","message":"No executor registered for node type 'bogus_node'"} }; (4) run run_4dd80a64 status failed, step weird {status:failure, error:{code:NO_EXECUTOR} }.
    • N0 pass: not silent on either side — the WARN carries the phrase and both meta lists, the trigger answers success:false, the run is failed with a NO_EXECUTOR step (A5 evidence). The 200 at registration was not scored as a finding.

    automation.flow-error-handling r6 — PASS

    • A0 pass: task Cl926VnMZnj1W9C_ (project J5pKxL6cb3v356bA, assignee admin@objectos.ai, in_review → PATCH done 200) fired showcase_resilient_sync run run_ef916949: status completed; step guarded_push (try_catch) status success.
    • A1 pass: step array index 2–5 = push (http) {parentNodeId:guarded_push, regionKind:try, status:failure, error:{code:NODE_FAILURE, message:"http: fetch failed"} }; index 6 = record_failure (update_record) {regionKind:catch, status:success} — every try failure precedes the catch step.
    • A2 pass: retryAttempt values on the try steps = 0,1,2,3 (4 = maxRetries+1), each failure; corroboration: attempt start times 04:41:31.971 / 32.996 / 35.006 / 39.013 (gaps ≈1 s, 2 s, 4 s), run durationMs 7208.
    • A3 pass: GET /api/v1/data/showcase_task/Cl926VnMZnj1W9C_ → sync_status:"failed", sync_error:"Node 'push' failed: http: fetch failed".
    • A4 pass: probe qa_r2a_unhandled (POST /api/v1/automation → 200, package-unbound/~org; start record-after-create on showcase_inquiry → http POST https://api.example.com/v1/tasks → end, no try_catch). Trigger-fired run run_f8d44fc6 (trigger record-after-create, fired by creating inquiry Y-le8oRPOQJIWIpL) status failed, error "Node 'push' failed: http: fetch failed"; the api-triggered run run_21805154 also failed with the same error (trigger answered 400 FLOW_FAILED).
    • A5 pass: Runs panel screenshots — showcase_resilient_sync run_ef916949: "Push with retry TRY_CATCH" with "↳ TRY" holding four "FAILURE Push to CRM HTTP … http: fetch failed" rows, then "↳ CATCH" holding "SUCCESS Flag Sync Failure UPDATE_RECORD"; ~org qa_r2a_unhandled run_f8d44fc6: "Failed" header, run-level red line "Node 'push' failed: http: fetch failed", step "FAILURE Push unprotected HTTP http: fetch failed".
    • A6 pass: boot.log 2026-11-02T04:47:02.464Z ERROR Trigger-fired run of flow 'qa_r2a_unhandled' failed (trigger 'record_change') — no caller holds this result and nothing retries the run; … {"error":"Node 'push' failed: http: fetch failed"}; the api-triggered failure 3 s later logged no ERROR line (correct per the clause).
    • N0 pass: the unhandled run is failed with a populated run-level error (A4), not completed.
    • N1 pass: successful-try runs show no catch steps — showcase_batch_reminders run_2e7334be: each guard_reminder's try (send_reminder success) has no reminder_failed (catch) step anywhere in the log.
    • N2 pass: the caught failure leaves the full trace — 4 regionKind=try failure steps with errors ahead of the catch step (A1/A2); not the [start, container, catch] shape.
    • N3 pass: the try_catch container step reading success after catching was not scored as a finding (by design, [Decision] A failed try_catch try-region produces no step at all — after a caught failure an operator cannot tell what failed, how many attempts ran, or which node threw #7546).

    automation.packaged-flow-clone-contract r10 — FAIL

    • A0 pass: POST /api/v1/automation/showcase_urgent_task_alert/clone {} → 400 VALIDATION_FAILED details.fields=[{field:name,code:required}] (one detail); {"name":"x"} → 400 [{field:label,code:required}]; {"label":"x"} → 400 [{field:name,code:required}]; {"name":"","label":"Empty"} → 400 [{field:name,code:invalid_type}]; none left a flow behind (GET qa_* → 404).
    • A1 fail (stale spec — Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149 / ADR-0146 D4): the clone RESPONSE data.flow diffs from the source on exactly name, label, status (+ the source's _provenance/_packageId/_packageVersion absent) and carries no clonedFrom; but status is obsolete, not the draft the verify pins, and the stored read GET /api/v1/automation/qa_urgent_alert_clone carries _packageId:"com.example.showcase.custom" (sys_metadata.package_id = com.example.showcase.custom) — the clause's "none of the 10 dropped keys present … a surviving _packageId defeats the feature (package-owned)" is the pre-Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149 package-less model; the copy is now package-owned BY RULING (lands in ‹base›.custom). Facets all present (description, version, type, nodes, edges, runAs deep-equal). See F-1.
    • A2 fail (stale spec — Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149 / ADR-0146 D4 "the copy is written inactive"): clone answered status:"obsolete"; across 3 urgent showcase_task creates with the clone as cloned, base runs 0→1→2→3 while clone runs stayed 0 (no double-fire); _status row qa_urgent_alert_clone enabled:false bound:false status:obsolete. The notice equals the CURRENT FLOW_CLONE_NOTICE verbatim ("The copy is switched off and does not run until you switch it on, and it still uses the same subflows, actions and objects as the original.") — but it no longer warns of a double-fire / carries a toggle instruction (The flow clone notice is a paragraph of engine prose; say it in one line (#27356 row 18) #27886). See F-1.
    • A3 pass: {"name":"showcase_urgent_task_alert","label":"Same"} → 409 RESOURCE_CONFLICT "…Same-name clones are refused on purpose: the automation engine keys flows by bare name, so a second definition under one name silently shadows the other … Retry with a machine name no flow uses (for example 'showcase_urgent_task_alert_copy')."; POST /automation/does_not_exist/clone → 404 RESOURCE_NOT_FOUND "Flow 'does_not_exist' not found"; {"name":"Bad-Name!"} → 400 VALIDATION_FAILED "Invalid flow definition: name: … must match pattern /^[a-z_][a-z0-9_]*$/" (field name, invalid_format).
    • A4 pass: POST …/showcase_task_done_notify_owner/clone {qa_task_done_clone} → 200; subflow node call_notify config.flowName "showcase_notify_owner" in the response AND on the stored GET.
    • A5 pass: PUT /api/v1/automation/qa_urgent_alert_clone (own definition, label "QA urgent alert clone (edited)") → 200; re-read label "QA urgent alert clone (edited)".
    • A6 fail (stale spec — Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149 / ADR-0146 D4; the promise itself holds): durability holds — after a cold restart over the same DB, GET …/qa_urgent_alert_clone → 200 label "(edited)", status obsolete; with it switched on (PUT status active → 200) one urgent create on the new process moved clone runs 0→1 (base 3→4); while off, a post-restart urgent create left clone runs at 0. Reachability holds on the CURRENT surface — /_console/studio/com.example.showcase/automations rail lists "QA urgent alert clone (edited) · Added · Off" and opens it (heading "QA urgent alert clone (edited)", badge "In com.example.showcase.custom", Disabled switch, its 3 nodes; URL ?surface=flow:qa_urgent_alert_clone). But the pinned surface /_console/studio/~org/automations renders "No flows outside a package. To change a packaged flow, open its package and use New similar." — the clone is absent there by design (no package-less clone after Flow clone door → "new similar": the copy lands in a writable target package, by default the paired <base>.custom, never package-less #28149), and the verify's "stored with package_id null" and "runs list grows" on an un-switched clone no longer hold. See F-1.
    • N0 pass: no facet missing — description, version, type, nodes(3), edges(2), runAs all deep-equal the source.
    • N1 pass: no clonedFrom/cloned_from/basedOn key in the clone response, the stored GET, GET /api/v1/meta/flow listing, or the post-restart read.
    • N2 pass: same-name clone → 409 RESOURCE_CONFLICT (never 2xx).
    • N3 pass: anonymous POST …/clone → 401 UNAUTHENTICATED; auditor.demo@example.com (positions org_member, auditor, everyone; session proven via get-session) → 403 PERMISSION_DENIED "Authoring automation flows requires the manage_metadata capability."; neither left a flow (GET → 404).
    • Teardown: DELETE …/qa_urgent_alert_clone and …/qa_task_done_clone → 200 deleted:true; re-read 404.

    dashboards.strict-widget-rejects-stray-keys r3 — PASS

    • A0 pass: PUT /api/v1/meta/dashboard/qa_stray_key_probe?mode=draft with a valid dataset widget + each of the 11 legacy keys (object, categoryField, categoryGranularity, valueField, aggregate, aggregation, rowField, columnField, xAxisField, yAxisFields, measures) → 422 INVALID_METADATA path widgets.0 code unrecognized_keys, message "Unrecognized key(s) on this dashboard widget: ‹key›." naming the literal key in all 11.
    • A1 pass: each of the 11 messages carries the prescription "The pre-ADR-0021 inline analytics shape (object + categoryField + valueField + aggregate, pivot rowField/columnField) was removed — bind a dataset and select dimensions + values by name."
    • A2 pass: chartFlavour → 422 "Unrecognized key(s) on this dashboard widget: chartFlavour." with no did-you-mean (no declared key is close — absent honestly); suggester proven live on near keys: chartConfg → "Did you mean chartConfg → chartConfig?", dimension → "Did you mean dimension → dimensions?".
    • A3 pass: chartConfig.dataset → 422 path widgets.0.chartConfig "dataset is not part of the chart config — it is the dashboard widget's own key (ADR-0021), a sibling of chartConfig"; chartConfig.drillDown → "it is a REACT-TIER prop … ‹ObjectChart … drillDown={ {…} } /› … On a REPORT the switch is drilldown"; aggregate and objectName likewise rejected with their own guidance (objectName: "dataset on a dashboard widget (ADR-0021), or the objectName PROP on a react ‹ObjectChart›").
    • A4 pass: after each of the 16 rejected PUTs (before any good revision) GET /api/v1/meta/dashboard/qa_stray_key_probe → 404 RESOURCE_NOT_FOUND and ?state=draft → 404 NO_DRAFT; after a clean draft was saved+published (seq 2), a categoryField draft PUT → 422 and the GET still returns the clean revision (0 occurrences of categoryField), ?state=draft → 404 NO_DRAFT; a non-draft PUT with measures also → 422.
    • A5 pass: clean widget (dataset showcase_task_metrics, dimensions [status], values [task_count]) published → /_console/apps/showcase_app/dashboard/qa_stray_key_probe screenshot shows "Tasks by Status" bar chart, 5 bars (Backlog/To Do/In Progress/In Review/Done, 2 each) in a 590×350 recharts SVG; its dataset query → 200 with 5 rows; 0 role=alert.
    • N0 pass: no variant reached the paint — all 16 refused at the parse with a named key (422), so no blank-without-error render is possible; the clean twin renders 5 bars.
    • N1 pass: 0 of 16 stray-key draft saves (plus one non-draft save) answered 2xx — every one 422.
    • Step 4 (inspector, no clause): see the studio-authoring.dashboard-widget-and-filter-types section for the inspector/Source-tab read.

    dashboards.chart-type-matrix r7 — PASS

    Ground truth (POST /api/v1/analytics/dataset/query, clock 2026-11-02T04:55Z UTC): status 5 buckets (2 each), priority 4 (Low 1/Medium 4/High 3/Urgent 2), created_at month buckets 3 (2026-09:1, 2026-10:8, 2026-11:1 — this boot date crosses two month boundaries), project accounts 3, status×priority 8 non-empty cells, progress 6 buckets, enum = 20 members (chart.zod.ts read, matches variants).

    • A0 pass: per-variant element screenshots of /_console/apps/showcase_app/dashboard/showcase_chart_gallery (18 widgets, 0 role=alert) + scratch qa_single_value_gallery: bar 371×350 SVG 5 rect bars; column 4 bars; horizontal-bar 5 horizontal bars; line/area 590×350 curve over Sep/Oct/Nov 2026; pie 5 sectors; donut 4 sectors (ring); funnel 5 trapezoids; scatter 6 symbols; treemap 5 depth-1 rects; sankey 5 links/6 nodes; combo 3 bars + 1 line; radar 1 polygon over 4 angle ticks; table real ‹table› 3 rows × 4 cols + Total; pivot ‹table› 5 status rows × 4 priority cols + totals; metric "10 Tasks" / "47.0" / "$616,000.00"; kpi/gauge/solid-gauge/bullet each "10 Tasks" on the scratch board.
    • A1 pass: DOM marks vs API buckets — bar 5=5, column 4=4, horizontal-bar 5=5, pie 5=5, donut 4=4, funnel 5=5, radar 4 angle ticks=4, line path MCC = 3 points = 3 month buckets, area path MCC = 3 = 3 (x ticks Sep/Oct/Nov 2026).
    • A2 pass: combo SVG 1246×350 holds 3 recharts-bar-rectangle (task_count, left axis 0–8) + 1 recharts-line-curve (avg_progress, right axis 0–60), 2 recharts-yAxis; legend "Avg Progress | Tasks"; screenshot shows bars with a green line crossing on dual axes.
    • A3 pass: screenshots — treemap draws 5 nested labelled rectangles (In Progress largest), sankey draws a "Tasks" root with 5 curved flow bands to Backlog/To Do/In Progress/In Review/Done, scatter draws 6 dots on numeric x (0–100)/y (0–60) axes; none is a bar fallback.
    • A4 pass: qa_single_value_gallery (PUT ?mode=draft → 200, publish → 200): KPI Tile / Gauge Tile / Solid Gauge Tile / Bullet Tile each read "10 | Tasks", Metric Ref reads "10 | Tasks" (value-only renders, no dial — one renderer, five names per the spec NOTE; the four tiles look identical as expected; metric tile chrome differs: title inline, no header rule — value compared only).
    • A5 pass: pivot header [Status, Low, Medium, High, Urgent, Total] = distinct priorities; rows Backlog/Done/In Progress/In Review/To Do = distinct statuses; cells In Review×High 2, Done×Medium 2, Backlog×Low 1 equal the API status×priority rows; totals row 1/4/3/2/10.
    • A6 pass: pin examples/app-showcase/test/coverage.test.ts run at 7fa9873 in TREE (pnpm exec vitest run --maxWorkers=1) → "Test Files 1 passed, Tests 13 passed" (declaration layer only; render checks above are the browser half).
    • A7 pass: qa_compareto_probe (dateRange issued_on last_90_days; line + metric on showcase_invoice_metrics invoice_count, compareTo {kind:previousPeriod}) → widget queries carry timeDimensions dateRange ["2026-08-04","2026-11-02"] + compareTo; literal-window reads of showcase_invoice from the same clock: last 90 days 7, prior 90 days 3. Metric tile reads "7 | Invoices | ↑ 133% vs previous period"; line draws 2 curves (solid "Invoices" + dashed "Invoices · vs previous period"), compare series rows 1/1/1/0 = 3 = the previous window's count.
    • A8 pass: pin packages/spec/src/ui/dashboard-compareto.test.ts at 7fa9873 → "Test Files 1 passed, Tests 17 passed".
    • N0 pass: every gallery variant draws its own family (sankey = flow links, treemap = rects, funnel = trapezoids, radar = polygon); the four synonyms rendering as the metric value is the declared exemption, not scored here.
    • N1 pass: 0 blank widgets across 18 gallery widgets + 5 synonym tiles + 2 compareTo widgets; 0 role=alert.
    • N2 pass: the comparison renders (dashed series + "↑ 133%"), not silently absent.
    • Retest dashboards: a sankey widget with two dimensions silently draws only the first category's flows #27460 (sankey, two dimensions): HOLDS — scratch qa_sankey_two_dim (sankey, dimensions [status, priority], values [task_count]) draws 8 recharts-sankey-link = the 8 non-empty status×priority cells, 9 nodes (5 statuses + Low/Medium/High/Urgent), no omitted-rows note, no chart error; screenshot shows every status flowing into its priorities (not only the first category's flows).

    dashboards.global-filters-rescope r5 — PASS

    • A0 pass: Revenue Pulse (/_console/apps/showcase_app/dashboard/showcase_revenue_pulse) initial 10 dataset queries; region → EMEA re-issued 9 POST /api/v1/analytics/dataset/query (200), the 6 invoice widgets with runtimeFilter {"$and":[{"issued_on":{…} },{"region":"emea"}]}, the 3 bound account widgets with {"$and":[{"signed_on":{…} },{"sales_region":"emea"}]}; tiles moved (Invoices 7→1, Accounts Signed 3→1).
    • A1 pass: custom window via Custom… calendar (2026-09-01 → 2026-10-31) re-issued the 9 bound queries with issued_on {"$gte":"2026-09-01","$lte":"2026-10-31"} on invoice widgets and signed_on {…same bounds} on account widgets (bar reads "2026-09-01 – 2026-10-31 | EMEA | Reset").
    • A2 pass: the opted-out "Accounts (all time)" query ({"dimensions":[],"measures":["account_count"]}, no runtimeFilter) appears only in the initial load; 0 such queries after region, custom window or Reset; tile reads "14" in all four phases.
    • A3 pass: "Accounts Signed" under EMEA + last 90 days shows 1; data API GET /api/v1/data/showcase_account counted independently: sales_region=emea and signed_on in 2026-08-04..2026-11-02 = 1 (Wonka Brands 2026-09-03); Invoices tile 1 = data API emea invoices in window 1; custom window EMEA invoices 0 = tile 0.
    • A4 pass: 14 accounts, 3 with NULL signed_on; all-time tile 14, while the dataset with signed_on window 2000-01-01..2030-12-31 → account_count 11 (= 14 − 3 prospects) and last-90-days → 3 (data API count of non-null signed_on in window = 3).
    • A5 pass: Delivery Operations task_status → In Review: 4 task-bound queries re-issued; kpi_awaiting_review query {"$and":[{"status":"in_review"},{"$and":[{created_at window},{"status":"in_review"}]}]} → tile 2 = API aggregate (status in_review ∧ in_review) 2; → Done: tile 0 = API (in_review ∧ done) 0; At-Risk (Red) stays 1 (no project query re-issued; API health=red count 1), Active Projects 2 and Total Budget $1,090,000.00 unchanged.
    • A6 pass: scratch package com.qa.r3 (POST /api/v1/packages 201, writable) + app r3_app navigating to dashboard r3_rescope; in /_console/studio/com.qa.r3/interfaces the Hours Total widget's Filter Bindings row "Region" field combo set to status, and Ticket Count's Region "Apply" unchecked → two PUT /api/v1/meta/dashboard/r3_rescope?mode=draft&package=com.qa.r3&surface=studio 200 carrying filterBindings {"region":"status"} / {"region":false}; Publish all → POST /api/v1/packages/com.qa.r3/publish-drafts 200 publishedCount 1; GET /api/v1/meta/dashboard/r3_rescope → rs_count {'region': False}, rs_hours {'region': 'status'}; reload + Region=North → rs_hours query carries {"status":"north"}, rs_status {"region":"north"}, rs_count issues no query.
    • A7 pass: Revenue Pulse pristine bar text "Last 90 days | Region: All", 0 Reset buttons; after EMEA "Last 90 days | EMEA | Reset" (1 Reset); click Reset → 9 re-queries back to the default window with no region term, bar back to "Last 90 days | Region: All", 0 Reset.
    • A8 pass: region select options All/AMER/EMEA/APAC; after picking EMEA the trigger shows "EMEA" while the query commits "region":"emea"; scratch acct filter (optionsFrom {object: showcase_account, valueField: id, labelField: name}) issues POST /api/v1/analytics/dataset/query with an inline dataset over showcase_account, dimensions [id, name], limit 1000 (server GROUP BY) → 14 options = all 14 accounts; picking one shows the label "Vandelay Industries" while the query commits {"account":"14hF3lvOO0IbhYRn"}.
    • N0 pass: every filter change in this run produced server re-queries (9 / 18 / 9 / 4 / 4), never a repaint alone.
    • N1 pass: opted-out KPI stayed 14 with no query across all changes.
    • N2 pass: the inspector-authored binding persisted (metadata re-read) and drove the reload (re-targeted field in the query; opted-out widget silent).
    • N3 pass: no Reset on the pristine bar; the optionsFrom list is complete (14 of 14 accounts, server limit 1000).
    • Retest dashboards: a widget filterBindings key naming an undeclared dashboard filter is accepted at draft and publish (silently inert) #27445 (filterBindings key naming an undeclared filter): HOLDS — direct PUT /api/v1/meta/dashboard/qa_fb_probe2 with filterBindings {"dateRange":"issued_on"} on a dashboard with no filters → 422 rule dashboard-filter-binding-unknown "declares no filters, so the binding is never read" (GET → 404); a draft with key regoin saves 200 (draft door) with _diagnostics.valid:false code dashboard-filter-binding-unknown, and its publish → 422 with hint 'Did you mean "region"?'.

    records-forms.form-view-gallery r6 — PASS

    • A0 pass: simple — deep-linked/in-app /record/6qa9ihKO_XZ-NkQF/edit (RecordFormPage) shows section "Task", 2-col grid (grid-cols-1 @md:grid-cols-2), labels in order Title*, Project*, Assignee, Status*, Priority, Due Date; tabbed overlay (PUT ?mode=draft 200 seq=1 + publish 200 seq=2, isDefault:true) → page form shows role=tab ["Overview","Schedule","Details"] with Overview=[Title,Project,Assignee,Status], Schedule=[Start Date,End Date,Due Date,Progress], Details=[Estimate (h),Labels,Work Location,Notes]; wizard overlay (seq=3/4) → /new and /edit show step chrome "1 Basics · 2 Assignment · 3 Schedule", "Step 1 of 3", Next/Back; task-desk (nav "Task Desk · drawer & modal" → /page/showcase_task_desk): row click opens role=dialog "Edit task" at x=1016 w=384 h=900 of a 1400 viewport (right side panel over the list), "+ New task" opens role=dialog "New task" centred x=188 w=1024 (dialog). One screenshot per variant taken in RUN/shots.
    • A1 pass: wizard on /new (in-app push from list): step headers wizard-step:step_assign/step_schedule disabled while on step 1; Next with title but no project stays "Step 1 of 3" with "Project is required"; 0 data writes through steps 1→2→back→2→3; final Save → exactly one POST /api/v1/data/showcase_task {"status":"backlog","priority":"low",…,"title":"R4b wizard task A","project":"Y84_4rM_pGWCrmrH","assignee":"r4b-assignee@example.com"} → 201 id Vqa-VkF47wZ7HG0q. On /edit: 0 writes across steps, one PATCH {"priority":"medium"} → 200 at final Save.
    • A2 pass: simple form, priority High→Urgent: Notes label count 0→1, Notes textarea spans the full row (276–1172px vs Title 276–716px in the 2-col grid); Urgent→High: Notes count 1→0 (screenshots s4-0 / s4-urgent / s4-high).
    • A3 pass: every save re-read by GET /api/v1/data/showcase_task/‹id› and diffed against the pre-read: simple PATCH {"title":…R4b-simple} → only title/updated_at/updated_by changed; tabbed PATCH {"title":…R4b-tabbed} → only title/updated_at; tabbed 2nd save PATCH {"notes":"R4b distinctive note ZQX-41370"} → only notes/updated_at; wizard /edit PATCH {"priority":"medium"} → only priority/updated_at; wizard /new POST 201 re-read title "R4b wizard task A", priority low, assignee as entered; task-desk drawer PATCH {"title":"Design system R4b-drawer"} → only title/updated_at/updated_by; task-desk modal POST 201 KjXye3UEEpQUEVdy re-read title "R4b modal task", project Y84_4rM_pGWCrmrH.
    • A4 pass: tabbed: Title typed on Overview → Schedule → Details → Overview reads "Warehouse schema R4b-tabbed"; Notes typed on Details → Overview → Schedule → Details reads "R4b distinctive note ZQX-41370" (both then saved). Wizard: step 2 → Back → step 1 reads title "R4b wizard task A", project "Data Platform"; re-Next → step 2 reads assignee "r4b-assignee@example.com", priority "Low".
    • A5 pass: overlay PUT /api/v1/meta/view/showcase_task.form?mode=draft with config.type split / drawer / modal → each 422 INVALID_METADATA, issue path config.type: "'split' was removed from the form view type enum in @objectstack/spec 18.3.0 (ADR-0050) — … Write type: 'tabbed' … declare subforms…", "'drawer' was removed … a list opens a clicked record in the drawer (navigation.mode…)", "'modal' was removed … the console's New and Edit dialogs open the record form themselves (sized by the form's modalSize)…". Refused at the DRAFT PUT; the following publish answered 404 NO_DRAFT each time. GET showcase_task.form before vs after the three: byte-identical (type wizard overlay still served).
    • N0 pass: Next on the empty wizard step 1 stays "Step 1 of 3" with named errors "Title is required" / "Project is required"; no per-step writes (0 data writes until final submit, both /new and /edit).
    • N1 pass: drawer and modal scored only on task-desk.page.ts (ObjectForm formType drawer / modal), never off the record-form page or an overlay; the overlay route for those types is refused (A5).
    • Revert (steps[7]): DELETE /api/v1/meta/view/showcase_task.form → 200 {"reset":true,"seq":5}; re-read byte-identical to the pre-run baseline (type simple, section "task").
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

    Per-clause verdicts, part 2 of 3 (text oracle evidence)

    Continuation of this record. One line per clause, A then N, in the scope order of the record body. Runner tag in the record fingerprint. Double braces inside quoted templates are spaced apart so the write door does not read them as tokens.

    records-forms.action-location-matrix r18 — PASS

    Fixture: qaact-placement-specimen installed via POST /api/v1/marketplace/install-local → 200 {manifestId:'com.objectstack.qaact.specimen', hotLoaded:true}; GET /meta/object/showcase_task → 13 actions, last three qaact_control [list_item,record_header], qaact_visible_throws (visible cel 'qaact_undeclared_root.flag == true'), qaact_no_locations (NO locations key); ledger landed in RUN/app/.objectstack/installed-packages (TREE has none). Placement matrix (admin, 1440x900, each surface screenshot-confirmed before the DOM read):

    • list_toolbar (Tasks list header): Reassign… only (+ host New/Import, header ⋯ = "Add to favorites" only).
    • list_item (row-action-trigger menus, 10 rows): Edit, Delete, Mark Done|Reopen, Reassign…, Quick View, QA Control.
    • record_header (Task Detail title bar): Mark Done (not-done only), Log Time, Archive (disabled until done), then host Edit.
    • record_more (Task Detail main ⋯): Refresh, Open Docs, Recalculate Selection, Add to favorites, Share, Delete.
    • record_section (Task Detail quick-actions bar): Recalculate Estimate only.
    • record_related (Project Detail aAqfHk3bag-5DKrj ?tab=related, Tasks list, 5 Row-actions menus): Edit, Mark Done|Reopen, Reassign…, Quick View, Log Time, QA Control, Delete; list header Reassign… + New. Tasks tab = line-items grid (Duplicate/Remove row only, no actions).
    • A0 pass: every hosted location renders ≥1 declared action — list_toolbar "Reassign…" (button in main header), list_item row-action-showcase_mark_done/quick_view/bulk_reassign on all rows, record_header "Mark Done"/"Log Time" buttons on Task Detail 35PvaKGB3ArK9G4d, record_more menuitems "Open Docs"/"Recalculate Selection" under main ⋯, record_section "Recalculate Estimate" in the bar, record_related row-action-showcase_log_time on all 5 Related-tab rows.
    • A1 pass: zero extra placements — Open Docs/Recalculate Selection appear only under ⋯ (not inline in the header although page:header lists their ids, i.e. the header location-filters explicitly-named ids), not on the list toolbar/toolbar ⋯; Log Time absent from all 10 showcase_task list rows (record_related only on the parent's rows); Recalculate Estimate only in the bar; Archive only in the header; Quick View/Reopen/Mark Done never on the toolbar. Related-tab rows carrying list_item actions + header Reassign… is the list's own list_item/list_toolbar placement (knownGaps[1]).
    • A2 pass: list_toolbar Reassign… (row "Content backlog" selected) → POST /api/v1/actions/showcase_task/showcase_bulk_reassign {recordId:S0Aw9yv92UYj2p3N} → 200 status 'paused' screen 'New Assignee'; wizard dialog "Reassign Task / Step 1 of 1"; Submit → POST /api/v1/automation/showcase_reassign_wizard/runs/run_2a794c03…/resume {inputs:{new_assignee:'qa-r4a-2@verify.test'} } → 200 acted:1; re-read assignee 'qa-r4a-2@verify.test'. list_item Quick View → dialog "Component Gallery …" opens (no navigation), closes on Esc. list_item Mark Done on qYyL0AKmRTP9QnS6 → POST /api/v1/actions/showcase_task/showcase_mark_done → 200 {operation:'update'}; toast "SEO migration plan marked done."; re-read done:true progress:100. record_header Mark Done on vjqmeLQDd5bdt5KM → same POST 200, re-read done:true. record_header Log Time → routed page /_console/apps/com.example.showcase/forms/showcase_task.form?recordId=35PvaKGB3ArK9G4d&recordObject=showcase_task (no dialog), Task form prefilled "Warehouse schema"/Data Platform. record_related Log Time (Related row "Build homepage") → /forms/showcase_task.form?recordId=e_o-L6i5fN3SyNUf&recordObject=showcase_task. record_more Open Docs → popup request https://objectstack.ai/ (egress blocks the page; the navigation was issued). record_more Recalculate Selection on dUnBpvBw2qA--v_N → POST /api/v1/actions/showcase_task/showcase_recalc_selection → 200 output.estimates [{estimate_hours:24}]; re-read estimate_hours 8 → 24 (window Oct 13–15 = 3d×8). record_section Recalculate Estimate on 35PvaKGB3ArK9G4d → POST …/showcase_recalc_estimate → 200 estimates [{estimate_hours:208}]; re-read 30 → 208.
    • A3 pass: not-done rows (8 of 10 initially) menu lists Mark Done; done rows "Audit current IA"/"App wireframes" list Reopen and no Mark Done; Task Detail header of not-done 35PvaKGB3ArK9G4d has "Mark Done", done dUnBpvBw2qA--v_N header has only Log Time/Archive/Edit; after the dispatch, SEO migration plan's row menu flips to Reopen and Design system's header loses Mark Done. Fail-closed: qaact_visible_throws absent from all 10 list row menus and all 5 Related-tab row menus where QA Control renders, and the console logged "[object-ui] A conditional predicate failed to evaluate (qaact_visible_throws) and was treated as its safe default … Unknown variable: qaact_undeclared_root".
    • A4 pass: (a) QA No Locations on none of: list toolbar + toolbar ⋯, 10 list row menus, 5 Related-tab row menus (both with QA Control present), Task Detail header/⋯/bar (QA Control absent there too, so no weight — recipe knownGaps[0]); (b) showcase_new_task "New Task" on none of the six surfaces (the toolbar's "New" is the host's data-testid=object-view-new-button, Related's "New" is related-list-new); recalc_selection present under ⋯ and absent from the toolbar.
    • N0 pass: no action rendered at an undeclared location (matrix above; the only non-declared items are host built-ins Edit/Delete/Share/Refresh/Add to favorites/New/Import).
    • N1 pass: list toolbar buttons = New, Import, Reassign…, More actions(→ Add to favorites); no Recalculate Selection.
    • N2 pass: Log Time from both record_header and record_related opens /forms/showcase_task.form (the declared form view), not a list view.

    records-forms.calendar-interactions r4 — PASS

    Surface: /_console/apps/com.example.showcase/showcase_task/view/calendar (view switcher "Calendar", month grid November 2026, footer "9 records loaded for these dates"; screenshot-confirmed before every DOM read). Cards are [role=button][aria-label=title] inside [role=gridcell][aria-label="‹weekday›, ‹Month› ‹d›, 2026, …"]; drags are ref-targeted Playwright dragTo (HTML5 DnD) card → target gridcell.

    • A0 pass: drag "SEO migration plan" Tue Nov 17 → Thu Nov 19 → PATCH /api/v1/data/showcase_task/qYyL0AKmRTP9QnS6 {"due_date":"2026-11-19"} → 200; GET re-read due_date "2026-11-19"; after page.reload() the card renders in gridcell "Thursday, November 19, 2026, 1 event".
    • A1 pass: contributor-bound member qa-r4a-contrib@verify.test (invite → sign-up → admin POST sys_user_position {position:'contributor'}; get-session email proven; GET /data/showcase_task as member → 1 row, its own "R4a contrib task" pml0DQujBR2trxuB of 12) loads the calendar (card draggable=true on Wed Nov 18); admin PATCH assignee→admin@objectos.ai (200); member drags to Nov 19 → PATCH {"due_date":"2026-11-19"} → 404 {"code":"RECORD_NOT_FOUND","error":"Record pml0DQujBR2trxuB not found in showcase_task"}; with the PATCH held 2 s the card read in "Thursday, November 19" right after the drop (optimistic), then back in "Wednesday, November 18" after the answer, with sonner toast data-type=error "Record pml0DQujBR2trxuB not found in showcase_task"; server due_date stays 2026-11-18. Reproduced twice (once unheld: Nov 20 target, same 404 + rollback + toast).
    • A2 pass: Month → 8 cards (Design system, Warehouse schema, Build homepage, PII access review, SEO migration plan, Ingest pipeline, Evidence collection, Content backlog); Week "Nov 1 - Nov 7, 2026" → Design system + Warehouse schema in the All Day lane; next week "Nov 8 - Nov 14" → Build homepage + PII access review; Day "Wednesday, November 4, 2026" → Design system; "Thursday, November 5, 2026" → Warehouse schema; Nov 2/3 → none. API spot-checks: vjqmeLQDd5bdt5KM due_date 2026-11-04, e_o-L6i5fN3SyNUf due_date 2026-11-12 — match.
    • A3 pass: click on empty gridcell "Tuesday, November 10, 2026" → URL ?form=new, dialog "Create Task" (the host's full create form: Title*, Project*, Assignee, Status*, Priority, Due Date) with due_date input "11/10/2026" pre-filled; filled Title + Project (Website Relaunch) → POST /api/v1/data/showcase_task {…"due_date":"2026-11-10","title":"R4a calendar create","project":"aAqfHk3bag-5DKrj"} → 201 id GhZVyoqgQCEK0X1p; toast 'Task "R4a calendar create" created'; GET re-read due_date "2026-11-10".
    • N0 pass: every drag that repainted issued a PATCH (A0: 200 and the card stayed; A1: 404 and the card did NOT keep the new day).
    • N1 pass: the failed reschedule surfaced an error toast with the server's message alongside the rollback (A1), not a silent snap-back.

    records-forms.field-group-visible-when r11 — PASS

    • A0 pass: install POST /api/v1/packages {manifest id com.objectstack.qa.group, namespace 'qagroup'} → 201 (namespace echoed); PUT /api/v1/meta/object/qagroup_group_gate?package=com.objectstack.qa.group with visibleWhen: "record.kind == 'pro'" → 200 seq=6, GET reads fieldGroups[0].visibleWhen = "record.kind == 'pro'" (bare string, verbatim), highlightFields ['kind']; PUT envelope {dialect:'cel',source:"record.kind == 'pro'"} → 200 seq=7, GET reads the envelope verbatim; batch20 pin "accepts the bare-string CEL shorthand and normalizes it to the envelope" + "accepts the full Expression envelope…" green (vitest, packages/spec, 70/70 across the two files, at 7fa9873); render identical for both spellings (A3 run once per spelling, same results). Provisioned with sharingModel: 'public_read_write' added — see accuracy findings.
    • A1 pass: PUT with fieldGroups[0].visibleOn (both ?mode=draft and active) → 422 INVALID_METADATA, path fieldGroups.0 unrecognized_keys: "visibleOn is not a field-group key — the section predicate slot is visibleWhen … Write visibleWhen: ‹CEL› — FALSE hides the whole group, header included."; GET after reads the prior envelope unchanged.
    • A2 pass: field-group-layout.test.ts › deriveFieldGroupLayout › "visibleWhen passthrough (ADR-0085 §5 re-introduction)": 5/5 green incl. "carries a bare CEL string through verbatim" and "carries an Expression envelope through verbatim (post-parse form)" (pure-function layer, which is the clause's oracle).
    • A3 pass: console is built in-repo from ui/ at this commit (7fa9873), so not stale; adapters read at that commit: plugin-form/src/fieldGroups.ts L98 copies visibleWhen (+ visibleWhenFault:'hidden'), record-details.tsx evaluates { group } refs per record. Live, after a PATCH pre-filling pro_tier "Gold-R4b"/pro_seats 7: FORM (/record/7bjI5bP0C8Dx_Ajj/edit) kind=Basic → "Pro details" header count 0, labels [Name, Kind]; select Kind=Pro (unsaved) → header 1, labels [Pro tier, Pro seats, Name, Kind]; back to Basic → 0. DETAIL kind=basic → header 0; after a saved flip (form Save, PATCH {"kind":"pro"}) → "Pro details | PRO TIER | Gold-R4b | PRO SEATS | 7"; after saved flip to basic → header 0. Same four observations with the envelope and with the bare string. Pin record-details.groupVisibleWhen-11630.test.tsx: 13/13 passed (cd ui && pnpm exec vitest run --maxWorkers=1 …).
    • A4 pass: kind=basic (section hidden on the form), unrelated edit of Name saved → the only write is PATCH {"name":"Gate R1 edited-g1env"} (and …-g1str on the second spelling) → 200; re-read pro_tier "Gold-R4b", pro_seats 7 unchanged both times. (Members carry no required/validation rule here, so "skip client-side validation" was not separately exercised.)
    • N0 pass: visibleOn refused 422 at draft and active, nothing stored (A1).
    • N1 pass: hidden-section save did not drop or null its members (A4 PATCH body has only name; re-read values intact). Boundary recorded: visibility is display-tier (drawing only).
    • N2 pass: faulting predicate int(record.kind) > 0 (accepted 200 at the door) → detail and form both render (Name/Kind present, no error boundary, no page error) with the section HIDDEN for kind=pro and kind=basic → fail-CLOSED on both pages, with a named console warning "[object-ui] A conditional predicate failed to evaluate (visibleWhen of group 'pro_details' of record:details … — fail-closed, the group is hidden) … Reason: [runtime] found no matching overload for 'int(…'". Unknown-field predicate record.nonexistent_field == 'x' is refused at the door (422 expression-invalid "unknown field nonexistent_field"), so it never reaches render. Direction recorded: fail-closed, matching the spec docblock — no drift left to file (see accuracy findings).
    • N3 pass: clause 4 scored on the OBJECT-level fieldGroups slot of the scratch object qagroup_group_gate, not on task.view.ts FormSection.visibleWhen.
    • Teardown: DELETE /api/v1/meta/object/qagroup_group_gate?package=… → 200 then GET 404; DELETE /api/v1/packages/com.objectstack.qa.group → 200 (it also removed a leftover object draft, see not-filed), GET 404. No installed-packages dir in the scratch app.

    studio-authoring.view-authoring-live r5 — PASS

    Persona note: no permission set grants a non-admin access to the runtime-authored object, so the end-user surfaces were scored as the admin (as the item's personas[1] permits). Surface: /_console/apps/showcase_app/qfive_asset (redirects to /apps/com.example.showcase/qfive_asset).

    • A0 pass: PUT /api/v1/meta/view/qfive_asset_views?mode=draft&package=com.qa.rfive {object:qfive_asset, list:{grid, columns:[serial,name,status]}, form:{sections:[QA Identity Block(serial,name), QA Money Block(cost)]} } → 200 {"success":true,"version":"hmac-sha256:661260bc…","seq":3,"state":"draft"}.
    • A1 pass: after the draft, screenshot of the list shows the synthesized default; headers ["Name","Serial","Status","Cost","Notes","Actions"] (same as before the draft); GET /api/v1/meta/view/qfive_asset_views → 404 RESOURCE_NOT_FOUND and GET /api/v1/meta/view?object=qfive_asset → 200 items:[] — no draft leakage.
    • A2 pass: POST …/qfive_asset_views/publish → 200 seq 4; client reload only: list headers ["Serial","Name","Status","Actions"]; New dialog "Create QF Asset … QA Identity Block / Serial / Name / QA Money Block / Cost" (screenshot: two titled sections, Serial+Name side by side); row menu Edit dialog "Edit QF Asset … QA Identity Block … QA Money Block / Cost"; boot.log has exactly one ✓ Server is ready and no restart/recompile line across draft→publish→render (03:55:26–03:56:27Z), same listener throughout.
    • A3 pass: GET /api/v1/meta/view?object=qfive_asset → 200 items qfive_asset.default (viewKind list, config.columns ["serial","name","status"]) and qfive_asset.form (viewKind form, sections "QA Identity Block"/"QA Money Block"), _packageId com.qa.rfive.
    • A4 pass: re-draft with columns [status,name,cost,serial] → 200 state draft seq 5; list still ["Serial","Name","Status","Actions"] before re-publish; publish → 200 seq 6; reload → headers ["Status","Name","Cost","Serial","Actions"] (screenshot confirms reordered + added Cost column).
    • N0 pass: no restart was needed for either publish (one Server is ready in boot.log).
    • N1 pass: neither the first draft nor the re-draft leaked into the list before publish (headers unchanged in both pre-publish reads).

    studio-authoring.draft-publish-lifecycle r10 — FAIL

    All forensics consulted on the live os dev REST server (port 41380), never a simulated dispatch. Dashboard drafts staged with ?package=com.qa.rfive (writable).

    • A0 pass: PUT /api/v1/meta/dashboard/qa_lifecycle_probe?mode=draft&package=com.qa.rfive (bar widget by_status, dataset showcase_task_metrics, dimensions [status], values [task_count], title "REV1 Tasks by Status") → 200 {"success":true,"version":"hmac-sha256:29133d59…","seq":7,"state":"draft"}; runtime GET /api/v1/meta/dashboard/qa_lifecycle_probe → 404 RESOURCE_NOT_FOUND.
    • A1 pass: POST …/qa_lifecycle_probe/publish → 200 "Published draft … [seq=9]"; GET → 200 {type:"dashboard",name:"qa_lifecycle_probe",item:{…widgets:[{id:"by_status",…title:"REV1 Tasks by Status"}]} }.
    • A2 pass: draft 2 (title "REV2 …") → 200 state draft seq 10; GET still "title":"REV1 Tasks by Status"; publish → 200 seq 11; GET "title":"REV2 Tasks by Status".
    • A3 pass: re-PUT with the superseded token If-Match: hmac-sha256:29133d59… → 409 {"code":"METADATA_CONFLICT","currentVersion":"hmac-sha256:85733b71…"}; the draft still reads "REV2", not the stale write.
    • A4 pass: staged dashboards qfive_pd_one/qfive_pd_two + object draft r5badobj (no namespace prefix) → POST /api/v1/packages/com.qa.rfive/publish-drafts HTTP 200, body data.success:false, outcome:"refused", publishedCount:0, failed:[{name:"r5badobj",code:"NAMESPACE_PREFIX"}]; qfive_pd_one, qfive_pd_two and the pending app qfive_app all 404 afterwards. Bad draft discarded (DELETE …/object/r5badobj?state=draft&package=… 200), re-POST → 200 data.success:true, outcome:"published", publishedCount:3 (app + both dashboards, one commitId); both dashboards then serve ("PD one count"/"PD two count"). (N4 shape observed, not scored.)
    • A5 pass: widget with stray object key → 422 INVALID_METADATA "widgets.0 [unrecognized_keys] … Unrecognized key(s) on this dashboard widget: object"; publish then → 404 NO_DRAFT; live GET still "REV2 Tasks by Status".
    • A6 pass: Studio /_console/studio/com.qa.rfive/interfaces → nav entry "QA Dash" (app draft qfive_app staged in the package naming the dashboard): header chip "Draft · 2 unpublished", Changes button "Changes · 2", canvas "Design · includes unpublished changes" (screenshot). After the REST per-ref publish + reload: no Draft line, "Changes · 1" (the app). After publish-drafts + reload: no Draft line, button reads "Changes" with no pill (screenshot).
    • A7 pass: GET …/history → 200 events v1 create seq7, v2 publish seq9 ("publish draft"), v3 create seq10, v4 publish seq11, ascending, actor = admin id on each.
    • A8 pass: GET …/diff?from=2&to=4 → 200 added:[],removed:[],changed:[{path:"widgets",from:[…"REV1 Tasks by Status"…],to:[…"REV2 Tasks by Status"…]}] — label/name not listed.
    • A9 pass: POST …/rollback {"toVersion":1} → 200 "Reverted to version 1 … restoredFromVersion:1"; GET → "REV1 Tasks by Status"; /_console/apps/qfive_app/dashboard/qa_lifecycle_probe screenshot shows the widget "REV1 Tasks by Status". Bad toVersion: {}, "abc", 0, -1 → each 400 {"code":"INVALID_REQUEST","error":"'toVersion' (positive integer) is required"}; 999 → 404 VERSION_NOT_FOUND.
    • A10 fail (stale spec): actor half holds — GET …/audit rows for save draft (allowed), both publishes (allowed), two 409s (denied, code metadata_conflict) and both rollbacks (allowed) each carry actor KiZpgpkw… (the admin), the NO_DRAFT 404 wrote no row; but the clause also pins "a 422 INVALID_METADATA spec refusal … write[s] no row", and the trail now carries {"operation":"save","outcome":"denied","code":"invalid_metadata","actor":"KiZpgpkw…"} for each 422 (2 of 2). See F-1.
    • A11 pass: rollback {"toVersion":4} sent with X-Actor: someone_else_entirely → 200; audit row (rollback, "restored from version 4") and history row (seq 19, op revert, "revert to version 4") both actor KiZpgpkw…; someone_else_entirely occurs 0 times in /audit and /history.
    • N0 pass: no draft body served before publish (404 for the first draft, REV1 while REV2 was pending).
    • N1 pass: every publish 200 was followed by a GET serving the new body.
    • N2 pass: recording note honoured — every forensics read was on the live REST server.
    • N3 pass: both rollbacks flipped the served body (REV1, then REV2).
    • N4 pass: open-ledger note honoured, not adjudicated — the abort answered HTTP 200 with data.success:false and left all names 404, as described.

    studio-authoring.authoring-validation-not-persisted r5 — PASS

    • A0 pass: PUT /api/v1/meta/object/qa_invalid_probe?mode=draft (field amount with label only) → 422 {"error":"object/qa_invalid_probe failed spec validation: 1 issue — fields.amount.type [invalid_value]","code":"INVALID_METADATA","issues":[{"path":"fields.amount.type",…}]}.
    • A1 pass: container-shaped body on a fresh object with no container (qfive_bare, GET /meta/view?object=qfive_bare → items []): PUT /api/v1/meta/view/qa_invalid_views?mode=draft {object, type:"grid", columns:[…], list:{…} } → 422 INVALID_METADATA, issue "Unrecognized key(s) on this view container: type, columns. • type belongs to a single VIEW, not to the container. Wrap it: defineView({ list: { type, data, columns, … } }) … The container's own keys are list, form, listViews, formViews."
    • A2 pass: after each rejection: object GET …/qa_invalid_probe → 404 RESOURCE_NOT_FOUND and ?state=draft → 404 NO_DRAFT; view GET …/qa_invalid_views → 404, ?state=draft → 404 NO_DRAFT, GET /meta/view?object=qfive_bare → items []; after the browser 422 below, qfive_bare?state=draft → 404 NO_DRAFT and the live field stays "code":{"type":"text","label":"Code"}.
    • A3 pass: /_console/apps/setup/metadata/object/qfive_bare (redirects to /_console/studio/~all/metadata/object/qfive_bare) → Source tab, body with code missing type pasted into the Monaco editor (clipboard paste, ref-targeted) → autosave PUT /api/v1/meta/object/qfive_bare?mode=draft → 422; screenshot shows two red banners: "Fields → code → type: Invalid option: expected one of …" and "This metadata does not match the spec — 1 validation error(s). Fields → code → type: …", plus an "Unsaved" chip.
    • A4 pass: corrected qa_invalid_probe (amount type number) → 200 state:"draft" seq 22; corrected qa_invalid_views {object:qfive_bare, list:{type:grid, columns:[name,code]} } → 200 state:"draft" seq 23.
    • N0 pass: no 2xx on any invalid draft; nothing invalid readable afterwards (A2).
    • N1 pass: the browser door surfaced the located error (A3); not silent.
    • N2 pass: note honoured — no _diagnostics banner was met on these saves and none was scored.

    studio-authoring.packaged-automation-studio-lock r6 — FAIL

    • Precondition: GET /api/v1/packages/com.example.showcase → writable:false; baseline GET /api/v1/meta/flow/showcase_urgent_task_alert md5 67b0bd10… (draft read 34cc63f4…). Pins run from ui/ at 7fa9873 (pnpm exec vitest run --root . --maxWorkers=1 …automationsReadOnly-11124.test.tsx …ledgerSwitch-28148.test.tsx): 2 files, 18 passed — component layer (jsdom); the 11124 pin answers _status 404, i.e. the no-ledger-row fallback that a live boot never takes, so no clause is scored from it.
    • A0 fail (stale spec — Studio: a base and its <base>.custom as one surface — "customized" / "added" marks, "only customizations", the pair folded under its base, New lands in the pair #28572, ADR-0146 D3 "on a read-only base … New lands in the pair"): the autosave half holds — on /_console/studio/com.example.showcase/automations?surface=flow:showcase_urgent_task_alert (canvas + "Read-only" badge screenshot-confirmed), a node drag (node did not move: Δ0,0), a node select, an inspector fill attempt and a 4 s wait past the 1.5 s autosave debounce sent zero flow saves (only PATCH /data/sys_user_preference, the recent-packages list), on 2 fresh loads. But the rail heading carries [data-testid=studio-rail-add] aria-label "New automation" (count 1, both loads), which opens the "New automation" dialog (Flow name / When it runs / Save as draft); per ui/…/StudioDesignSurface.tsx (AutomationsPillar openCreateFlow = (!packageReadOnly || newInPair), create target customization.ensurePackage()) it saves into com.example.showcase.custom — the clause's "New-flow button absent" pins the pre-ADR-0146 shape. See F-2.
    • A1 pass: after the whole sweep (drag, inspector, switch ×2 loads, reload) GET /meta/flow/showcase_urgent_task_alert md5 67b0bd10… = baseline; forged PUT …?mode=draft&package=com.example.showcase → 403 code ITEM_LOCKED; …?package=com.example.showcase → 403 ITEM_LOCKED; …?mode=draft (no package) → 403 NOT_OVERRIDABLE; no query → 403 NOT_OVERRIDABLE (flat {error, code} body; each message names a remedy — clone/toggle for ITEM_LOCKED, "Declare an override from 'com.example.showcase.custom' … or switch it off" for NOT_OVERRIDABLE); the sweeps' traces carry no flow write at all; final read md5 identical.
    • A2 fail (stale spec — Studio: switch a read-only package's flow off for this deployment from the Automations pillar — ledger switch on the item, posture-aware, refusal verbatim #28148, ADR-0146 D4 first bullet "the ledger switch, on the item"): the header switch is data-testid=auto-ledger-switch, aria-checked true, NOT disabled, text "On for this deployment", described "This flow ships in a read-only package. The switch turns it on or off for this deployment only; the package is not changed."; a click fired POST /api/v1/automation/showcase_urgent_task_alert/toggle {"enabled":false} → 200 and read "Off for this deployment" (restored with a second click → {"enabled":true} 200), reproduced on 2 fresh loads; the write lands in sys_metadata_activation (row flow/showcase_urgent_task_alert package com.example.showcase active 1), the flow definition md5 unchanged. The clause's PASS shape ("disabled … no request fires"; "a switch that fires a request is now a FAIL") is the pre-Studio: switch a read-only package's flow off for this deployment from the Automations pillar — ledger switch on the item, posture-aware, refusal verbatim #28148 definition switch. See F-2.
    • A3 pass: with Notify Assignee selected, all 17 inspector controls (ID "alert", Label "Notify Assignee", type, recipients, title, message, topic, severity, actionUrl…) are disabled; a fill on the first text input timed out (not editable); after a page reload and re-select every control still disabled with the original values — inert, nothing taken, nothing vanished (2 fresh loads).
    • N0 pass: no 2xx on any flow write into com.example.showcase — 4 forged writes 403; the sweeps sent no flow write; the only 2xx flow write of the session was the probe below, a declared-override draft into com.example.showcase.custom (by design, Studio: customize a read-only package's flow into <base>.custom under its own name — one ledger switch, the declaration's badge, "Restore package original" #28570), with the base read byte-identical after it.
    • N1 pass: the inspector edit never took (inputs disabled) and nothing changed across reload, so nothing silently vanished.
    • Beside the clauses (not scored): the pillar now offers "Customize" (Studio: customize a read-only package's flow into <base>.custom under its own name — one ledger switch, the declaration's badge, "Restore package original" #28570, ADR-0146 D2/D9). Pressed, it sent nothing; a Label edit then sent POST /packages/com.example.showcase/ensure-customization 200 + PUT /meta/flow/showcase_urgent_task_alert?mode=draft&package=com.example.showcase.custom&surface=studio 200, the aside saying "Edits save as a draft of this deployment's customization; Publish applies them."; base GET md5 unchanged; draft discarded via POST /packages/com.example.showcase.custom/discard-drafts → discardedCount 1, draft read md5 back to baseline.

    studio-authoring.doc-on-app-menu r9 — PASS

    Baseline (step 1): GET /meta/book → [showcase_manual (public)], /meta/doc → 9 docs, /meta/app?id=showcase_app → 0 type:"doc" entries (knownGaps[0] holds). Fixture, all in com.qa.r6doc over the metadata API with ?package= then publish-drafts: docs qsixd_gated_doc / qsixd_open_doc / qsixd_open_doc2 authored and published BEFORE the books; books qsixd_a_empty (label "AAA Empty Book", claims none — first by label), qsixd_gated (audience {permissionSet: showcase_auditor}, claims qsixd_gated_doc), qsixd_org (audience org, claims both open docs); app qsixd_app nav {doc}, {book}, {book,doc} over the gated and open corpus. Reader r6reader@example.com holds showcase_auditor via a sys_user_permission_set row (201); outsider r6outsider@example.com holds none; both admin create-user mustChangePassword:false, identities proved with get-session before each cell; each in its own browser context.

    • A0 pass: reader /meta/app?id=qsixd_app nav = [n_doc_g, n_book_g, n_bookdoc_g, n_doc_o, n_book_o, n_bookdoc_o]; outsider = [n_doc_o, n_book_o, n_bookdoc_o] (gated doc / book / book+doc pruned). Server beside it: outsider GET /meta/doc/qsixd_gated_doc and /meta/book/qsixd_gated/tree → 403 PERMISSION_DENIED "This documentation is limited to holders of a permission set you do not have"; reader 200.
    • A1 pass: after a screenshot, rendered menu reader = ["Gated Page","Gated Book Entry","Gated Page In Book","Open Page","Org Book Entry","Open Page Two In Book"], outsider = ["Open Page","Org Book Entry","Open Page Two In Book"]; the labels map 1:1 to each persona's /meta/app ids (empty diff both ways).
    • A2 pass: reader: Gated Page, Gated Book Entry and Gated Page In Book → /_console/apps/com.qa.r6doc/docs/qsixd_gated/qsixd_gated_doc, h1 "Gated Doc", sidebar "Gated Book · MAIN · Gated Doc" (= reader's tree [main:[qsixd_gated_doc]]), not "AAA Empty Book"; Open Page / Org Book Entry → …/docs/qsixd_org/qsixd_open_doc, Open Page Two In Book → …/qsixd_org/qsixd_open_doc2, sidebar [Open Doc, Open Doc Two] = tree. Flat permalinks as reader: /docs/qsixd_gated_doc → /docs/qsixd_gated/qsixd_gated_doc, /docs/qsixd_open_doc2 → /docs/qsixd_org/qsixd_open_doc2. Outsider on the open book: sidebar [Open Doc, Open Doc Two] = outsider's tree, no Gated Doc.
    • A3 pass: outsider, after a screenshot of each: /docs/qsixd_gated_doc, /docs/qsixd_gated, /docs/qsixd_gated/qsixd_gated_doc and the three under /apps/com.qa.r6doc/docs/… all render the lock heading "You do not have access to this documentation" + "This documentation is limited to holders of a permission set you do not have" (server 403 for the same names); control /docs/qsixd_nosuch_name and /apps/com.qa.r6doc/docs/qsixd_nosuch_name → "Documentation not found … No book or document named qsixd_nosuch_name is installed." (GET /meta/doc/qsixd_nosuch_name → 404).
    • N0 pass: no gated doc entry drawn for the outsider (A0/A1).
    • N1 pass: no "Documentation not found" on any of the six 403 routes.
    • N2 pass: no blank region or crash on any of the eight direct routes (each has a heading and the reason text).
    • N3 pass: every page entry opened in its claiming book (A2).
      Teardown (step 8): DELETE app, 3 books, 3 docs ?package=com.qa.r6doc → 200 each; the set assignment row deleted 200; corpus re-reads as baseline (books [showcase_manual], 9 docs, 0 doc nav entries); reader's GET /meta/doc/qsixd_gated_doc → 404.

    studio-authoring.held-edit-leave-guard r5 — PASS

    Retest of wave-3 A1 FAIL (#27464, closed): HOLDS — nav rename + reload now raises beforeunload (2 of 2 fresh loads). Driver: Playwright, form sign-in, ref/role-targeted clicks, page.on('dialog') registered before every departure and answered deliberately (dismiss = Stay, accept = Leave), a fresh page + fresh held edit per departure cell; departures: (a) pillar link getByRole('link',{name:‹pillar›}), (b) rail item, (c) header package button "QA R5" → "QA R5 B", (d) [data-testid=studio-home], (e) page.reload(). Server truth from Node per cell: GET /api/v1/meta/_drafts?packageId=com.qa.rfive + GET /api/v1/meta/TYPE/NAME?state=draft&package=com.qa.rfive. Package fixture: object qfive_asset (+ qfive_bare), flows qfive_flow/qfive_flow2, app qfive_app (nav dashboard entry), dashboard qa_lifecycle_probe, permission qfive_editor, second package com.qa.rfiveb.

    • A0 pass: confirm "You have unsaved changes. Leave this page anyway?" fired on Stay AND on Leave in every held/dirty in-app cell — Data picklist (strip "Not saved yet: the field “Priority” needs at least one option. Your changes are kept here and saved once it is filled in.", no PUT) a/b/c/d; Data lookup ("…“Linked Thing” needs the object it links to") a; Automations step ("Object on the step “Get record” needs a value") a/b (rail → QF Flow Two); Interfaces widget ("Dataset on the widget “New bar chart” needs a value") a; Access matrix tick (dirty, departure 4–6 ms after the edit) a/d; Interfaces nav rename (dirty, 306–309 ms after typing) a/d. Clean controls (no edit): Data a/b/d, Automations a, Interfaces a/d, Access a/d → 0 dialogs, navigation went straight through.
    • A1 pass: page.reload() raised a dialog of type beforeunload on Stay and on Leave for picklist, lookup, step, widget, Access tick (gap 4–6 ms) and nav rename (gap 306–309 ms; the Studio Interfaces: an in-progress navigation rename does not raise the beforeunload guard, so a reload or tab close silently loses it #27464 retest); clean reloads of Data, Automations, Interfaces and Access raised none.
    • A2 pass: after every Stay the URL was unchanged and the held strip still showed (held cells) / the box stayed checked (Access); completing the edit saved it: picklist option High → PUT …/object/qfive_asset?mode=draft&package=com.qa.rfive&surface=studio 200 seq 52 body priority:{type:"select",label:"Priority",options:[{value:"high",label:"High"}]}; lookup target QF Bare → PUT 200, body linked_thing:{type:"lookup",reference:"qfive_bare"}; step Object = QF Bare → PUT …/flow/qfive_flow… 200 seq 54 node {type:"get_record",config:{objectName:"qfive_bare"} }; widget Dataset Task Metrics + measure task_count → PUT …/dashboard/qa_lifecycle_probe… 200 seq 55 widget {id:"widget_1",type:"bar",dataset:"showcase_task_metrics",values:["task_count"]}; Access Stay → autosave draft qfive_asset:{…allowDelete:true}; nav Stay → rename committed and saved (history seq 50 create / 51 update). Each completion draft was discarded after its read.
    • A3 pass: every Leave cell read _drafts {"drafts":[]} before and after and the item draft 404 NO_DRAFT before and after (picklist a–e, lookup a/e, step a/b/e, widget a/e, Access a/d/e); nav-rename Leave a/d/e: the pre-existing app draft (from the Stay cells) unchanged — GET /meta/app/qfive_app/history gained no row after 04:22:20Z, label still "QA Dash Renamed Lost Lost". Leave URLs: /automations, /data?surface=object:qfive_bare, /com.qa.rfiveb/data, /_console/studio, reloaded page with the held strip gone.
    • N0 pass: no departure dropped a held edit without a prompt (all held cells prompted).
    • N1 pass: no prompt on any clean pillar (12 clean cells, 0 dialogs).
      Observation (not a clause): the nav rename shows no "Not saved yet: the rename of the navigation item …" strip (engine.studio.held.navRename exists in i18n) — the guard fires regardless; recorded under "not filed".

    studio-authoring.flow-edge-type-authoring r4 — PASS

    Fixture: com.qa.r6flow, object qsixf_ticket (published), flow qsixf_edges Start→Push(http)→Check(decision)→End A / End B, Push→End Err, Check→Revise (conditional) and an untyped Revise→Push loop added as a draft over a published baseline (live baseline had no loop). Edges selected in /_console/studio/com.qa.r6flow/automations through each edge's hit path (svg path holding ‹title›Check → End A‹/title>, element-dispatched click), inspector controls by role/label.

    • A0 pass: autosave PUT /meta/flow/qsixf_edges?mode=draft&package=com.qa.r6flow&surface=studio 200 per choice: Check→End A Type Conditional → "type":"conditional"; Push→End Err Fault (error path) → "type":"fault"; Revise→Push Back-edge (revise loop) → "type":"back" (the canvas's "Cycle detected …" banner cleared); Start→Push set Fault then Normal → the second PUT carries {"id":"e_start","source":"start","target":"push"} with no type key. Type options offered: Normal, Conditional, Fault (error path), Back-edge (revise loop).
    • A1 pass: GET ?state=draft&package= → e_a {"type":"conditional","label":"Open path","condition":{"dialect":"cel","source":"record.status == 'open'"} } (inspector showed the same), e_b "isDefault":true (checkbox read checked), _diagnostics {valid:true}.
    • A2 pass: after the accepted publish (Publish → Publish all → publish-drafts outcome published 1), GET /meta/flow/qsixf_edges serves edges identical to the draft read (fault / conditional+label+condition / isDefault / back; e_start no type).
    • A3 pass: condition set to record.name == → autosave 200; after a screenshot, the inspector shows data-testid flow-edge-server-error "invalid CEL predicate: Unexpected token: EOF > 1 | record.name == ^ — predicates are bare CEL …", the Problems tab reads "Problems 1" listing it at "Check → End A · SCHEMA"; draft read _diagnostics {valid:false, errors:[{path:"edges.3.condition", code:"expression-invalid", …}]}. (The Problems half, silent in wave 3, now counts it.)
    • A4 pass: Studio Publish all → POST /api/v1/packages/com.qa.r6flow/publish-drafts 200 outcome "refused", failed [{flow qsixf_edges, code INVALID_METADATA, issues[{rule:"expression-invalid", path:"flows[0].edges[3].condition", where:"flow 'qsixf_edges' · edge 'e_a' (check→end_a) condition"}]}], the dialog reads "Not published: the server refused this draft."; live flow body md5 identical before and after the refusal (1270758f…); after fixing the condition the inspector error cleared and publish → published.
    • N0 pass: every inspector choice wrote its member (A0); a forged PUT with edges[6].type "loopback" → 422 INVALID_METADATA "Invalid option: expected one of "default"|"fault"|"conditional"|"back"", nothing stored (draft read 404 NO_DRAFT).

    studio-authoring.summary-field-authoring r5 — PASS

    Fixture: com.qa.r6sum, qsixs_parent (public_read_write) + qsixs_child (controlled_by_parent; name/memo text, due datetime, amount number, price currency, parent master_detail required), published 200.

    • A0 pass: /_console/studio/com.qa.r6sum/data → Sum Parent → Form → Add field "Child Count", type Rollup → Create: 0 non-GET /api/v1 requests (trace) and the banner "Not saved yet: Child object on the field “Child Count” needs a value. Your changes are kept here and saved once it is filled in."; after Child object = Sum Child still 0 PUTs and the line names "Aggregation …needs a value"; only after Aggregation = Count → one PUT /meta/object/qsixs_parent?mode=draft&package=com.qa.r6sum&surface=studio 200 carrying "child_count":{"type":"summary",…,"summaryOperations":{"object":"qsixs_child","function":"count"} }. Same held shape on a second fresh load ("Child Count a", 0 PUTs) and for "Child Sum" (held "Child field to aggregate … needs a value" until the field was picked). Retest of Studio: adding a Rollup field autosaves it unconfigured, and the Sum/Avg child-field picker offers text and lookup fields #27461 — fixed live.
    • A1 pass: after a screenshot confirmed the open picker, Aggregation=Sum child-field options = ["Amount amount number","Price price currency"]; Average = the same two; text name/memo, datetime due and master_detail parent not offered. Retest of spec/Studio: a count rollup cannot be saved without summaryOperations.field, which the spec itself says is ignored for count #27462 — fixed live.
    • A2 pass: GET /meta/object/qsixs_parent?state=draft → child_count.summaryOperations {object:qsixs_child,function:count} (no field key), child_sum {object:qsixs_child,function:sum,field:amount}; publish-drafts → published 1; live read carries the same two.
    • A3 pass: POST /api/v1/data/qsixs_parent + 5 children (amount 10..50) → GET parent child_count:5, child_sum:150; PATCH one child 10→110 and DELETE the 20 child → child_count:4, child_sum:230.
    • N0 pass: the unconfigured summary was never saved (A0 trace: no PUT until object+function(+field for sum)).

    studio-authoring.api-written-keys-survive-studio-save r3 — PASS

    Method per variant: API PUT /api/v1/meta/TYPE/NAME?mode=draft&package=com.qa.rfive writing the keys → GET …?state=draft&package=com.qa.rfive kept → one Studio edit (form sign-in, Playwright, autosave PUT body captured off the request) → draft re-read and deep-diffed → POST /api/v1/packages/com.qa.rfive/publish-drafts → live GET /api/v1/meta/TYPE/NAME.

    • A0 pass: deep diff kept-draft → post-Studio draft per variant has exactly one change each: object label "QF Asset" → "QF Asset Renamed" (Settings › Basic info); field fields.notes.inlineHelpText undefined → "QA help text R5" (field inspector Help text); flow nodes[1].label "Set X" → "Set X Renamed" (canvas node); app navigation[1].label "QA Dash" → "QA Dash Renamed" (nav Edit, double-click rename); dashboard widgets[0].title → "REV2 Tasks by Status Studio"; permission objects.qfive_asset.allowEdit false → true plus an added empty fields: {} (matrix editor normalisation, no key lost). Every API-written key equal before/after: object titleFormat, indexes [{fields:[serial],unique:"organization"}], userActions {exportCsv:false,import:false}, editMode "page", searchableFields, publicSharing {enabled:true,maxExpiryDays:7}, imageField "photo"; field serial.maskingRule/searchable, cost.step 0.5/useGrouping false, price.currencyConfig {fixed,EUR}, notes.rows 7/sortable false/widget "markdown-lite", due.dueLike true; flow description, successMessage, errorMessage, variables (incl. y_api), errorHandling {retry,maxRetries 2}, requiredPermissions; app description, branding, areas, contextSelectors; dashboard requiredPermissions ["studio.access"]; permission description, isDefault true, packageId.
    • A1 pass: each Studio autosave PUT …?mode=draft&package=com.qa.rfive&surface=studio → 200 (seq 25, 26, 30, 32, 35, 39); every captured body carries every API-written key with the same value (only differences vs the kept draft: the edit itself and the server-added _diagnostics omitted; permission also fields:{}).
    • A2 pass: publish-drafts → 200 data.success:true, publishedCount:5 (object, flow, app, dashboard, permission); live GETs carry every key as written (object 18/18 incl. the Studio label + help text, flow 6/6, app 4/4, dashboard requiredPermissions + Studio title, permission 3/3 + allowEdit true).
    • N0 pass: no key dropped or nulled by any of the six Studio saves (A0/A1).
      Note (dashboard): the first attempt set requiredPermissions:["showcase.export_data"] (a capability the admin does not hold); the author's own draft read then answered 403 PERMISSION_DENIED, so the variant was re-run with studio.access (held via admin_full_access). See Probe findings.

    studio-authoring.dashboard-widget-and-filter-types r5 — FAIL

    Fixture: package com.qa.r3 (POST /api/v1/packages 201, writable), object r3_ticket (18 seeded rows over 3 statuses × 3 regions × 6 opened_on months, numeric score/hours, account lookup), dataset r3_ticket_metrics (PUT ?mode=draft&package=com.qa.r3 → publish 200; dims status/region/opened_on/account/score, measures ticket_count/hours_sum/avg_score), app r3_app whose navigation names dashboard r3_board.

    • A0 pass: all 20 types authored in Studio (Add widget → type → Dataset → Add field), 0 through the API; published board /_console/apps/r3_app/dashboard/r3_board (21 widgets, 0 role=alert, 21 dataset queries 200): bar 3 bars (590×350 SVG), horizontal-bar 3, column 3, line path 6 points = 6 months, area 6, pie 3 sectors, donut 3, funnel 3 trapezoids, scatter 18 symbols = 18 score buckets, treemap 3 rects, sankey (dims status+region) 7 links = 7 non-empty API cells + 6 nodes, combo 6 bars + 1 line on 2 y-axes, radar 1 polygon / 3 angle ticks, table 3 rows × 4 cols + Total 18/198/31.61, pivot 3 status rows × South/West/North + totals (= API cells); gauge "18 Tickets", solid-gauge "198 Hours", metric "18 Tickets", kpi "31.61 Avg Score", bullet "198 Hours" (= API totals). No collapsed frame.
    • A1 pass: Add-widget menu (after screenshot) lists 20: gauge, solid-gauge, metric, kpi, bullet, bar, horizontal-bar, column, line, area, pie, donut, funnel, scatter, treemap, sankey, combo, radar, table, pivot; a widget's Type picker lists 20: Bar Chart, Horizontal Bar, Column Chart, Line Chart, Area Chart, Pie Chart, Donut Chart, Funnel, Scatter Plot, Treemap, Sankey Diagram, Combo Chart, Gauge, Solid Gauge, KPI Metric, KPI, Bullet Chart, Radar Chart, Table, Pivot Table — the same 20 ChartTypeSchema members.
    • A2 pass: board filter bar screenshot shows "Status Sel: All" select, "Region Text" text input, "All time" date-range picker, "Score Num" number input, "Account Lookup: All" select (lookup options = 14 accounts from a server GROUP BY over showcase_account).
    • A3 pass: each change re-issued 20 dataset queries (every widget but the unbound one): select → {"status":"open"} (gauge 18→8), text "north"+Enter → {"region":{"$contains":"north"} } (gauge 6), date Last 90 days → {"opened_on":{"$gte":"2026-08-04","$lte":"2026-11-02"} } (gauge 10), number 40+Enter → {"score":40} (gauge 1), lookup Vandelay → {"account":"14hF3lvOO0IbhYRn"} (gauge 6); widget_6 carries explicit inspector bindings (status/region/opened_on/score/account) and maps the same; unbound widget_3 (filterBindings all five false, authored in the inspector) issued 0 queries and stayed "18 Tickets" across all five; Reset restored "Status Sel: All | All time | Account Lookup: All".
    • A4 fail: the literal empty-item case holds (Studio: "Add item" on a dashboard list property autosaves an empty item (422), and every later edit is refused and lost until reload #27459 fix holds): Add item → 0 PUTs; Description edit while held → 0 PUTs; filling Field → one PUT 200 carrying the item + the held description; label edit before Add item and the held-period description survive reload ("R3 Board A4" / "held edit desc"); reload while held raises the beforeunload prompt and Leave discards the held item and label edit with 0 PUTs. BUT authoring a filter's optionsFrom (step 3's lookup/select) is not held: typing optionsFrom.object autosaves optionsFrom:{object:"r3_ticket"} → PUT …/dashboard/r3_hold?mode=draft&package=com.qa.r3&surface=studio 422 "globalFilters.N.optionsFrom.valueField [invalid_type]; …labelField", the next dashboard Description and Label edits each PUT 422 for the same filter, the page shows "Changes not saved: the server refused this draft", beforeunload is not guarded (defaultPrevented false, no prompt) and after reload both edits are gone. See F-1.
    • N0 pass: no type rendered an empty or zero-height frame; every grouping type showed ≥3 buckets over the multi-bucket fixture.

    studio-authoring.report-authoring-options r3 — PASS

    Fixture: same package com.qa.r3 / dataset r3_ticket_metrics (dims status, region; measures ticket_count, hours_sum) / app r3_app; each report created in /_console/studio/com.qa.r3/interfaces → nav "+" → Report → name + dataset + measure → Save as draft (PUT /api/v1/meta/report/NAME?mode=draft&package=com.qa.r3&surface=studio 200, app nav PUT 200), type/rows/columns/order/chart/blocks set on the Studio properties panel (every completing PUT 200), then each published (POST /api/v1/meta/report/NAME/publish 200).

    • A0 pass: in the app (/_console/apps/com.qa.r3/report/…, 0 role=alert): R3 Tabular (tabular, rows status+region, hours_sum) → flat table "Status / Region / Hours" with 7 detail rows (Open/North/42 … Closed/West/27) and no group totals; R3 Summary (summary, rows status) → grouped rows Open 30 / Closed 27 / Pending 24 + Total 81; R3 Matrix (matrix, rows status, columns region, ticket_count) → cross-tab header "Status / North / South / West / Total", rows Open 3/3/2/8, Pending 3/—/2/5, Closed —/3/2/5, Total 6/6/6/18 (= API status×region cells); R3 Joined (blocks b_status ticket_count by status, b_region hours_sum by region) → two tables, "Status / Tickets" Open 8/Pending 5/Closed 5/Total 18 and "Region / Hours" North 72/South 45/West 81/Total 198.
    • A1 pass: summary runtimeFilter {"region":"west"}, order [{by:hours_sum,direction:desc}], chart {type:bar,xAxis:status,yAxis:hours_sum}; the page's own query carries runtimeFilter {"region":"west"}, order {"hours_sum":"desc"}; POST /api/v1/analytics/dataset/query {datasetName:r3_ticket_metrics, selection:{dimensions:[status],measures:[hours_sum],runtimeFilter:{region:west},order:{hours_sum:desc} }} → Open 30, Closed 27, Pending 24 = rendered rows in the same order (unfiltered would be Open 99/Pending 54/Closed 45; data API west sum open 30/closed 27/pending 24); the embedded bar chart draws 3 bars Open≈30, Closed≈27, Pending≈24 on a 0–32 axis, same order.
    • A2 pass: PUT /api/v1/meta/report/r3_rep_bad_chart?mode=draft&package=com.qa.r3 (joined + chart) → 422 path chart "a joined report draws no chart … Delete chart"; PUT …/r3_rep_bad_block?… (block b2 without dataset) → 422 path blocks.1.dataset "block b2 binds none … Bind the block to a dataset"; after each, GET ?state=draft → 404 NO_DRAFT and GET → 404 RESOURCE_NOT_FOUND (nothing stored).
    • N0 pass: matrix drew a row × column grid (not a flat table), tabular drew a flat list (not grouped), joined drew one table per block; the stored runtimeFilter is applied (query carries it; rows are the west subset 81 h, not 198 h).

    Item verdicts: dashboards.strict-widget-rejects-stray-keys PASS · dashboards.chart-type-matrix PASS · dashboards.global-filters-rescope PASS · studio-authoring.dashboard-widget-and-filter-types FAIL (A4) · studio-authoring.report-authoring-options PASS.
    Retests: #27460 (two-dimension sankey) HOLDS, on both the stock-dataset scratch board and the package board · #27445 (filterBindings key naming an undeclared filter) HOLDS · #27459 (empty Add item) HOLDS for its own shape, but a nested variant fails (F-1).
    Trap checks for every verdict above: stale-console-bundle is ruled out by construction (the Console is built from this same commit's ui/). For hydration-race, every DOM read came after a settle wait and after a screenshot showed the surface. For single-datapoint, every grouping widget read 3 or more buckets. For automation-input, clicks are role- or testid-targeted, and fills go through Playwright fill and then Tab or Enter; the server-received PUT bodies prove each value committed.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

    Per-clause verdicts, part 3 of 3 (text oracle evidence)

    Continuation of this record. One line per clause, A then N, in the scope order of the record body. Runner tag in the record fingerprint. Double braces inside quoted templates are spaced apart so the write door does not read them as tokens.

    studio-authoring.app-and-nav-properties r5 — PASS

    Fixture: com.qa.r6nav — published objects qsixn_project (5 rows: 3 open, 2 closed) and qsixn_task (1 row), view container qsixn_project with listViews.closed_projects (filter status=closed), permission set qsixn_reader (read project+task) assigned to member r6member@example.com (admin create-user, mustChangePassword:false; get-session positions ["org_member","everyone"]); admin get-session positions include "org_owner". App qsixn_app (package's first app) via PUT ?mode=draft&package= then publish: branding {primaryColor #E11D48, accentColor #0EA5E9, logo /_console/vite.svg}, contextSelectors [active_pkg → /api/v1/packages], areas area_main [a_closed viewName, a_owner visible "'org_owner' in current_user.positions" (bare string), a_ghost requiresObject qsixn_nosuch_obj] and area_ops [b_rec recordId+recordMode edit, b_filt filters {status:open}, b_badge badge HOT/secondary], top-level navigation with one entry per key; second app qsixn_hidden_app hidden:true. Each persona in its own browser context, form sign-in, identity proved in-page with get-session.

    • A0 pass: after screenshot, both personas: --brand-primary #E11D48 / --primary 347 77% 50%, --brand-accent #0EA5E9 / --accent 199 89% 48%, "New" button bg rgb(226,29,72), header img src /_console/vite.svg 150x150 loaded; sidebar "Area" switcher lists Main Area and Ops Area, Main Area shows its own entries (Area Closed View …), Ops Area shows Area Edit Proj 1 / Area Open Filter / Area Badged; context selector renders ("Account" with package options for admin; "Select Package Scope" for the member).
    • A1 pass (both personas): Area Closed View → /apps/com.qa.r6nav/qsixn_project/view/closed_projects "Closed Projects · 2" (= 2 closed rows); Area Edit Proj 1 → /qsixn_project/record/5Lm53JmErzZwudfs/edit "Edit Nav Project … Cancel Save"; Area Open Filter → /qsixn_project/data?filter[status]=open "Filtered by status = open … 3 records" (= 3 open rows of 5); Area Badged shows badge "HOT" with class "bg-secondary text-secondary-foreground" (secondary variant).
    • A2 pass: Area Ghost (requiresObject qsixn_nosuch_obj) absent from the rendered Main Area for admin and member (entries read ["Main Area","Ops Area","Area Closed View","Area Owner Only"] / [… "Area Closed View"]) while /meta/app serves it to both.
    • A3 pass: "Area Owner Only" (bare-string visible "'org_owner' in current_user.positions") present for admin, absent for the member, on two fresh loads each; no "visibility predicate could not be evaluated" console line. Retest of console: a bare-string CEL predicate (nav visible, page visibleWhen) runs on the legacy evaluator and fails, though the spec defines a bare string as CEL shorthand #27457 (wave-3 A3) — fixed live.
    • A4 pass: member GET /meta/app?id=qsixn_hidden_app → 200 with hidden:true; member App Switcher lists "Showcase, R6 Nav App" only; Home "Your apps 2 Showcase R6 Nav App"; avatar menu lists "R6 Hidden App"; /_console/apps/qsixn_hidden_app (its address: the name, since it shares a package with the first app) renders "R6 Hidden App … Hidden Projects … 5 records" twice. (The avatar entry itself opens the wrong app — probe P-1.)
    • A5 pass (requiresObject cell not judged, knownGaps[0]): /_console/studio/com.qa.r6nav/interfaces → Edit → each entry's inspector has Landing (Named view / Record deep-link / Filtered slice / Default), View, Record ID, Open mode, URL filters rows, Badge, Badge style, Order, VISIBLE WHEN (shows "'org_owner' in current_user.positions" for Owner Only, "Valid CEL"); no requiresObject control. Autosave PUT /meta/app/qsixn_app?mode=draft&package=com.qa.r6nav&surface=studio 200 per edit: badge "NEW2", badgeVariant "outline", order 9, visible "'finance' in current_user.positions", recordMode "view", recordId "_uzO9FPXnsvaOdmg", filters {status:"closed"}, viewName "qsixn_project.default"; the stored requiresObject survived these saves. Draft discarded afterwards (404 NO_DRAFT).
    • N0 pass: hidden:true kept the app reachable for the member (meta 200, URL renders) and off the switcher/launcher.

    studio-authoring.authored-page-slots-and-variables r3 — PASS

    Fixture: com.qa.r6pg — published object qsixp_item (Alpha/Beta/Gamma rows, no default record page), app qsixp_app (Items, Vars Page). Pages written through the API (?mode=draft&package=) and published with publish-drafts.

    • A0 pass: slots.tabs save → 422 with the ADR-0085 tombstone "page.slots.tabs was removed in @objectstack/spec 18.3.0 (ADR-0085) …" (step note confirmed). qsixp_item_record {type:record, object:qsixp_item, kind:slotted, isDefault:true, slots highlights/details/discussion → element:text} published; /_console/apps/com.qa.r6pg/qsixp_item/record/lV9JvqPWwX2NpmeG (screenshot first, two fresh loads) shows "SLOT-HIGHLIGHTS authored block" where the Owner/Amount highlights stood, "SLOT-DETAILS authored block" in the Overview body where "More details / NOTES" stood, and "SLOT-DISCUSSION authored block" inside the Activity card in place of the composer/feed; the un-overridden header ("Alpha Pg Item", Edit, ⋯) and the Overview/Activity tab strip are the defaults seen on the pre-page baseline.
    • A1 pass: qsixp_vars declares string name "" (source name_input), number count 3, boolean flag true, object obj {k:"v"}, array items ["a"], record_id rec "" (source rec_picker); after the first-render screenshot the six gated blocks read +VAR-STRING (page.name == '') +VAR-NUMBER (page.count > 2) +VAR-BOOL (page.flag == true) +VAR-OBJECT (page.obj.k == 'v') +VAR-ARRAY (size(page.items) > 0) +VAR-RECID (page.rec == ''), and six false-on-defaults controls (page.name != '', count > 5, flag == false, obj.k == 'w', size(items) > 1, rec != '') all absent; no console error. Retest of console: a bare-string CEL predicate (nav visible, page visibleWhen) runs on the legacy evaluator and fails, though the spec defines a bare string as CEL shorthand #27457 (wave-3 '"size" is not a function') — fixed live. Two fresh loads.
    • A2 pass: same page load (window marker kept): typing "hello" into the element:text_input → VAR-STRING disappears, CTRL-STRING appears; picking "Beta" in the element:record_picker → VAR-RECID disappears, CTRL-RECID appears; the other four blocks unchanged. Two fresh loads.
    • N0 pass: every slot override saved renders; no variable block ignored its predicate (12/12 reads match).

    studio-authoring.access-permission-set-lifecycle r4 — FAIL

    Fixture: com.qa.rsl with two published objects qrl_project / qrl_task (private OWD); read-only legs on stock com.example.showcase / showcase_contributor.

    • A0 pass: empty package → New permission set, typed display name "Project Editor" → Identifier field derived project_editor; "Save as draft" → PUT /api/v1/meta/permission/project_editor?mode=draft&package=com.qa.rsl&surface=studio 200, toast "Permission set “Project Editor” created"; GET …/project_editor?state=draft&package=com.qa.rsl → 200 {"name":"project_editor","label":"Project Editor",…,"_packageId":"com.qa.rsl"}.
    • A1 pass: New again over the DRAFT-held id → dialog notice "This package already has a permission set with the identifier “project_editor”: Project Editor. Open it, or choose another identifier." + "Open “Project Editor”", 0 non-GET requests, draft version unchanged (hmac 5751c4c0…); after publish (objects.qrl_project R/U), New again over the PUBLISHED id → same notice, 0 non-GET requests, live read byte-identical before/after (diff empty), draft read still 404 NO_DRAFT.
    • A2 pass: (screenshot-confirmed rail) "Proj" → [Project Editor/project_editor]; "qrl_view" → [Task Viewer/qrl_viewer]; "editor" → [Project Editor]; "zzqq" → No matches for "zzqq" + "Clear search"; empty package (before the first create) → main "No permission sets in this package yet." + rail "No permission sets yet — click “+” above to create one".
    • A3 fail: com.example.showcase › Access shows the "Read-only" chip (title "Read-only package — switch to or create a writable package to edit.", header link title "…The package is read-only: its permission sets can be viewed here, but not changed."), 192 matrix checkboxes all disabled, 0 enabled facet controls on System capabilities / Row-level security / Tab visibility — but the rail still offers New: data-testid=studio-rail-add "New permission set" is enabled, opens the dialog, and Save as draft → POST /api/v1/packages/com.example.showcase/ensure-customization 201 (packageId: com.example.showcase.custom, created:true) then PUT …/qr7_pair_set?mode=draft&package=com.example.showcase.custom 200, toast "Permission set “QR7 Pair Set” created" (2 fresh loads). By design under ADR-0146 D3 (Studio: a base and its <base>.custom as one surface — "customized" / "added" marks, "only customizations", the pair folded under its base, New lands in the pair #28572: "A New on the read-only base lands in the pair through the ensure door") → disposition stale spec (F-2).
    • A4 pass: forged PUT /api/v1/meta/permission/showcase_contributor?mode=draft&package=com.example.showcase (label HACKED, objects {}) → 403 ITEM_LOCKED "Cannot overlay 'permission' in package 'com.example.showcase': that package is read-only…"; forged PUT …/qr7_forged_new?mode=draft&package=com.example.showcase → 422 WRITABLE_PACKAGE_REQUIRED; showcase_contributor live read before/after cmp byte-identical; qr7_forged_new live 404 and draft 404 NO_DRAFT.
    • N0 pass: the create over the published project_editor was refused in the dialog with no request; the live set kept objects.qrl_project {allowRead, allowEdit} (byte-identical read).

    studio-authoring.access-facets-runtime r4 — PASS

    Fixture: com.qa.rsf, published qrf_ticket (public_read_write; status open/closed/blocked; amount) with an INLINE action qrf_restricted_echo (type script, requiredPermissions [showcase.restricted_ops], record_header/record_more/list_item) + qrf_note; rows T1 open/10, T2 closed/20 (admin-owned), T3 open/30, T4 blocked/40 (owned by member qr7o); set S = qrf_member_set (qrf_ticket R+U) assigned to member M = qr7m@example.com via sys_user_permission_set; control member qr7s (no set). Identity proved with GET /api/v1/auth/get-session before each persona leg (email qr7m / qr7s).

    • A0 pass: baseline M POST /api/v1/actions/qrf_ticket/qrf_restricted_echo → 403 "requires capability [showcase.restricted_ops] — caller is missing [showcase.restricted_ops]"; Studio System capabilities › Add capability › "Restricted Showcase Operations" → autosave PUT 200, draft "systemPermissions":["showcase.restricted_ops"]; published → M's record page (/_console/apps/account/qrf_ticket/record/…) shows "Restricted Echo" and the action answers 200 {"ok":true,"record":"S_ET2QhxAhNbRqU-"}; chip toggled off, published (systemPermissions: []) → 403 again and the record page buttons read ["Edit","Overview","Activity"] (no Restricted Echo).
    • A1 pass: draft rowLevelSecurity = [{name:qrf_open_only, object:qrf_ticket, operation:select, using:"record.status == 'open'", enabled:true}, {name:qrf_upd_guard, object:qrf_ticket, operation:update, using:"record.amount >= 0", check:"record.status != 'blocked'", enabled:true}] exactly as typed; Test dialog on policy 0: {status:open} → "Allowed", {status:closed} → "Denied".
    • A2 pass: published → M GET /api/v1/data/qrf_ticket → [T1 open, T3 open] = admin's 4 rows filtered by status=='open'; M PATCH …/T1 {"status":"blocked"} → 403 PERMISSION_DENIED "You are not allowed to save this record with the values you entered…" (row still open); PATCH …/T1 {"amount":11} → 200 (amount 11 read back); RLS read policy switched off in Studio (draft enabled:false) and published → M reads all 4 rows.
    • A3 pass: Tab visibility › Add tab, typed showcase_app, picker Hidden → autosave PUT 200, draft "tabPermissions":{"showcase_app":"hidden"} (key is the whole app name).
    • A4 pass: M GET /api/v1/me/apps → apps [account] only (control qr7s → [showcase_app]); M's Home (screenshot-confirmed) "Your apps 0", no Showcase tile; control qr7s Home "Your apps 1 · Showcase".
    • N0 pass: every authored facet changed M's runtime (capability 403→200→403, rows 4→2→4, CHECK 403/200, app hidden from /me/apps and Home).

    studio-authoring.access-owd-and-suggestions r5 — PASS

    Fixture: com.qa.rso, published qro_project (private), qro_step (private, master_detail project → qro_project, not required), qro_doc (private, no master); rows P1, S1, D1, D2; isDefault sets qro_default_set (read qro_doc) and qro_default2 (read qro_project) written via PUT ?mode=draft&package=com.qa.rso and published; member M = qr7s@example.com (no set of the package), identity proved by GET /api/v1/auth/get-session before its reads.

    • A0 pass: Access › Record sharing (OWD) lists all 3 package objects ("QRO Doc … Private — owner only", "QRO Project …", "QRO Step …"); qro_doc → Public read: row shows "Unsaved", button "Save 1 change(s)"; Save → exactly one request PUT /api/v1/meta/object/qro_doc?mode=draft&package=com.qa.rso&surface=studio 200 carrying "sharingModel":"public_read", none for qro_project / qro_step.
    • A1 pass (Studio OWD panel: saving "Controlled by parent" on a detail whose master_detail is not yet required answers 409 DESTRUCTIVE_CHANGE with no way forward #27458 retest): picker offers "Controlled by parent — inherited from the master record"; on qro_step it names the master "By parent → qro_project"; on qro_doc it reads "By parent — no master: the object needs exactly one required master-detail field" and Save → PUT …/qro_doc… 422 INVALID_METADATA "declares sharingModel 'controlled_by_parent' but no required master_detail field…" shown in the panel, qro_doc draft stays public_read (nothing written), undo → "No changes" and the banner clears; on qro_step Save → 409 DESTRUCTIVE_CHANGE now opens the "Make the relationship required?" dialog naming "project → qro_project"; "Make required and save" → PUT …/qro_step?force=true&mode=draft&package=com.qa.rso&surface=studio 200, toast "Saved 1 object draft(s)."; publish-drafts → live qro_step sharingModel: controlled_by_parent, field project {type: master_detail, reference: qro_project, required: true}; qro_doc public_read, qro_project private unchanged.
    • A2 pass: after publishing qro_default_set, GET /api/v1/security/suggested-bindings?packageId=com.qa.rso → status pending (anchor everyone); pillar reads "This package suggests granting "qro_default_set" to all signed-in users (Everyone)." with Grant / Dismiss; M GET /api/v1/data/qro_doc → 403 PERMISSION_DENIED; Grant → POST …/suggested-bindings/sug_…/confirm 200, status confirmed, pillar "…was accepted for all signed-in users (Everyone)." + Revoke; M → 200 with rows; Revoke → POST …/dismiss 200, status dismissed, pillar "…is not granted: the suggestion was dismissed or revoked."; M → 403.
    • A3 pass: qro_default2 published → pending; Dismiss → POST …/dismiss 200, toast Suggestion for "qro_default2" dismissed., status dismissed, pillar "qro_default2" is not granted: the suggestion was dismissed or revoked. (also after reload); M GET /api/v1/data/qro_project → 403 before and after.
    • N0 pass: Grant moved M 403 → 200 and Revoke moved it back to 403 immediately (no publish in between).

    studio-authoring.no-entry-property-matrix r4 — FAIL

    • A0 pass: every row's key was read back from GET /api/v1/meta/TYPE/NAME?state=draft&package=app.qar8 and from live GET /api/v1/meta/TYPE/NAME after POST /packages/app.qar8/publish-drafts → outcome "published", carrying the written value (byte-equal compare per key). Rows whose variant requires a refusal were refused as written (unknown datasource 422 / publish outcome refused; split keys 422 "was removed in @objectstack/spec 18.3.0 (ADR-0050)"; indexes unique:true 422 ADR-0120 D1, re-authored unique:'global').
    • A1 fail: 5 rows lack their runtime effect: object.titleFormat (F-1), field.searchable (F-2), view.list sharing.lockedBy (F-3), the dataset row's report render, where the null bucket is not labelled (F-4), and view.form drawerSide/drawerWidth (F-5). All other rows show their effect (see below).
    • A2 fail: 4 keys are stored, served and silently inert. None was refused at save or publish: titleFormat (when the object has any eligible title field), field.searchable, list-view sharing.lockedBy, form-view drawerSide/drawerWidth.
    • N0 fail: PD#10 class present: the same 4 keys are declared, accepted by the draft door, served by publish, and ignored by the runtime (F-1, F-2, F-3, F-5).

    A0 — stored as written and served (row · door · observed · verdict)

    • object.managedBy / ownership org / ownership business_unit / systemFields audit / systemFields tenant / activityMilestones / displayNameField / indexes unique:'global' / datasource+external / fileAccessDelegate / sharing.controlledByParent.ownerMayEdit / defaultRowScope+membershipKeys / searchableFields / publicSharing / attachedOnRead / listViews / userActions / editMode / imageField / titleFormat · meta draft → publish → live · each draft value == live value · pass (one row each; values in the row log)
    • object.datasource unknown · draft PUT, live PUT and publish · 422 INVALID_METADATA at datasource / publish outcome refused · pass (refusal is the row's A0)
    • field.format autonumber / storage.notNull / multiple / internal / maskingRule / deleteBehavior restrict (lookup + master_detail) / accept / maxSize / requiredPermissions / referenceVia / inlineEdit+inlineTitle+inlineColumns+inlineAmountField / relatedListColumns+relatedListFilter+relatedListTitle / relatedList / dueLike / widget / rows / step / currencyConfig / lookupColumns / language / useGrouping / sortable / searchable (two fixtures) · meta door · draft == live · pass
    • flow.description+successMessage+errorMessage / variables.defaultValue / requiredPermissions / errorHandling / nodes[].inputSchema · meta door · draft == live · pass
    • view.list ×21 keys (searchableFields … emptyState) and sharing.lockedBy · meta door · draft == live per view · pass
    • view.form groups (folded to sections on parse, a declared alias) / subforms / submitBehavior / modalSize / allowSkip / showStepIndicator / buttons / layout / drawerSide / drawerWidth / defaults / defaultTab / tabPosition · meta door · draft == live · pass; splitDirection/splitSize/splitResizable · PUT 422 refused at parse · pass
    • dashboard.requiredPermissions · sys_metadata draft row (the author-admin's own draft read answers 403: the admin lacks the capability) + live read as holder m2 · key present both · pass
    • dataset include / dimensions.nullLabel / measures / currency · meta door · draft == live · pass
    • permission.description / isDefault / allowExport / rowLevelSecurity[].positions · meta door · draft == live · pass

    A1 — runtime effect (row · door/surface · observed · verdict)

    • object.managedBy append-only · POST/PATCH/DELETE /data · 403 engine-owned · pass
    • object.ownership org / business_unit · sqlite columns + created row · no owner_id / only owning_business_unit_id · pass
    • object.systemFields audit:false / tenant:false · sqlite columns · no created_by/updated_by / no organization_id · pass
    • object.datasource showcase_external + external · GET/POST /data · remote rows; 403 EXTERNAL_WRITE_FORBIDDEN · pass
    • object.datasource unknown · draft/live/publish · 422 / outcome refused INVALID_METADATA path datasource · pass
    • object.attachedOnRead · publish · visible reading can_act published; reading viewer.nope refused · pass
    • object.fileAccessDelegate · GET /storage/files/:id/url as m1 · 403 FILE_DOWNLOAD_DENIED vs control 200 · pass
    • object.activityMilestones · sys_activity · milestone row written · pass
    • object.displayNameField · sys_activity + detail H1 · "Title Value" · pass
    • object.listViews · console list · named view with its columns and filter · pass
    • object.sharing.controlledByParent.ownerMayEdit · PATCH as owner · 200 vs control 403 · pass
    • object.defaultRowScope + membershipKeys · member LIST / PATCH · roster rows only; out-of-scope 404 · pass
    • object.userActions · console list · no New / Import · pass
    • object.editMode page · console detail Edit · navigates to …/record/‹id›/edit · pass
    • object.searchableFields · $search · only declared field scanned · pass
    • object.titleFormat · console detail H1 + list · "ZX-42", not "ZX-42 @ Lyon" · FAIL (F-1)
    • object.imageField · console detail · uploaded picture in header · pass
    • object.indexes unique:'global' · PRAGMA index_list + duplicate POST · unique index; 409 UNIQUE_VIOLATION · pass
    • object.publicSharing · POST /share-links · 201 vs control 422 SHARING_NOT_ENABLED · pass
    • field.format autonumber · stored values · GD-0003 on third row · pass
    • field.storage notNull · POST without value · 400 · pass
    • field.multiple lookup · API + console · id array; two chips · pass
    • field.useGrouping false · console · "12345" beside "12,345" · pass
    • field.accept / maxSize · upload + attach · 400 ERR_FILE_CONSTRAINT both · pass
    • field.referenceVia · console detail (details grid) · referenced record card · pass
    • field.inlineEdit/inlineTitle/inlineColumns/inlineAmountField · Edit dialog · titled grid, columns, total · pass
    • field.relatedListColumns/relatedListFilter/relatedListTitle · detail Related tab · columns, filtered rows, title · pass
    • field.relatedList false · detail · no related list · pass
    • field.dueLike · console list + detail · text-red-600 overdue · pass
    • field.widget textarea · Edit · TEXTAREA · pass
    • field.internal · GET / ?select= · key omitted · pass
    • field.requiredPermissions · m1 vs m2 · omitted + 403 vs read/write 200 · pass
    • field.maskingRule phone · API + detail + Edit save · masked; DB keeps clear value · pass
    • field.searchable · $search / $searchFields · non-searchable fields scanned; searchable:true code field "not searchable" 400 · FAIL (F-2)
    • field.rows / step / currencyConfig / sortable / lookupColumns / language · Edit / detail / list · rows=9; step=0.25; "€1,234.50"; header not sortable vs sortable sibling; picker sub-columns; sql placeholder · pass
    • field.deleteBehavior restrict · DELETE parent · 409 DELETE_RESTRICTED (lookup and master_detail) · pass
    • flow.description/successMessage/errorMessage · trigger · 200 successMessage; 400 FLOW_FAILED errorMessage · pass
    • flow.variables defaultValue · trigger → written record · default written · pass
    • flow.requiredPermissions · trigger as m1 / m2 · 403 / 200 · pass
    • flow.errorHandling retry maxRetries 2 · runs · 3 failed attempts · pass
    • flow.nodes[].inputSchema · served · stored+served (row asks no more) · pass
    • view.list searchableFields / userFilters / resizable / compactToolbar / selection / pagination / rowHeight / grouping / rowColor / hiddenFields / fieldOrder / rowActions / bulkActions / conditionalFormatting / inlineEdit / exportOptions / appearance / addRecord / showRecordCount / allowPrinting / emptyState · console named view vs control view · each renders as described (per-key lines in the row log) · pass ×21
    • view.list sharing.lockedBy · console named view · nothing rendered · FAIL (F-3, knownGaps[0])
    • view.form groups / subforms / submitBehavior / modalSize / allowSkip / showStepIndicator / buttons · New dialog or /forms page · each renders as described · pass ×7
    • view.form split keys · PUT · 422 retired · pass
    • view.form layout horizontal · New, Edit, record edit page, /forms · labels beside fields · pass (knownGaps[0] stale)
    • view.form drawerSide / drawerWidth · New dialog, /forms · identical to control, no drawer surface exists for a form view · FAIL (F-5, knownGaps[0])
    • view.form defaults / defaultTab / tabPosition · 3–4 surfaces each · applied everywhere measured · pass (knownGaps[0] "partial" not reproduced)
    • dashboard.requiredPermissions · GET /meta/dashboard as admin/m1/m2 · 403/403/200 · pass
    • dataset (include, nullLabel, measures, currency) · /analytics/dataset/query + console report · server pass; report shows null bucket "—" instead of "No region (R8)" · FAIL (F-4)
    • permission.description · Setup permission list · "R8 PS DESCRIPTION TEXT" · pass
    • permission.isDefault · /security/suggested-bindings · pending suggestion · pass
    • permission.allowExport · export as m2 / m1 · 200 / 403 EXPORT_NOT_PERMITTED · pass
    • permission.rowLevelSecurity[].positions · m1 vs m2 LIST · policy limited to position holder · pass
    • mailAssociation / ackPlaintextMasking / lifecycle.ttl / dimensions · — · not-run (knownGaps[1] "NOT JUDGEABLE HERE", cited)

    A2 — a key the runtime cannot honour is refused, never silently inert (row · door · observed · verdict)

    • knownGaps[0] rows: view.form layout 'horizontal' · rendered on all 4 surfaces · no longer a gap → pass (accuracy finding); drawerSide/drawerWidth · accepted 200, published, inert · FAIL (F-5); defaults/defaultTab/tabPosition · applied on all surfaces measured · pass; view.list sharing.lockedBy · accepted, published, inert · FAIL (F-3)
    • New: object.titleFormat · accepted, published, inert whenever any eligible title field exists (the registry stamps a nameField that outranks it) · FAIL (F-1)
    • New: field.searchable · accepted, published, read by nothing · FAIL (F-2)
    • Keys the runtime does refuse: unknown datasource (422 / outcome refused), attachedOnRead undeclared key (refused at publish), split keys (422), indexes unique:true (422), report on an unknown dataset (refused at publish, probe) · pass
    • Every other row: effect observed under A1 → not inert · pass
    • (F-4 is a rendering defect on one surface; the key is honoured by the dataset answer, so it is scored under A1 only)

    N0 — PD#10 class (declared, not enforced) (row · observed · verdict)

    • object.titleFormat · stored+served, ignored by detail/list/global-search title resolution · FAIL (F-1)
    • field.searchable · stored+served, no reader in server or ui · FAIL (F-2)
    • view.list sharing.lockedBy · stored+served, no ui reader · FAIL (F-3)
    • view.form drawerSide / drawerWidth · stored+served, no reader on any form-view surface · FAIL (F-5)
    • all other rows · enforced (A1 pass) · pass

    Studio census (step 4, source-checked in ui/packages/app-shell/src/views): controls now exist for field.currencyConfig, field.relatedList, field.relatedListFilter (ObjectFieldInspector), view.list conditionalFormatting (ViewVariantInspector VIEW_CURATED_FIELDS + rule editor) and permission.allowExport (PermissionMatrixEditor "E" toggle). These move out of the matrix, which is not a fail. No control was found for any other key in the matrix. ObjectSettingsPanel's "searchable" is the object-level enable.searchable, not field.searchable.

    studio-authoring.uninstalled-package-refused r3 — PASS

    • A0 pass: GET /api/v1/packages/com.example.nosuch → 404 RESOURCE_NOT_FOUND; PUT /meta/flow/qa_ghost_flow?mode=draft&package=com.example.nosuch → 422 code WRITABLE_PACKAGE_REQUIRED "Cannot save flow 'qa_ghost_flow' into package 'com.example.nosuch': no package with that id is installed … Nothing was written."; read-back ?state=draft&package= → 404 NO_DRAFT, live → 404 RESOURCE_NOT_FOUND; active flow save also 422.
    • A1 pass: object (with sharingModel) / dashboard / page, each ?mode=draft and active → 422 WRITABLE_PACKAGE_REQUIRED naming com.example.nosuch, "Nothing was written."; every read-back (draft+pkg, live+pkg, live) → 404; a DB-wide LIKE '%qa_ghost%' scan of all 106 tables → 0 rows. Control: the same four bodies into installed com.qa.r6flow → 200 (so the 422 is the package rule, not a body error); drafts discarded.
    • A2 pass: POST /api/v1/packages/com.example.nosuch/publish-drafts → 404 RESOURCE_NOT_FOUND "Package 'com.example.nosuch' not found"; all four names live → 404; GET /meta/diagnostics stats.‹type›.packages contains com.example.nosuch for none of 30 types (raw body grep count 0).
    • A3 pass: /_console/studio/com.example.nosuch (redirects to …/data) and /…/data both render "Package not found. No package with the id "com.example.nosuch" is installed here, so there is nothing to build in." + "Studio home" button (click → /_console/studio); 0 New object / Create app buttons; /_console/studio/com.qa.r6flow/bogus_pillar renders "This page does not exist. Studio has no "bogus_pillar" section; pick Data, Automations, Interfaces, Access or All metadata above." (not Interfaces). Read twice on fresh loads, screenshot first.
    • N0 pass: no 200 for any write bound to com.example.nosuch (10 saves, all 422).
      Teardown: nothing was stored (every read 404, DB scan 0), nothing to delete.

    studio-authoring.expression-editors r8 — FAIL

    Fixture: com.qa.rsx, published object qrx_job (number est_hours/quantity/unit_price/a/b, select status, formula f_formula record.est_hours * 1.1 returnType text, formula f_line record.quantity * record.unit_price returnType text), published set qrx_job_editor with RLS [qrx_probe_policy select USING record.status == 'open', qrx_upd_policy update USING same + CHECK record.est_hours >= 0]. Engine truth: MCP validate_expression (POST /api/v1/mcp tools/call) for formula/conditional probes; POST /api/v1/meta/permission/qrx_job_editor/publish for RLS probes. Browser: Playwright chromium, form sign-in, Studio /_console/studio/com.qa.rsx/{data,access}.

    • A0 pass: Data › QRX Job › Form › F Formula, Formula (CEL) = record.est_hours * 1.1 → "Valid CEL" + "Result type: Number"; record.a + record.b → "Result type: Unknown — the engine cannot prove a single type. Wrap operands in double() / int() / string() to pin it…" (engine validate_expression formula: inferredType number / unknown — identical).
    • A1 pass: edited f_formula to record.est_hours * 1.2 → autosave PUT /api/v1/meta/object/qrx_job 200; draft and (after publish-drafts) live read f_formula returnType number (was text); f_line, opened in the inspector but not edited, keeps returnType text; an edit to the unprovable record.a + record.b cleared returnType (null) as the spec says.
    • A2 fail: tiers match on 8 of 9 probes (unknown field record.est_hourz*1.1 error "unknown field est_hourz on qrx_job — did you mean est_hours?" = engine unknown-field; bare est_hours * 1.1 error "…Write record.est_hours." = engine bare-reference; parse faults error = engine invalid-cel / rls-predicate-unparseable; non-pushdownable USING record.est_hours * 2 > 10 error "RLS using is not lowerable (unsupported operand "*")…" = publish 422 rls-predicate-unenforceable; CHECK record.f_formula > 5 "Valid CEL" = publish 200), but the formula-field USING record.f_formula > 5 reads "Valid CEL" (aria-invalid null) for ~1.7 s while the engine refuses it (publish → 422 rls-predicate-unenforceable "RLS using (INVALID_FIELD / 400)… a virtual formula field"); the error tier appears only after the autosave's re-read (~2.0 s). Scored fail per knownGaps[3] (Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482 open); disposition stale spec (F-1).
    • A3 pass: (screenshot-confirmed Visible When editor on qrx_job.notes) previous. and record. → listbox of 8 field options (organization_id … queue, the system fields first, cap 8); record.est → [est_hours]; previous.s → [status]; current_user. → no options; parent. → no options; bare est → no options (withheld); accepting the record.est option inserted record.est_hours; bare est_hours > 5 → aria-invalid=true "bare reference est_hours … Write record.est_hours." (engine validate_expression validation: bare-reference error).
    • A4 fail: non-pushdownable and parse-fault USING (two fresh loads each) → error finding, perm-autosave-held "Fix the CEL syntax errors before saving.", 0 PUTs in 7 s; but the formula-field USING record.f_formula > 5 (three fresh loads) shows no hold and issues PUT /api/v1/meta/permission/qrx_job_editor?mode=draft&package=com.qa.rsx&surface=studio → 200 carrying "using":"record.f_formula > 5"; each newly refused text (> 7) autosaves once more (200); only a sibling edit (CHECK of policy 1) while the judged text stands is held (0 PUTs) — the Studio: an expression the engine refuses (RLS USING on a formula field, a null-unsafe validation predicate) keeps autosaving — the server verdict is shown on the field but never holds the save #27482 residual. Disposition stale spec (F-1).
    • A5 pass: "Test policy" dialog: record.status == "open" + {status:"open"} → "Allowed"; + {status:"closed"} → "Denied"; record.est_hours * 2 + {est_hours:4} → "Non-boolean result 8 — a row filter should evaluate to true or false."; direct @objectstack/formula ExpressionEngine.evaluate of the same pairs → {ok:true,value:true} / false / 8.
    • A6 fail: parity table 8/9 — the formula-field RLS probe differs before the first autosave (editor ok vs engine error); all others equal (see A2). Disposition stale spec (F-1). (Engine side: validate_expression live answers above, not a pin run.)
    • A7 skipped: pin-only clause; the brief assigns the celAuthoring.test.ts pin to another card, and this runner does not run vitest in the shared tree.
    • N0 fail: same divergence as A2/A6 (formula-field USING: editor "Valid CEL" + PUT 200 vs engine 422 rls-predicate-unenforceable). Disposition stale spec (F-1).
    • N1 pass: parse-fault RLS USING record.status == → held, 0 PUTs (2 fresh loads); parse-fault formula record.est_hours * → aria-invalid, banner "Not saved yet: an expression on the field “F Formula” has an error to fix…", 0 PUTs, draft unchanged.
    • N2 pass: current_user. / parent. list none of qrx_job's fields; bare est_hours withheld from bare completion and flagged as an error.
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:devx#1 · os-warren · session_01JQwxbX9avDwM2goWd7zimE

    Close-out for W4-C3b1. Every fail has a disposition; product defects are extracted, stale clauses are owned by a card.

    Extracted: #29006 · studio-authoring.dashboard-widget-and-filter-types · acceptance[4]
    Extracted: #29007 · studio-authoring.no-entry-property-matrix · acceptance[1]
    Extracted: #29008 · studio-authoring.no-entry-property-matrix · acceptance[1]
    Extracted: #29009 · studio-authoring.no-entry-property-matrix · acceptance[1]
    Extracted: #29010 · studio-authoring.no-entry-property-matrix · acceptance[1]
    Extracted: #29012 · studio-authoring.no-entry-property-matrix · acceptance[2]
    Extracted: #29013 · studio-authoring.draft-publish-lifecycle · acceptance[10]
    Extracted: #29015 · studio-authoring.api-written-keys-survive-studio-save · observed beside the clauses
    Extracted: #29016 · automation.packaged-flow-clone-contract · observed beside the clauses
    Extracted: #29019 · studio-authoring.no-entry-property-matrix · observed beside the clauses
    Extracted: #29020 · records-forms.calendar-interactions · observed beside the clauses
    Extracted: #29021 · records-forms.form-view-gallery · observed beside the clauses
    Extracted: #29022 · studio-authoring.report-authoring-options · observed beside the clauses
    Extracted: #29023 · dashboards.global-filters-rescope · observed beside the clauses
    Extracted: #29024 · studio-authoring.app-and-nav-properties · observed beside the clauses
    Extracted: #29025 · studio-authoring.flow-edge-type-authoring · observed beside the clauses
    Parked-on: #28579 · automation.packaged-flow-clone-contract · re-point acceptance[1], acceptance[2], acceptance[6] (ADR-0146 D4)
    Parked-on: #28579 · studio-authoring.packaged-automation-studio-lock · re-point acceptance[0], acceptance[2] (ADR-0146 D3, D4)
    Parked-on: #28579 · studio-authoring.access-permission-set-lifecycle · re-point acceptance[3] (ADR-0146 D3)
    Parked-on: #27482 · studio-authoring.expression-editors · re-point acceptance[2], acceptance[4], acceptance[6], negative[0] once #27482 is ruled
    Parked-on: #27377 · checklist-accuracy findings and fixture gaps (reported to the wave anchor)

    Dispositions: 5 stale spec (R-1 to R-5), 2 product defect (R-6, R-7). No assertion defect, no invalid pairing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions