Skip to content

QA run · release-18.2.0-w2-manual-browser (34/34) · 756220c4 · 2026-10-27 · 30 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN #26456

Closed

Description

@objectstack-fleet

This is the QA run record for card W2-C3b of the 18.2.0 release sweep, wave 2 (wave anchor #25807; Version Packages PR #25640): the manual set, browser/mixed surface. It was run with the checklist-test skill following docs/qa/platform-checklist/RUNNER.md and the dogfood-verification skill. The maintainer ordered this wave directly as a test run; lane/seat dispatch protocol does not apply.

The scope is the unpinned P1 items whose source[] cites a file changed by a Clause-②: yes changeset in #25640, with P0 excluded (card W2-C1 owns P0). The set was named on the card, not resolved by one selector. Each id was resolved on its own with node scripts/checklist-select.mjs ID --json. All 34 came back runnable: none is blocked, none is planned, and none carries an automated.ref, so every clause was driven live. Every item has a verdict (34/34).

This record supersedes, for the 16 items it shares with it, the wave-1 manual-set record #25817 (old head 6679644a). The other 13 items of #25817 belong to the sibling card W2-C3a. Under the card's supersede protocol, whichever of the two cards finishes second closes #25817. When this record was filed, W2-C3a had filed no record, so #25817 stays open and the close is left to W2-C3a (see the close-out comment).

Environment fingerprint

  • Framework: 756220c4a7adcbb1228864607cb4b6a1ca486dc0, checked out detached. The tree was clean before and after. git status stayed empty throughout, and no runner wrote to the tree.

    • pnpm install --frozen-lockfile passed in 34 s.
    • pnpm build --concurrency=2 passed 119/119 in about 14 min.
    • node scripts/check-dev-prereqs.mjs was green: 108 package build artifacts present.
    • The tree is unreleased, so the CLI reports @objectstack/cli/18.1.0.
  • Console build: built in-repo from ui/ at that same commit. pnpm build built @object-ui/console (packages/console/dist/index.html 01:44Z). pnpm console:build --concurrency=2 then ran as a cache hit, 72/72 (71 cached). The .objectui-sha pin and pnpm objectui:build are retired. Every console observation was against this build, which rules out stale-console-bundle.

  • App and boot: examples/app-showcase, booted with the canonical line OS_PORT=PORT setsid pnpm -C …/examples/app-showcase exec objectstack dev --ui --seed-admin -p PORT -d file:SCRATCH/BOOT.db.

    • Every boot had its own free port in 43100–43700 and its own file DB.
    • Seed: --seed-admin (admin@objectos.ai) plus the showcase seed (132 rows; banner 32 flow(s) 19 bound · 8 draft).
    • Every server was stopped by process group, and a final lsof sweep found no listeners.
  • Special boots:

    • --no-seed-admin with OS_DISABLE_SIGNUP=true on a fresh DB: first-run owner bootstrap.
    • OS_DISABLE_SIGNUP=true: the signup-gate toggle leg.
    • OS_METADATA_WRITABLE=capability: probe only.
    • OS_SEARCH_PINYIN_ENABLED=false: the pinyin OFF leg.
    • SHOWCASE_WEBHOOK_SECRET set: the webhook intake leg of the node-type matrix.
    • A reboot with OS_LOCALIZATION_TIMEZONE=Europe/Paris and OS_MAIL_SMTP_PASSWORD: the settings env-lock legs and webhook activation survival.
    • An app-todo --preset minimal scratch copy: the messaging-absent 501 leg.
    • Scratch app copies (tar, without node_modules, .objectstack or dist): the capability shadow/malformed apps and the retired-webhook-trigger builds.
    • A published scaffold: npx -y create-objectstack@latest, which resolved to 18.1.0 through the proxy.
    • OS_AUTOMATION_SCHEDULED_WORK_ENABLED was not needed by any clause.
  • Tree side effects: examples/app-showcase/.objectstack/installed-packages/ never existed. examples/app-showcase/.objectstack/data/ appeared, empty, at the first boot (01:46:41Z), although every boot passed -d file:…. It is gitignored, and no runner installed a package.

  • Browser: Playwright chromium build 1243 (matching /opt/pw-browsers/chromium-1243), launched with chromium.launch() and default options. Each persona had its own context and signed in through the form, with both the localStorage auth-session-token and the better-auth.session_token cookie confirmed and the identity proven via GET /auth/get-session. At most three runners used a browser at once, and each held at most one browser instance.

  • Execution: six runner subagents, each with its own port range and DBs, ran in two staggered waves, never more than three at once. The batches were:

    • A: approvals
    • B: automation and formula
    • C: identity and access
    • D: studio-authoring and the permission matrix
    • E: records, search and UX
    • F: i18n, integration, settings, cli and audit

    Items ran sequentially inside each batch. The model tier was the default (opus): node scripts/pm/dispatch-gates.mjs --tier gives no path-derived mandate for this surface. The coordinator re-verified each extracted defect independently on a boot of its own (port 43700) or against the built package.

  • Wall clock: 01:29Z to 03:00Z (UTC, 2026-10-27).

Scope and verdicts

Labels: A0, A1, … are acceptance[0], acceptance[1], …; N0, … are negative[0], … (zero-based).

item priority surface rev verdict
access-security.audit-log-browser P1 mixed 7 PASS
access-security.public-form-intake P1 mixed 3 PASS
access-security.permission-matrix-edit-loop P1 mixed 4 PASS
access-security.capability-declaration-lifecycle P1 mixed 7 PASS
api-backend.formula-stdlib-matrix P1 mixed 4 PASS
approvals.per-group-signoff P1 browser 10 PASS
approvals.quorum-m-of-n P1 browser 7 PASS
approvals.inbox-metadata-actions P1 browser 6 PASS
approvals.decision-action-matrix P1 mixed 9 PARTIAL (A9 blocked(fixture), knownGaps[0])
automation.flow-run-step-nesting P1 mixed 8 PASS
automation.rollup-summary-filter P1 mixed 6 PASS
automation.flow-node-type-matrix P1 mixed 9 PASS
automation.flow-error-handling P1 mixed 6 PASS
cli.scaffold-console-first-paint P1 mixed 6 PASS
i18n.notification-localized-and-clears P1 browser 5 PASS
i18n.strict-translation-key-rejection P1 mixed 3 PASS
identity-auth.api-key-ui-lifecycle P1 mixed 4 PASS
identity-auth.org-membership-team-management P1 mixed 5 PASS
identity-auth.first-run-owner-bootstrap P1 mixed 3 PASS
identity-auth.self-signup-gate P1 mixed 6 PASS
integration-system.webhook-lifecycle P1 mixed 10 PARTIAL (A2 blocked(dependency) #23166; A4 partial: the cross-org and parked arms come from below-HTTP pins)
integration-system.notify-inbox-delivery P1 mixed 6 PASS
platform-core.settings-hub-roundtrip P1 mixed 5 PASS
records-forms.form-view-gallery P1 browser 4 PASS
records-forms.action-location-matrix P1 browser 15 PARTIAL (A3 fail-closed half and A4 half (a) blocked(fixture), knownGaps[2])
records-forms.field-group-visible-when P1 mixed 7 PASS
search.pinyin-flag-both-sides P1 mixed 6 PASS
search.console-global-search P1 mixed 5 PASS
studio-authoring.view-authoring-live P1 mixed 3 PASS
studio-authoring.draft-publish-lifecycle P1 mixed 8 PASS
studio-authoring.authoring-validation-not-persisted P1 mixed 4 PASS
studio-authoring.packaged-automation-studio-lock P1 browser 4 PASS
studio-authoring.doc-on-app-menu P1 mixed 6 PASS
ux-conventions.write-feedback P1 browser 6 FAIL (A1, the Mark Done row)

Derived: 30 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN. One clause failed: ux-conventions.write-feedback A1. Its reproduction rule is below, and it is a product defect in the showcase fixture's copy, extracted. There is no authentication or authorization finding, so nothing is withheld.

The #25822 retest (i18n.strict-translation-key-rejection, a probe beside the clauses that scores no clause):

  • The fix holds for the exact reproduction. PUT /api/v1/meta/translation/qa_bad_locale with locale: "not a locale!!" → 422 INVALID_METADATA (path locale, invalid_format), and the read-back is 404.
  • It also holds for zh_CN, for a value padded on either side, and for multi-word names (Simplified Chinese), at both the save door and defineTranslation / defineTranslationBundle. The control zh-CN saves 200.
  • One residual: a single-word language name of 2–8 letters (Chinese, English, Japanese) still saves 200 _diagnostics.valid:true and is accepted by both define functions. The changeset and the refusal text both say "a language name" is refused. This is extracted as its own card (see the close-out); it does not reopen spec: a translation item accepts any locale string ("not a locale!!" saves 200, valid) — LocaleSchema is a bare z.string() #25822.

Per-clause evidence (one line per clause; text oracles)

approvals.per-group-signoff (rev 10) · PASS

  • A0 pass: EXP-2001 reads pending.
    • pending_approvers holds two distinct ids (the admin and usr_showcase_auditor_demo).
    • pending_approver_groups maps admin→[manager] and Ada→[finance].
    • decision_progress per_group reads got 0 need 2, and the run is paused at dual_signoff.
  • A1 pass: the admin approves through the request page's dialog: POST …/approve → 200.
    • The request stays pending, and the slate is [Ada].
    • The groups read manager 1/1 satisfied:true and finance 0/1.
  • A2 pass: the chips match the API at all three stages: "0 of 2 groups" with finance 0/1 and manager 0/1, then "1 of 2 groups" with manager ✓, then Approved with no chips.
  • A3 pass: Ada, in her own session, approves → 200 approved.
    • The run goes paused → completed with [start, dual_signoff, mark_approved], and there is no return_to_draft step.
    • The report reads approved.
  • A4 pass: /actions holds submit, approve (admin) and approve (Ada), with via_override:false on both approves.
  • A5 pass: the admin's approve carrying actorId set to Ada → 403 FORBIDDEN "cannot act as …", and no row is written. With no actorId → 200, recorded against the session user.
  • N0 pass: unanimous INV-1012 (need 1) finalizes on one approve; per-group needed two.
  • N1 pass: Mei (in neither group) approving → 403 "not a pending approver", and no row is written.

approvals.quorum-m-of-n (rev 7) · PASS

The verdict rests on the runtime fixture, not the clamp. On a fresh boot:

  • Ada was bound to finance and Lena Legal (QA) to legal through sys_user_position (201 each). Lena was minted by invite then sign-up.
  • showcase_expense_signoff was toggled off and back on.
  • Two submitted reports (QA-Q1, QA-Q2) each carry a 6000 line. Each slate holds 3 distinct holders, with quorum got 0 need 2.

Clauses:

  • A0 pass (clamp contrast): EXP-DEMO need 1 finalizes on one approve, with exactly one approve row.
  • A1 pass: Q1 goes 0/2 → 1/2 (Ada's approve in the UI) → approved (Lena's approve in the UI).
    • There are 2 approve rows from 2 distinct actors.
    • The run completed with mark_approved.
  • A2 pass: on Q2, Ada approves (1/2), then Lena rejects in the UI → 200 rejected.
    • The slate empties, the run completes with return_to_draft, and the report reads draft.
    • A later approve → 409 INVALID_STATE.
  • A3 pass: after Q1 finalizes, pending_approvers=[] and the third holder is dropped.
    • The admin's My Pending count goes 4 → 3.
    • ?approverId=…&status=pending no longer lists Q1.
  • A4 pass: the page tally matches the API at each stage: "Approvals — 0 of 2 · 3 eligible approvers", then "1 of 2 · 2 eligible approvers", then Approved.
  • N0 pass: Ada's repeat approve and Mei's approve (no position) each → 403, and the tally holds at 1/2.
  • N1 pass: the clamp is not scored as M-of-N.

approvals.inbox-metadata-actions (rev 6) · PASS

Fixture: a plain member (sys_member.role = member) was reassigned onto EXP-DEMO.

  • A0 pass: the member opens My Pending, then the Request cell, which leads to the sys_approval_request record page.
    • The decision bar renders [Approve, Reject, Reassign, Send back, Request info, Reply].
    • That is exactly the set the served GET /meta/object/sys_approval_request declares for can_act on a pending request.
  • A1 pass:
    • The admin's own INV-1012 adds Send reminder and Recall.
    • The member's view of EXP-DEMO has neither.
    • The admin (override actor) on EXP-DEMO gets Override Approve/Reject/Reassign/Recall and no Send reminder.
  • A2 pass: the viewer flags predict each rendered set exactly from the declared CEL visible expressions:
    • member/EXP-DEMO: can_act T, is_submitter F, can_override F
    • admin/EXP-DEMO: F, F, T
    • admin/INV: T, T, T
  • A3 pass:
    • The member's Approve click → POST …/approve 200, and the request is approved.
    • The admin's Send reminder → POST …/remind 200, and the request stays pending.
  • A4 pass: one new approve row (the member) and one new remind row (the admin).
  • N0 pass: no button rendered against a denying flag in any of the three (viewer, request) pairs.

approvals.decision-action-matrix (rev 9) · PARTIAL

  • A0 pass: all nine variants → 200, and each appends exactly one /:id/actions row naming its actor and action. The variants are approve, reject, revise, resubmit, recall, remind, reassign, request-info (by the reassigned approver) and comment. The maxRevisions revise appends the designed [revise, reject "Auto-rejected: revision limit (2) exceeded"] pair.
  • A1 pass:
    • EXP-DEMO goes pending → approved, and its run goes paused → completed.
    • A budget reject (210000) → rejected, and its run has no needs_exec step.
    • A budget approve (160000) → approved, with a needs_exec step.
  • A2 pass: revise → returned, and the run pauses at wait_revision.
  • A3 pass:
    • A resubmit by a non-submitter → 403 "only the submitter may resubmit".
    • The submitter's resubmit → 200, a new pending id on the same flow_run_id / flow_node_id=manager_review.
  • A4 pass: R1 still reads returned after R2 opens.
  • A5 pass: the third revise → 200 rejected, and the run completes.
  • A6 pass:
    • A recall by a non-submitter → 403.
    • The submitter's recall → recalled, the slate empties and the run completes.
    • The request leaves ?status=pending&approverId=….
  • A7 pass: reassign moves the slate [admin, admin] → [Ada, Ada] and the request stays pending. After remind, request-info and comment, the status, progress, slate and current-step snapshots are byte-identical, with one new row each.
  • A8 pass: a project PATCH under manager_review (lockRecord:false) → 200. Under exec_review (lockRecord:true) → 409 RECORD_LOCKED.
  • A9 blocked(fixture, knownGaps[0]): still no stock door mints an org-less request at 756220c4.
    • approval-service.ts sets requestOrg = subjectOrg ?? ctxOrg.
    • The REST ledger has no submit door.
    • No stock flow pairs an approval node with a schedule, time-relative or api trigger.
  • N0 pass: revise at exec_review → 400 VALIDATION_FAILED "no 'revise' out-edge", with no row written.
  • N1 pass: a second remind → 429 THROTTLED, with no row written.
  • N2 pass: every wrong-relationship call → 403 with no row written (recall/resubmit by a non-submitter; request-info after reassigning away; request-info, comment, approve and reassign by an outsider; remind by a non-submitter).

automation.flow-run-step-nesting (rev 8) · PASS

  • A0 pass: a trigger with 3 tasks → 200, and the run completed with exactly 3 send_reminder steps.
  • A1 pass:
    • The 3 guard_reminder steps carry {parentNodeId: loop_tasks, regionKind: loop-body}, with iteration 0/1/2.
    • Each send_reminder carries {parentNodeId: guard_reminder, regionKind: try} with its guard's iteration.
    • start and loop_tasks have no parent.
  • A2 pass: the nested fan-out over 2 rows gives (iteration, branch) pairs (0,0), (0,1), (1,0) and (1,1), each once, with branch:0 present. All 18 steps parse under the built ExecutionStepLogSchema.
  • A3 pass: the Studio Runs tab renders a nested tree, not a flat one: "For each task LOOP" → ITERATION 1/2/3 → TRY_CATCH → TRY → Send Reminder.
  • A4 pass: the panel shows 8 step rows, matching the 8 API steps, with the same grouping.
  • A5 pass: with tasks:[] the run completed with [start, loop_tasks] and 0 child steps.
  • N0 pass: the designer panel is the oracle; the developer Flow Runs page was not used.

automation.rollup-summary-filter (rev 6) · PASS

  • A0 pass: EXP-2001's 4 lines give total 1500.5, approved 960, reimbursable 512, lines 4, rejected 0 and over_limit 2. All six equal a recomputation from the child rows.
  • A1 pass: a line moved approved → rejected (PATCH 200). approved 960 → 620 and rejected 0 → 1; the other four are unchanged.
  • A2 pass: billable true → false moves reimbursable 512 → 0; the other five are unchanged.
  • A3 pass: deleting a line (DELETE 200) moves total → 880.5, lines → 3, approved → 0 and over_limit → 1, all equal to recomputation.
  • A4 pass:
    • A scratch package com.qa.rollup was created (201), with a parent summary big_total filtered {kind:'big'}.
    • In Studio, Data › Rollup Parent › Form › Big Total › "Filter (rows to aggregate)", the value was set to "huge" and published ("Published 1 item").
    • GET /meta/object/rollup_parent → filter:{kind:'huge'}, and a live recompute gives 15 over huge 10 + huge 5 + big 100.
  • N0 pass: no out-of-filter roll-up moved on any edit.

automation.flow-node-type-matrix (rev 9) · PASS

  • A0 pass: GET /automation/actions returns 19 ids (the 21 variants minus start and end). The Add-node palette on a clone lists the same 19 plus End.
  • A1 pass: every variant was evidenced by a real run step: start, decision, get_record, delete_record, assignment, loop, create_record, update_record, http, notify, script, screen, wait, subflow, map, connector_action, parallel, try_catch, approval, approval_revise, and end.
    • create_record came from a signed webhook post (202).
    • http came from the knownGaps self-ping recipe.
    • connector_action came from the self-ping connector with OS_PORT exported.
    • end is evidenced by 25 completed runs.
    • A skipped end, and an http to an egress-blocked host, were not cited.
  • A2 pass: each suspending variant read paused before and completed after: screen (reassign wizard), wait (60 s hold), approval, approval_revise and map.
  • A3 pass: a sweep of 35 flows / 34 runs / 104 steps found 0 steps with an empty or missing nodeType.
  • A4 pass: loop-body, parallel-branch, try and catch regions each carry parentNodeId and regionKind.
  • A5 pass: an unknown node type bogus_node:
    • saves 200;
    • logs a WARN with unknownTypes:["bogus_node"];
    • its trigger → 400 FLOW_FAILED "No executor registered for node type 'bogus_node'";
    • its run reads failed, with the step carrying NO_EXECUTOR.
  • N0 pass: the unknown type was not silent.

automation.flow-error-handling (rev 6) · PASS

  • A0 pass: a resilient_sync run completed, and the guarded_push try_catch step reads success.
  • A1 pass: 4 push steps carry {parentNodeId: guarded_push, regionKind: try}, failure, NODE_FAILURE "http: fetch failed". All of them come before the catch step record_failure (success).
  • A2 pass: retryAttempt 0/1/2/3 (maxRetries 3). The durations of 7091 ms and 7219 ms are consistent with 1+2+4 s backoff.
  • A3 pass: both tasks read sync_status=failed with sync_error set.
  • A4 pass: an unprotected probe flow's run reads failed with "Node 'push' failed: http: fetch failed".
  • A5 pass: the Runs panel shows TRY with 4 FAILURE rows, then CATCH SUCCESS "Flag Sync Failure". The probe panel shows the run-level error.
  • A6 pass: the log reads ERROR Trigger-fired run of flow 'qa_b_unprotected' failed … nothing retries the run.
  • N0 pass, N1 pass (the catch node never ran on successful runs), N2 pass.
  • N3: noted only. The container reading success is 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).

api-backend.formula-stdlib-matrix (rev 4) · PASS

  • A0 pass (test oracle, built-package layer): the harness resolved packages/formula/dist/index.js, and no src file is newer than dist.
    • registerBoundStdLib registers 29 distinct names, and the docs table lists the same 29, with 0 missing either way.
    • CEL_STDLIB_FUNCTIONS has 37 entries: those 29 plus 8 CEL builtins.
    • 79 known-answer rows cover all 29 functions, with 0 failures.
  • A1 pass: today(), daysFromNow, daysAgo and isoDate resolve to the zone's calendar day (Asia/Shanghai, UTC). An unknown zone does not throw.
  • A2 pass:
    • addMonths clamps 01-31+1 to 02-28.
    • floor(-1.2) is -2 and an int; ceil(-1.2) is -1.
    • joinNonEmpty skips null and blank values.
    • coalesce('','x') = ''.
    • min/max return the operand verbatim.
  • A3 pass: 250/100 → 2.5 (double), and 7/2 → 3 (int).
  • A4 pass: the stock budget_remaining = 180000 and field_zoo f_formula = [315, 3.5]. A scratch upper(joinNonEmpty(…)) field gives 'ALPHA-GAMMA' on both create and read.
  • A5 pass: sqrt(4) in an active PUT → 422 INVALID_METADATA expression-invalid (a NAME fault), and the item reads 404.
    • A draft save → 200, but publish-drafts → outcome "refused" with the same issue, and the item still reads 404.
    • Control: floor(record.amount) saves and evaluates.
  • N0 pass, N1 pass, N2 pass.

identity-auth.api-key-ui-lifecycle (rev 4) · PASS

  • A0 pass: a member mints a key through the UI (Generate key → confirm). POST /api/v1/keys → 201 with the full osk_… secret, shown once in the dialog. The sys_api_key reads and lists afterwards carry prefix only and no key field.
  • A1 pass: x-api-key on GET /data/sys_session → 200 with 3 rows, all the owner's, the same as the owner's own session read. An admin read sees 8 rows across 5 users.
  • A2 pass: PATCH {revoked:true} → 200, and the next x-api-key calls → 403 API_KEY_REVOKED with no records. Control: an unknown key → 401.
  • A3 pass: un-revoking → 200, and x-api-key → 200.
  • A4 pass:
    • A second member's list holds only their own key, and reading the first member's key id → 404.
    • The admin list holds both.
    • The UI views are scoped the same way, and the admin all_keys view carries an Owner column.
  • A5 pass: the "Your new API key" dialog carries the copyable secret and the "will not be shown again" warning, and the re-opened grid is prefix-only.
  • N0 to N3 pass.

identity-auth.org-membership-team-management (rev 5) · PASS

  • A0 pass: the Setup Organization nav points to sys_organization/record/ followed by the id get-active-member returns. The page shows Members, Invitations and Teams with the management actions.
  • A1 pass:
    • A member's invite → 403.
    • Promoted to admin, the invite → 200.
    • Demoted again, the invite → 403.
    • delegated_admin → 200.
  • A2 pass: undefined roles (guest, contributor, showcase_ops) → 400 ROLE_NOT_FOUND on both update-member-role and invite-member. Nothing is written.
  • A3 pass: organization/update (rename) → 200. The full-organization read and the page title show the new name, with slug default preserved.
  • A4 pass: after remove-member (200), the old session → 401. A fresh sign-in → 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION, and the sessions read revoke_reason organization_membership_ended.
  • A5 pass:
    • create-team → 200, and add-team-member ×2 → 2 sys_team_member rows.
    • A duplicate add returns the existing row, so it is idempotent.
    • A generic POST /data/sys_team_member → 405.
  • A6 pass: a non-admin member is refused 403 on role change, remove, update, create-team, add-team-member and invite. The membership, org-name and team snapshots are identical before and after, and the UI renders "You don't have access to this app".
  • N0 to N3 pass.

identity-auth.first-run-owner-bootstrap (rev 3) · PASS

  • A0 pass: on a zero-user boot, GET /auth/bootstrap-status → {"hasOwner":false} before the wizard and true after.
  • A1 pass: the wizard sign-up → 200, and get-session shows the owner with isPlatformAdmin true, positions platform_admin/org_owner, and member role owner.
  • A2 pass: the CJK and emoji org name 测试组织🚀 is applied via organization/update, with no create call. There is exactly 1 org, and slug default is preserved.
  • A3 pass: the exit is a full document load to /_console/home, which renders the owner's home with the Showcase app. An authed /api/v1/meta/app → 200 [showcase_app, setup, account].
  • A4 pass:
    • After bootstrap, an uninvited forge → 403 SELF_REGISTRATION_CLOSED.
    • An invited forge under the disable toggle → 400 EMAIL_PASSWORD_SIGN_UP_DISABLED, twice.
    • The stock-boot forge → 403, twice.
    • The user count stays at 1.
  • A5 pass: anonymous /_console/setup redirects to login with no wizard fields, and the owner's /setup opens the Setup app.
  • N0 to N4 pass.

identity-auth.self-signup-gate (rev 6) · PASS

  • A0 pass: /auth/config reads audiencePosture invite_only with disableSignUp false on stock, and true on the disabled boot.
  • A1 pass: an uninvited sign-up → 403 SELF_REGISTRATION_CLOSED twice, and no sys_user row is created.
  • A2 pass: invite-member → 200, then sign-up → 200, and get-session names the invitee.
  • A3 pass: on the disabled boot, /register and /register?redirect=… bounce to login, which has no sign-up link.
  • A4 pass: on the disabled boot, an uninvited sign-up → 403 SELF_REGISTRATION_CLOSED and an invited one → 400 EMAIL_PASSWORD_SIGN_UP_DISABLED, twice, with no rows created.
  • N0 to N4 pass.

access-security.public-form-intake (rev 3) · PASS

  • A0 pass: anonymous GET /api/v1/forms/contact-us → 200 with fields [name, email, company, message]. No owner_id, organization_id, status, source or created_by appears anywhere in the body.
  • A1 pass: an anonymous submit with forged owner_id, organization_id, created_by, status and source → 201. Staff read the row back with owner_id null, the deployment org, and created_by null.
  • A2 pass: the landed row has status new and source web.
  • A3 pass: all four fields persisted.
  • A4 pass: GET /forms/contact-us/lookup/owner_id → 404 ENDPOINT_NOT_FOUND, identical to a never-registered path.
  • A5 pass:
    • /forms/contact-us → 302 to /_console/f/contact-us, which renders 4 inputs.
    • The submit → 201, with no Authorization header on any API request.
    • The thank-you text renders.
  • A6 pass: a view overlay with allowAnonymous:false makes the anonymous GET and submit → 404 FORM_NOT_FOUND, twice. Republishing restores the form.
  • N0 pass. N1 pass: a __proto__-nested forgery lands nothing.

access-security.capability-declaration-lifecycle (rev 7) · PASS

  • A0 pass: GET /meta/capability/showcase.export_data → 200, matching capabilities.ts. GET and POST /data/sys_capability → 404 OBJECT_NOT_FOUND.
  • A1 pass, on the action dispatcher:
    • Grant and deny:
      • The granted member's perm_held → 200.
      • The ungranted member → 403 PERMISSION_DENIED "…caller is missing [showcase.export_data]".
      • The admin → 403.
    • Regrant:
      • PUT /meta/permission/showcase_ops → 403 NOT_OVERRIDABLE.
      • A clone with a label → 200, and granting it gives 200.
      • Withdrawing the capability → 403 twice; restoring it → 200.
    • Cross-checks: perm_missing and perm_and → 403 for all; perm_empty → 200 for all.
  • A2 pass: the shadow scratch declares manage_users. os validate and os build exit 0, but the boot refuses with "Security catalog name conflict … capability "manage_users" is already held by the platform". At runtime, the PUT → 403 NOT_OVERRIDABLE.
  • A3 pass:
    • A malformed scope:'galaxy' → os validate exit 1, "capabilities.2.scope: Invalid value 'galaxy'".
    • Runtime PUT /meta/capability/qa.new_cap → 403 NOT_CREATABLE.
    • A member's PUT → 403 FORBIDDEN.
  • N0 to N4 pass.

studio-authoring.view-authoring-live (rev 3) · PASS

The fixture is the runtime object batchd_repair_asset in the authored package com.qa.batchd. A runtime object grants no non-admin persona, so per the item's persona note the end-user surfaces were scored as the admin.

  • A0 pass: a draft view PUT → 200 state:"draft", seq 17.
  • A1 pass: before publish, the list and form render the old shape. The by-name GET → 404, and ?object= → {items:[]}, so nothing leaked.
  • A2 pass: publish → 200. After a reload the list shows the authored columns and the New dialog the authored sections. The boot log shows only registry lines, with no restart, rebuild or second "Server is ready".
  • A3 pass: GET /meta/view?object=batchd_repair_asset serves the list and form views.
  • A4 pass: a reorder plus an added column, drafted and published (200 each), renders the new header order after a reload.
  • N0 pass (no restart). N1 pass (no pre-publish leak).

studio-authoring.draft-publish-lifecycle (rev 8) · PASS

  • A0 pass: a draft PUT → state:"draft". Runtime GETs → 404 for the admin and for a member, including ?state=draft and ?preview=true for the member.
  • A1 pass: a per-item publish → 200, and the GET serves Rev1.
  • A2 pass: with draft 2 staged, the GET still serves Rev1. After publishing, it serves Rev2.
  • A3 pass: a stale If-Match → 409 METADATA_CONFLICT with currentVersion, and the audit row reads denied / metadata_conflict.
  • A4 pass: publish-drafts with a namespace-less object draft → data.success:false, outcome refused, publishedCount 0, code NAMESPACE_PREFIX, and nothing is promoted (all 404). After discarding that draft, it → success:true, publishedCount 2, with a commitId.
  • A5 pass: a draft with a stray widget key → 422 INVALID_METADATA at the save itself, and the GET reads 404.
  • A6 pass: Studio shows "Draft · 2 unpublished" and a Changes pill of 2. After the publish the pill reads 1, and after publish-drafts it is gone.
  • A7 pass: /history lists v1 to v4 (create, publish, create, publish), each with the admin as actor.
  • A8 pass: diff?from=2&to=4 → changed:[{path:"widgets",…}], with added and removed empty. It is not a whole-body dump.
  • A9 pass: rollback {toVersion:1} → 200 and serves Rev1, which renders in the console. {} and "abc" → 400; 999 → 404 VERSION_NOT_FOUND.
  • A10 pass: /audit holds 8 rows, each naming the admin.
  • A11 pass: a forged X-Actor header is ignored. The history and audit rows name the admin, and the forged string appears 0 times.
  • N0 to N3 held.
  • N4 (open ledger question, studio-authoring run notes: one correction to #7637, two stale automated.ref specs, and three ledger/route observations #7753 item 2): HTTP 200 with data.success:false was observed and is not scored.

studio-authoring.authoring-validation-not-persisted (rev 4) · PASS

  • A0 pass: a draft object with a field missing its type → 422 INVALID_METADATA, fields.amount.type [invalid_value].
  • A1 pass: a container-shaped view (type and columns beside list) → 422, with an unrecognized_keys "Wrap it" hint.
  • A2 pass: after each rejection, the GET → 404 and ?state=draft → 404 NO_DRAFT.
  • A3 pass: in the Setup Source tab (Monaco), the type was edited to numbr with the editor's own find/replace. The autosave → 422, and red banners name the field. The stored draft still holds the valid type.
  • A4 pass: the corrected object and view both save 200 as drafts.

studio-authoring.packaged-automation-studio-lock (rev 4) · PASS

  • A0 pass: GET /packages shows com.example.showcase with writable:false.
    • The Automations rail has no New button, and the header reads "Read-only · Customize with an overlay".
    • Across select, drag, an edit attempt, a wait past the debounce, and a reload, there were 0 non-GET /api/v1 requests. The console's own preference PATCH is excluded.
  • A1 pass: the flow GET before and after is byte-identical (cmp).
    • A forged draft PUT with ?package= → 403 ITEM_LOCKED.
    • Without it → 403 NOT_OVERRIDABLE.
  • A2 pass: the header switch is disabled with aria-describedby "Read-only package — switch to or create a writable package to edit." A forced click fires nothing.
  • A3 pass: all 13 inspector inputs are disabled. A forced typed edit and a node drag change nothing, including after a reload.

studio-authoring.doc-on-app-menu (rev 6) · PASS

Fixtures, all in com.qa.batchd:

  • docs: a gated page and two open pages;
  • books: a gated book (permissionSet showcase_auditor), an org book, and an empty book that sorts first;
  • six doc entries in the app navigation.

The reader holds showcase_auditor; the outsider does not.

  • A0 pass: the reader's /meta/app holds all 6 doc entries and the outsider's holds the 3 open ones. The admin, who does not hold the set, is pruned too, which matches knownGaps.
  • A1 pass: the rendered menus equal /meta/app for both personas.
  • A2 pass: a flat doc entry lands on its claiming book, not the empty one that sorts first. Each sidebar equals that persona's /tree.
  • A3 pass: the outsider gets "You do not have access to this documentation" on 9 gated direct routes, and the server answers 403 PERMISSION_DENIED. Control: an unknown doc → "Documentation not found" with a 404.
  • The fixtures were torn down, and the corpus reads back at baseline.

access-security.permission-matrix-edit-loop (rev 4) · PASS

Fixture: set S, granting showcase_task CRU with delete false, held by member M through sys_user_permission_set. M owns 5 tasks.

  • A0 pass: M's DELETE on M's own row → 403 PERMISSION_DENIED.
  • A1 pass: the Setup matrix Delete checkbox plus Save → a plain PUT /meta/permission/… 200, active, with allowDelete:true. This is the console path; publish then → 404 NO_DRAFT.
  • A2 pass: M's DELETE → 200, and the read-back → 404.
  • A3 pass: flipping delete off (PUT 200) makes M's DELETE → 403.
  • A4 pass: a second set granting delete makes it → 200. After that assignment is removed, it → 403.
  • A5 pass: the re-opened editor shows Delete off, matching the server.
  • N1 extra: a draft-mode save left the DELETE at 403 until the draft was published, then 200. S was then restored.

i18n.notification-localized-and-clears (rev 5) · PASS

The zh-CN user switched locale through the avatar menu (PATCH sys_user {"locale":"zh-CN"} → 200). The EN control has locale null. The flow's notify node has an inline title.

  • A0 pass: the zh-CN bell (消息中心) entry reads "New task assigned: Content backlog", with the placeholder rendered.
  • A1 pass: the click lands on the task record page, rendered in zh-CN.
  • A2 pass: POST /api/v1/notifications/read {"ids":[…]} → 200 readCount:1, and the server's unreadCount goes 2 → 1.
  • A3 pass: after a hard reload, and from an independent client, the row stays read:true with unread at 1.
  • A4 pass: the EN recipient's title is identical to the zh-CN one.
  • N0 pass, N1 pass, N2 pass.

i18n.strict-translation-key-rejection (rev 3) · PASS

  • A0 pass: all 10 variants → 422 INVALID_METADATA via PUT /api/v1/meta/translation/qa_strict_zh_cn. Every issues[] entry names its surface and echoes the key.
  • A1 pass: the built spec's defineTranslation (/system) and defineTranslationBundle (root) both throw for all 10 and accept the valid document.
  • A2 pass: "Did you mean helpText → help?" and "label → title?" appear at both doors.
  • A3 pass: each retired key carries its prescription: o, app, nav, dashboard, errors (rule message home), validationMessages (removed in spec 17.0.0, ADR-0049), and _meta (top-level locale).
  • A4 pass: rejected saves on a fresh name read back 404, and over a saved baseline they leave it byte-identical.
  • A5 pass: the corrected document saves 200. On a zh-CN session the type badge reads 任务(QA-F修正), the New dialog 新建任务(QA-F修正), and the field help QA-F 帮助.
  • N0 pass, N1 pass.

integration-system.webhook-lifecycle (rev 10) · PARTIAL

  • A0 pass: on a fresh boot, sys_webhook holds 0 rows.
    • The admin's POST /api/v1/security/_activation/webhook/showcase_task_changed {"enabled":true} → 200, writing one sys_metadata_activation row (active:true).
    • An undeclared name → 404.
    • Anonymous → 401; a member → 403 (manage_metadata).
  • A1 pass: an in-org scratch sys_webhook (201). Create, update and delete of a task each give exactly 1 receiver hit, with X-Objectstack-Event data.record.created / updated / deleted, and type and recordId in the body.
  • A2 blocked(dependency, knownGaps[1] / QA run · surface:mixed (90/90) · 36b2889c · 2026-10-12 · 72 PASS / 10 PARTIAL / 8 FAIL / 0 BLOCKED / 0 NOT-RUN #23166):
    • A flow update_record multi:true on 2 rows produced no bulk delivery. The log reads [webhook-auto-enqueuer] … names no organization — refusing to deliver it inside an organization wall.
    • The absence half holds: no fabricated per-record deliveries.
  • A3 pass:
    • The custom header arrives on every hit.
    • The signature equals sha256=HMAC(secret, raw body).
    • timeoutMs 2000 is honored ("timeout after 2000ms", retry pending).
    • The outbox rows carry status, attempts, response_code and next_retry_at. A 410 receiver → dead.
  • A4 partial (scored partial under RUNNER rule 6):
    • Live: redeliver of a dead row → 200 pending, then success, with a byte-identical, verifying signature. Anonymous → 401, non-JSON → 400, {} → 400, an unknown id → 404, a pending row → 409 DELIVERY_NOT_ELIGIBLE.
    • Not drivable on stock single: the cross-organization 404 and the parked-row 409 DELIVERY_NEVER_SENT arms. These came from pins: webhook-redeliver-tenant-scope.test.ts plus webhook-redeliver-member.test.ts (22 passed, plugin-unit layer), and http-outbox-parked-record.integration.test.ts (8 passed, engine/SQL layer, no HTTP). Both stop below HTTP.
  • A5 pass: a scratch app with triggers:['undelete'] or ['api'] fails objectstack compile with exit 2 and a named refusal. The control ['delete'] compiles with exit 0.
  • A6 pass: after a reboot, the activation row is unchanged, and a task PATCH produces a new declared:showcase_task_changed delivery row.
  • N0 pass: failures surface as pending/dead rows, never dropped silently.
  • N1 pass: the data door refuses undelete with 400. A planted row logs "declares trigger(s) the engine never emits … it will NEVER fire".
  • N2 pass (spec-parse layer): ConnectorSchema refuses webhooks / triggers with the retired-key prescription.

integration-system.notify-inbox-delivery (rev 6) · PASS

  • A0 pass: after a reassign, the assignee's GET /api/v1/notifications lists task.assigned "New task assigned: SEO migration plan" with actionUrl /showcase_task/ followed by the task id.
  • A1 pass: the third member's list is empty with unread 0, and their sys_inbox_message / receipt reads total 0.
  • A2 pass:
    • POST /notifications/read → readCount 1, and unread goes 2 → 1, with the receipt flipping delivered → read.
    • POST /read/all → unread 0.
  • A3 pass: anonymous → 401. On a --preset minimal boot without service-messaging, all three doors → 501 NOT_IMPLEMENTED "Install @objectstack/service-messaging to enable".
  • A4 pass: a notify with channels:['push'] logs "channel 'push' is not registered; refused 1 delivery row(s)", and the delivery and inbox counts are unchanged.
  • N0 pass. N1 pass: a cross-user mark-read → readCount 0.

platform-core.settings-hub-roundtrip (rev 5) · PASS

  • A0 pass: PUT /api/settings/localization {"timezone":"Asia/Tokyo"} → 200, and the re-GET reads Asia/Tokyo with source tenant.
  • A1 pass: GET /api/v1/auth/me/localization follows: UTC → Asia/Tokyo, and Europe/Paris under the env pin.
  • A2 pass: a sys_setting_audit row (namespace, key, scope tenant, action set, source api, actor the admin, new_hash set). old_hash is null; see the extraction.
  • A3 pass: the console Save sends PUT {"fiscal_year_start":"april"} → 200, and after a reload the field shows "Overrides Default · Reset to default · Tenant".
  • A4 pass: with OS_LOCALIZATION_TIMEZONE=Europe/Paris, the GET reads source env, locked true, lockedReason "Set via env: OS_LOCALIZATION_TIMEZONE", and a PUT → 409 SETTINGS_LOCKED.
  • A5 pass: the "Locked by env" badge shows, with the input disabled.
  • A6 pass: POST /api/settings/mail/test → 400 SETTINGS_ACTION_FAILED, with "SMTP host is required — nothing was sent" or "No delivery transport is active".
  • A7 pass: the secret values land in sys_secret (aes-256-gcm), sys_platform_setting.value_enc holds a handle with value NULL, and no mail or ai rows exist in sys_setting.
  • A8 pass: set secrets read as the mask, with masked cascade entries, and unset ones read null. No plaintext appears in any body.
  • A9 pass: an env-pinned secret reads mask, env, locked. A real PUT → 409, with no plaintext in the body.
  • A10 pass: an echoed mask → 200 {values:{}}.
    • The ciphertext, handle and updated_at are unchanged.
    • A fake SMTP AUTH proved the in-process plaintext was not overwritten.
    • A new value rotates the handle.
  • A11 pass: anonymous → 401 with no body data.
  • A12 pass: GET /api/settings lists 11 namespaces, and each /:ns → 200.
  • Negatives pass: Mars/Phobos → 400 SETTINGS_VALIDATION, and bogus_key → 400 UNKNOWN_KEY.

cli.scaffold-console-first-paint (rev 6) · PASS

This was the published path: npx -y create-objectstack@latest (18.1.0 through the proxy, nothing refused), with the blank template. So this proves the published 18.1.0 console, not the 756220c4 workspace console (knownGaps[2]).

  • A0 pass: npx os validate exits 0.
  • A1 pass: npx os dev --ui -p PORT serves /_console/ 200, which redirects to login. The sign-in card renders, and after sign-in the home page renders.
  • A2 pass: bare npm run dev -- -p PORT (no --ui) serves the same console, and the banner prints the console URL and the dev admin.
  • A3 pass: the newcomer signs in with the dev admin the banner prints. The data POST → 201 and the row reads back. The nav/list half is the stated not-applicable limit, since the blank scaffold ships no app.
  • N0 to N4 pass.

access-security.audit-log-browser (rev 7) · PASS

  • A0 pass: the admin's $filter read returns:

    • the fresh member's login (sys_session);
    • the admin's delete with record_id and old_value;
    • the admin's config_change.

    The member's filtered and unfiltered reads → 403 PERMISSION_DENIED. The parent-record gate pins audit-log-read-visibility.test.ts and audit-log-read-visibility.integration.test.ts passed 38 tests (plugin-unit and engine layers); the live door scores the clause.

  • A1 pass: /_console/system/audit-log redirects into Setup. The table paints the columns Timestamp, Action, Object, Record, Actor and IP Address, with the three events present.

  • A2 pass: the Action select delete re-issues GET …sys_audit_log?$filter={"action":"delete"}…, and the table shows exactly the 4 delete rows.

  • A3 pass: an update row's drawer shows the Before and After JSON panels.

  • A4 pass: all 50 page rows match an API read with the same query field by field, with 0 mismatches.

  • A5 pass: POST, PATCH and DELETE → 405 OBJECT_API_METHOD_NOT_ALLOWED. The total is unchanged.

  • N0 pass, N1 pass.

records-forms.form-view-gallery (rev 4) · PASS

  • A0 pass: every form type rendered its own shape. tabbed, wizard and split were applied as PUT /api/v1/meta/view/showcase_task.form?mode=draft with isDefault:true (200), then published (200), and the served config.type matched each time.
    • simple: a "Task" section, 2 columns.
    • tabbed: Overview/Schedule/Details tabs.
    • wizard: 1 Basics · 2 Assignment · 3 Schedule, "Step 1 of 3".
    • split: two panes side by side.
    • drawer (task-desk): a 384 px dialog docked right over the list.
    • modal (task-desk): a centered dialog.
  • A1 pass: the wizard sends exactly one POST /api/v1/data/showcase_task 201 at Save on /new, and none per step. On /edit it sends one PATCH at Save.
  • A2 pass: notes is absent at medium priority, mounts full-width at urgent, and unmounts again at medium.
  • A3 pass: after every save, the re-read shows only the edited keys changed, across all six types.
  • A4 pass: values survive tab hops and wizard Back.
  • N0 pass: Next with an empty title shows "Title is required" / "Project is required" and sends no write.
  • N1 pass: drawer and modal were scored only on task-desk.
  • The view was reverted with DELETE …/showcase_task.form (200 reset:true), and the re-read is byte-identical to the pre-run snapshot.

records-forms.action-location-matrix (rev 15) · PARTIAL

The served actions match the source: 10 actions, and showcase_new_task has locations: [].

  • A0 pass: each location renders exactly its declared set.
    • list_toolbar: Reassign…
    • list_item: Mark Done or Reopen, Reassign…, Quick View
    • record_header: Mark Done, Log Time, Archive
    • record_more (⋯): Open Docs, Recalculate Estimate, Recalculate Selection
    • record_section: Mark Done, Log Time, Archive
    • record_related (Project Detail): Log Time
  • A1 pass: there are no extra placements anywhere.
  • A2 pass: one dispatch per location.
    • list_toolbar, no selection: "select a row first", and no request.
    • list_toolbar, row selected: POST /actions/showcase_task/showcase_bulk_reassign 200 opens the screen, and resume → 200 changes the assignee on the server.
    • list_item: Quick View opens its dialog.
    • record_header: Mark Done → 200, and done goes false→true.
    • record_more: Recalculate Selection → POST /api/v1/apps/showcase/recalc 200, and estimate_hours goes 7→88.
    • record_section and record_related: Log Time opens its declared form.
  • A3 partial:
    • The both-sides half passes. A not-done task shows Mark Done; a done task shows Reopen with no Mark Done.
    • The fail-closed half is blocked(fixture, knownGaps[2]), re-judged this run. A runtime-authored action with a declared record_header (PUT 200) is listed under /meta/action but not joined into the object's actions[], and does not render. A throwing visible is now also refused at authoring (422 unknown field).
  • A4 partial:
    • Half (b) passes: New Task renders nowhere, and Recalculate Selection appears only under ⋯.
    • Half (a) is blocked(fixture, knownGaps[2]): the runtime action is not served, as before.
  • N0 pass, N1 pass, N2 pass.

records-forms.field-group-visible-when (rev 7) · PASS

The scratch object had to be named groupgate_qa_group_gate because of the namespace prefix.

  • A0 pass:
    • The bare-string PUT (200) reads back bare.
    • The envelope PUT (200) reads back {"dialect":"cel","source":"record.kind == 'pro'"}.
    • Both gate identically on the form and the detail page.
    • Pins object-strictness-batch20.test.ts and field-group-layout.test.ts: 70 passed, spec layer.
  • A1 pass: the visibleOn PUT → 422 INVALID_METADATA naming visibleWhen ("FALSE hides the whole group, header included"). The re-read is unchanged.
  • A2 pass: the "visibleWhen passthrough" describe is green, 5 of 5.
  • A3 pass:
    • The objectui pin record-details.groupVisibleWhen-11630.test.tsx passed 13/13.
    • Live form: basic hides the "Pro details" header and members, and flipping kind shows or hides them live.
    • Detail page: an inline-edit save of kind toggles the section without a reload.
  • A4 pass: a form edit sends only {"name":…}, and the hidden pro_tier / pro_notes stay byte-identical.
  • N0 pass, N1 pass.
  • N2 pass, direction recorded as fail-closed: an unknown-field or syntax-broken predicate hides the section on both surfaces, with no page or console error.
  • N3 pass.

ux-conventions.write-feedback (rev 6) · FAIL (A1, the Mark Done row)

  • A0 pass: every landed write showed feedback, each paired with a server read:
    • create
    • inline edit
    • bulk delete ×2
    • kanban move
    • Mark Done
    • invite
    • approve
    • import
    • Studio autosave, publish and discard
    • Settings save
    • Reassign flow
    • member create
  • A1 fail, the Mark Done row only.
    • The record action's toast reads "Task marked done.", which names only the object. Under the rev 6 clause a record or param action is record-level and must name the record or a count.
    • Every other row passes its class test:
      • 'Task "WF Create E3" created'
      • "1 row saved"
      • "Deleted 2 Task records"
      • "Moved Evidence collection to To Do."
      • "Invitation sent to …"
      • 'Approved "EXP-2001"'
      • "Imported 2 rows."
      • "Published 1 item…"
      • "Draft discarded: QA Group Gate"
      • "Settings saved"
      • 'Flow "Reassign Task" completed'
    • The dialog create toast now names the record, so Record create/update success toasts name the record by its title, not only the object (ruling C on #25849) #26172's row is no longer an expected FAIL.
  • A2 pass: "Task marked done." is the declared successMessage verbatim, and "Invitation sent to …" renders the declared template with result.email.
  • A3 pass:
  • A4 pass: Escape, or re-picking the same value, sends no PATCH and shows no modified state.
  • A5 pass: an import job's Undo works in create mode (POST …/undo 200, count 17→15) and in update mode (priority restored).
  • N0 to N2 pass.

search.console-global-search (rev 5) · PASS

The persona is a contributor-bound member, minted by invite and sign-up with sys_user_position contributor (201).

  • Their list shows 0 of 12 invoices, INV-1003 → 404, and their own INV-QA-E1 is visible. The admin sees 12.
  • A0 pass: ⌘K, Ctrl+K, the header button, ?palette=1 and ?cmdk=1 each open overlay:command-palette.
  • A1 pass: the palette calls GET /api/v1/search?q=zhangwei&limit=24&objects=… → 200 (searchAll).
  • A2 pass: zhangwei and 张伟 return 张伟 under "Contact". huaning groups under the authored label "Account (Success Overlay)" and "Contact".
  • A3 pass: the top hit has aria-selected. Enter and a ref click both open the record.
  • A4 pass: an empty input shows Recently viewed. A one-character query sends 0 requests.
  • A5 pass:
    • The member's search for INV-1003 → hits:[], totalHits:0, and the palette shows "No results found.".
    • The admin's same search finds the invoice.
    • The member's search finds their own row.
  • A6 pass: the full search page (limit=60) shows "4 results" grouped by object. A no-hit query shows an empty state with 0 console errors.
  • N0 to N4 pass.

search.pinyin-flag-both-sides (rev 6) · PASS

  • A0 pass (ON): zhangwei/zw/张 → 张伟, wf/wangfang → 王芳, and huaning/hnkj → 华宁科技, with 1 hit each.
  • A1 pass: on contacts, hnkj/huaningkeji → 0, while 华宁科技 → 3 through the company source column.
  • A2 pass (OFF: a fresh boot, and a restart of the ON DB with the flag off):
    • Initials zw/wf/hnkj → 0.
    • 张 still hits.
    • Full-spelling queries still hit through email/website. No errors.
  • A3 pass: "Plugins: 53 loaded" ON vs 52 OFF. The roster diff is exactly PinyinSearchPlugin, and the OFF debug logs (6313 lines) hold 0 'pinyin'.
  • A4 pass: "[pinyin-search] companion hooks bound", and the reconcile reports account updated:1 and contact updated:3.
  • N0 pass, N1 pass.

Reproduction rule for the one fail

ux-conventions.write-feedback · A1 · the record action's success toast names no record. Reproduced twice on fresh loads (tasks D31UoUvnteeR7MXf and sy4Ntk7PLzSu4UoN). The clicks were ref-targeted (role and name inside [aria-label="Page header actions"]), not coordinates. The coordinator independently confirmed the declared string in source.

  1. Boot stock showcase at 756220c4 and sign in as admin through the form.
  2. Open /_console/apps/com.example.showcase/showcase_task/record/ID for any task with done: false (for example "PII access review").
  3. Click the record-header button "Mark Done". The request is POST /api/v1/actions/showcase_task/showcase_mark_done with {"recordId":"ID","params":{}} → 200, and the server row reads done: true.
  4. Read the success toast ([data-sonner-toast]).

Expected (acceptance[1], record-level class, the maintainer ruling recorded on #25849 letter C): the text names the record, e.g. its title "PII access review", or a count.
Actual: "Task marked done.", which names the object only.

Self-check:

  • hydration-race: ruled out. The toast was read after the 200 and after the screenshot.
  • automation-input: ruled out. The click was ref-targeted and the request fired.
  • eventual-consistency: ruled out. The server read confirmed the write.
  • Not the console: acceptance[2] shows the console renders the declared string verbatim.

Disposition (§5): product defect, in the showcase fixture's declared copy. The capability works; the declared message violates the ruled convention. It is extracted (see the close-out).

RUNNER rule 7 (a second independent agent before acting): this is a P1 item and the fail blocks nothing, so a full verify pass is not owed. The coordinator re-derived the verdict from the captured strings and the source (examples/app-showcase/src/ui/actions/index.ts:69, successMessage: 'Task marked done.').

No authentication or authorization finding arose in this run, so nothing is withheld.

Derived item verdicts and gaps

30 PASS / 3 PARTIAL / 1 FAIL / 0 BLOCKED / 0 NOT-RUN.

The three PARTIALs, with the gaps that make them partial:

  • approvals.decision-action-matrix A9: blocked(fixture, knownGaps[0]), re-confirmed at 756220c4. There is no org-less request on stock: no REST submit door, and no stock approval flow on a schedule, time-relative or api trigger.
  • records-forms.action-location-matrix A3 (fail-closed half) and A4 (half (a)): blocked(fixture, knownGaps[2]), re-confirmed. A runtime-authored action is listed under /meta/action but never joined into the object's actions[], even with a declared location. A throwing visible is now refused at authoring (422), so the fail-closed half needs a packaged specimen.
  • integration-system.webhook-lifecycle:

The one FAIL: ux-conventions.write-feedback A1, the Mark Done row (product defect, extracted).

Superseded verdicts from #25817: this run re-judged 16 of #25817's 29 items at 756220c4:

  • unchanged:
    • access-security.capability-declaration-lifecycle PASS
    • api-backend.formula-stdlib-matrix PASS
    • approvals.per-group-signoff, quorum-m-of-n, inbox-metadata-actions PASS
    • approvals.decision-action-matrix PARTIAL
    • automation.flow-run-step-nesting, flow-node-type-matrix, flow-error-handling PASS
    • i18n.notification-localized-and-clears, strict-translation-key-rejection PASS
    • identity-auth.org-membership-team-management PASS
    • integration-system.notify-inbox-delivery PASS
    • records-forms.action-location-matrix PARTIAL
    • search.pinyin-flag-both-sides PASS
  • changed:
    • ux-conventions.write-feedback PASS (rev 4) → FAIL (rev 6). The clause was reworded to the maintainer ruling between the two runs, so the change is in the clause, not a regression.

The other 13 items of #25817 are W2-C3a's; this record carries none of them forward.

Checklist-accuracy findings and fixture gaps

These close out through the wave anchor #25807 and are not extracted. To keep this body under GitHub's size cap, they and the not-extracted observations are in the first comment on this record.

Probes (RUNNER rule 8)

About 26 runner-minutes, spread across the six batches on the same boots and personas as the clauses. The personas were admin, plain and contributor members, the seeded demo personas, a key as a machine caller, anonymous, and an outsider or forger. The boundary classes were empty, missing or unknown ids and bodies; the opposite posture (flag OFF, walled vs stock, hatch on, a minimal preset); and readers outside the asserted scope.

The doors probed:

Four probe findings passed the filing gate and are extracted (see the close-out): the diff missing-version 200, the single-word locale residual of #25822, the old_hash gap, and the page-route update toast. The one clause fail is extracted too. The rest are listed under the observations above.

Timings

  • Environment prep: about 15 min (01:29Z to 01:44Z): checkout, install 34 s, pnpm build 14 min, and a console:build cache hit. In-run prep (boots, personas, scratch apps) was about 33 runner-minutes across the six batches.
  • Testing: about 114 runner-minutes for the clauses, plus about 26 for probes. Wave 1 (A, B, C) ran 01:46Z to 02:06Z. Wave 2 (D, E, F) ran 02:01Z to 02:56Z. E was the longest batch, at 55 min wall clock.
  • Coordinator verification: the reproductions and source checks for the extractions took about 10 min (02:22Z to 03:05Z, interleaved).
  • Wall clock: 01:29Z to 03:00Z.

Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Checklist-accuracy findings, fixture gaps and not-extracted observations for this record (card W2-C3b, wave anchor #25807, at 756220c4). They were moved out of the body for size.

    • approvals.quorum-m-of-n rev 7: steps[8] and acceptance[4] still say "drawer" and "the drawer's tally line". The Request cell opens the sys_approval_request record page, where the tally reads "Approvals — x of 2 · N eligible approvers". The fixture's admin-created reports make the admin both the submitter and the third holder; this is harmless and deserves a line in requires[1].
    • approvals.inbox-metadata-actions A1: the override view renders "Override Recall" (and "Override Approve/Reject/Reassign"), not "Recall". Name the label.
    • approvals.decision-action-matrix A1: the ≤500k budget-approve run also logs a skipped exec_review step after needs_exec. The discriminator holds.
    • automation.flow-node-type-matrix:
      • steps[6]: the webhook intake accepts only the timestamped signature t=UNIX,v1=HEX over "t.rawbody". A body-only HMAC → 401 INVALID_SIGNATURE.
      • steps[1]: clone needs both name and label.
      • steps[3]: release_signoff items must be record-shaped [{id,title}].
    • automation.rollup-summary-filter:
      • steps[2]: the child's parent field is expense_report.
      • steps[6]: the editor lives at Studio Data › object › Form › summary field › Properties › "Filter (rows to aggregate)", and the edit is a draft until "Publish all".
    • api-backend.formula-stdlib-matrix A5 / steps[6]: the draft's promotion is refused as an outcome:"refused" entry inside publish-drafts' batch envelope, not as an HTTP 422.
    • identity-auth.api-key-ui-lifecycle:
      • The "API Keys" nav sits in a collapsed Developer group. Name the routes /_console/apps/com.objectstack.account/sys_api_key/view/mine and …/com.objectstack.setup/sys_api_key/view/all_keys.
      • Minting is two clicks: Generate key, then confirm.
    • identity-auth.self-signup-gate step 1: /login?redirect=/accept-invitation/ID renders an "Accept your invitation" card with a "Create an account and join" link. It does not forward to /register automatically.
    • identity-auth.first-run-owner-bootstrap:
      • The exit is two document loads to /_console/ landing on /_console/home.
      • A --no-seed-admin boot seeds no demo personas.
    • access-security.capability-declaration-lifecycle:
      • The surface is mixed, but no clause has a browser oracle.
      • The header comment of examples/app-showcase/src/security/capabilities.ts still says the declarations are "seeded into sys_capability at boot", which contradicts ADR-0131 D3 and the live 404.
    • access-security.public-form-intake A5: the thank-you panel is partly covered by the "Submitted" toast. Read the panel text, or screenshot after the toast settles.
    • studio-authoring.view-authoring-live fixture:
      • The object must carry the package namespace prefix (NAMESPACE_PREFIX).
      • sharingModel takes private|public_read|public_read_write|controlled_by_parent.
    • studio-authoring.draft-publish-lifecycle:
      • A3: the HTTP body code is METADATA_CONFLICT; the lower-case spelling is the audit row's.
      • A5 / steps[7]: the stray-key gate fires at the draft save (422), so the publish meets 404 NO_DRAFT.
      • A8: the diff is shallow by contract (path widgets), so "isolates the widget/dimension key" overstates it.
      • Draft discard is DELETE /meta/TYPE/NAME?state=draft.
    • studio-authoring.doc-on-app-menu: menu hrefs use /apps/PACKAGE_ID/docs/…, and the app-name form redirects there.
    • access-security.permission-matrix-edit-loop:
      • steps[5] (the FLS flip) maps to no acceptance clause. Add a clause or drop the step.
      • The field-level controls sit behind each object row's expand chevron.
    • integration-system.webhook-lifecycle:
      • The status column is response_code (the item says lastStatusCode), and the activation row carries active (the item says enabled).
      • $filter must be JSON.
      • A5's scratch builds need requires: ['webhooks','realtime'].
      • "The declaration still reads switched on" has no public read door: the meta read answers isActive:false (see the observations below). The working oracle is a post-reboot delivery row.
      • A terminal row needs a 4xx receiver.
    • i18n.strict-translation-key-rejection steps[3]: a runner barred from the tree can require packages/spec/dist by absolute path from a scratch dir.
    • cli.scaffold-console-first-paint:
      • content/docs/getting-started/quick-start.mdx carries no create-objectstack command.
      • The scaffolder prints pnpm run dev where the item says npm run dev; both work.
      • The published scaffold resolves 18.1.0, so the workspace console is not what this item proves (knownGaps[2]).
    • platform-core.settings-hub-roundtrip:
      • steps[13] expects invalid_option for Mars/Phobos; the live refusal is 400 SETTINGS_VALIDATION with code: "value_domain".
      • steps[9]: no stock manifest has a user-scope secret, so only the 2-entry global chain is walkable.
      • An echoed mask on an env-locked secret answers 200 no-op, not 409.
    • records-forms.form-view-gallery: deep-linking /new or /edit with a page load makes save's navigate(-1) leave the document and drop the toast. That is a driver artifact; steps could say to reach the route by in-app navigation.
    • records-forms.action-location-matrix: knownGaps[2] was re-confirmed, and authoring now also refuses a throwing visible.
    • records-forms.field-group-visible-when:
      • steps[1] must name the object with the namespace prefix (groupgate_qa_group_gate).
      • On the detail page the gate applies on save, not on an unsaved inline flip.
    • ux-conventions.write-feedback:
    • Shared recipe search:qa-contributor-bound-member step 5: the first account GET /api/v1/data/showcase_account returns is churned, so the invoice POST → 400 "This account has churned". Say to pick a non-churned account.
    • Environment (RUNNER facts): examples/app-showcase/.objectstack/data/ is created, empty, by a boot that passes -d file:…. It is gitignored and harmless, and it is not the installed-packages trap.

    Observations seen beside the clauses, no declared contract violated, not extracted (for the anchor's judgment):

    • sys_audit_log keeps the full sys_secret row, ciphertext included, in old_value/new_value on create and delete. A rotated secret's AES-GCM ciphertext therefore stays readable by audit-log readers. No plaintext was found in any of 157 rows.
    • GET /api/v1/meta/webhook/showcase_task_changed answers isActive:false while the activation ledger has it on and it delivers.
    • An API key can mint further keys (POST /api/v1/keys with x-api-key → 201), so a leaked key can mint replacements that outlive its own revocation.
    • Signing up again with an already-registered address that holds a pending invitation → 422 USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL, an account-existence signal limited to invited addresses.
    • ⌘K palette: replacing the query in one shot (select-all plus a programmatic insert, or Playwright fill) leaves no item aria-selected, so Enter does nothing (2/2 each). Typing key by key always works (3/3). This is driver-sensitive (automation-input), so it is not confirmed as a user-facing defect.
    • Search: limit=0 and limit=-1 are coerced to 1 (200, truncated:true) rather than refused, and totalHits reports the returned count.
    • GET /approvals/requests/ID/actions answers 200 [] for a request the caller cannot see, where GET /approvals/requests/ID answers 404. No data leaks.
    • A field-group visibleWhen with an unknown field saves 200 and fails closed at render, while the same predicate on an action visible is refused 422 at authoring. The two predicate slots are inconsistent at authoring time.

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Close-out for this record (card W2-C3b, RUNNER extraction obligation and checklist-test §5). #26456 in the extracted cards stands for this record's number. Each card number below is the one its filing returns.

    Extracted (product defects, each its own card, deduped open AND closed):

    Extracted: #26459 · ux-conventions.write-feedback · acceptance[1]
    Extracted: #26461 · ux-conventions.write-feedback · observed beside the clauses
    Extracted: #26462 · studio-authoring.draft-publish-lifecycle · observed beside the clauses
    Extracted: #26463 · i18n.strict-translation-key-rejection · observed beside the clauses
    Extracted: #26465 · platform-core.settings-hub-roundtrip · observed beside the clauses
    

    Parked on the wave anchor (partial coverage and fixture gaps):

    Parked-on: #25807 · approvals.decision-action-matrix · A9 blocked(fixture) knownGaps[0], no org-less request on stock
    Parked-on: #25807 · records-forms.action-location-matrix · A3 fail-closed half + A4 half (a) blocked(fixture) knownGaps[2], runtime-authored actions are not served into actions[]
    Parked-on: #25807 · integration-system.webhook-lifecycle · A2 blocked(dependency) #23166; A4 cross-org 404 and parked-row 409 arms scored from below-HTTP pins only on stock single
    

    Four-way disposition of the one fail: ux-conventions.write-feedback A1 is a product defect in the showcase fixture's declared successMessage. It is not an assertion defect: the clause matches the maintainer ruling on #25849.

    Supersede: this record re-judges 16 of #25817's 29 items (listed in the body). The other 13 belong to the sibling card W2-C3a, which had not filed a record when this one was filed. So under the card's rule ("whichever of the two cards finishes second") #25817 is NOT closed here. W2-C3a closes it with Superseded-by: naming both records, and carries forward any verdict neither record re-judged.

    The #25822 retest: the fix holds for every reproduced shape except single-word language names (carded above as #26463, not a reopen).


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Close-out (checklist-test §5) · 2026-10-28T15:34Z. The one FAIL, ux-conventions.write-feedback A1, is a product defect extracted as #26459, now fixed and closed. The other defects found in the run (#26461, #26462, #26463, #26465) are fixed and closed too. The 3 PARTIAL items are parked on #25807. Every verdict has a disposition: closing completed.


    Generated by Claude Code

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