Skip to content

Commit 18c7dfd

Browse files
docs(qa): re-point 25 platform-checklist clauses the 17.7 pre-release console runs proved stale, mis-asserted or mis-paired (#21811)
Fixes #21797 Clause-②: no ## What this changes This PR re-points the 25 clauses the card lists, on 20 items in `docs/qa/platform-checklist/areas/*.json`. The two 17.7 pre-release console runs (#21782, #21784) proved each clause stale, mis-asserted or mis-paired. Every item keeps its id, area and selectors. Each item gets exactly one `revision` bump, even when it carries several entries, and one `history` entry. That entry cites the run (#21782 or #21784), the verifier and this card. There are no product changes and no run record. Nothing under `examples/**` is touched. `docs/qa/**` publishes nothing, so there is no changeset. `coverage.json` is untouched because no mapping moved. Each row was re-derived from source rather than copied from the card: - objectui at the current pin `.objectui-sha` 9dfaca6543 (a shallow read-only checkout). The runs read 2e818d0b51ec, and the two pins were diffed on every cited objectui file. - The framework at the claim ref 18c2ddc. Line numbers below are at those commits. The checklist itself carries symbol and path citations only, because its validator refuses `file:line`. Disposition key: **S** stale clause · **AD** assertion defect · **IP** invalid pairing. Where the verifier's disposition differs from the lane's, the verifier's is used. | # | item | clause | disp. | old → new (short) | source re-read | matches card | |---|---|---|---|---|---|---| | B1 | platform-core.docs-portal-render | A5 (+ step 5, persona) | AD | Documentation entry in a /home sidebar → top-bar Help menu, All documentation, /docs, for every persona; member is invite + sign-up | HomeLayout.tsx:1-6 (omits the sidebar); ConsoleLayout.tsx:130 (`setContext('app')`, the only UnifiedSidebar mount); AppHeader.tsx:792-795 | Y | | B2 | platform-core.home-admin-cluster-links | A2 | S | every hub card resolves → every Settings-hub entry reached via the one-hop redirect resolves | apps/console AppContent.tsx:192-194 (`SystemLandingRedirect`), :203; SystemHubPage.tsx absent at both pins | Y | | B3 | platform-core.home-admin-cluster-links | A4 (+ N3, steps 5-6) | S | hub count badges → clause dropped, with its negative and the two badge steps | same retirement (objectui#3743, carried out in objectui#10507) | Y | | B4 | platform-core.home-admin-cluster-links | A5 (+ whole item: steps 1-3 and 7, A1, N1, N4, title, knownGap) | AD | member home nav = Home + Documentation → no Administration entry anywhere in the member's shell; the 9 adminItems are walked as target URLs | UnifiedSidebar.tsx:340-402 (homeNavigation); ConsoleLayout.tsx:130 | Y | | B5 | platform-core.marketplace-console-honesty | A4 (+ N3, step 5, title) | S | admin guard before runtime resolution → enabled: AccessDenied; disabled: the Disabled card for every viewer | MarketplacePage.tsx:216 / :233-235; MarketplacePackagePage.tsx:568 | Y, with a nuance (D1) | | B6 | ai.console-ai-surface-gating | A4 (+ steps 6-7, N3, source, title) | S | SystemHub 'AI Approvals' card expected-fail → no surface advertises it; the bare route is scored by A6 | apps/console AppContent.tsx:207 (route still ungated) | Y | | B7 | records-forms.form-view-gallery | A1 (+ steps 1-5, fixtures, N2, a revert step) | AD | drawer/modal as form layouts → layouts on the page route via a default-form overlay; drawer/modal are presentation modes (ADR-0050), shown on task-desk.page.ts | RecordFormPage.tsx:282-289; recordFormNavigation.ts:133-153 (only `tabbed` maps); task-desk.page.ts:50, :58 | Y, with two divergences (D2, D3) | | B8 | records-forms.conditional-rules-grid | A5 (+ step 3, fixture) | IP | siblings byte-identical → the same, on an invoice whose lines carry distinct stored positions; fixture says how to seed one | GridField.tsx:778-789 (`row[sortField] = index`); masterDetailTx.ts:172 (`changedFields`); deriveMasterDetail.ts:497 | Y (the config key moved, D4) | | B9 | records-forms.action-location-matrix | A1 (+ A3, step 2, knownGap) | IP | every location renders → every location the fixture HOSTS renders; record_header / record_more / record_related are blocked(fixture) | RelatedRecordActionsBridge.tsx:197; task-detail.page.ts:24 (`kind: 'full'`); project-detail.page.ts:72 (`record:line_items`); task.object.ts:37 | Y | | B10 | approvals.inbox-metadata-actions | A2 (+ personas, steps 1/2/4, A3 verify, fixture) | S | remind + recall only on own request → Recall also for an override actor on any pending request; a non-override approver persona | sys-approval-request.object.ts:556-595 (`approval_recall` override arm), remind :550 | Y | | B11 | dashboards.global-filters-rescope | A6 (+ step 8) | S | at-risk tile composes with the status filter → `kpi_awaiting_review` shows the intersection; `kpi_at_risk` is opted out | ops-dashboard.dashboard.ts:87-88 | Y | | M1 | access-security.readonly-package-locks-studio | A3 (+ step 5, fixture, A2 code set) | S | writable contrast via duplicate → a scratch base via POST /api/v1/packages; A2 adds NOT_OVERRIDABLE | runtime/src/domains/packages.ts:560-577 (`requireDuplicableSource`, `DUPLICATE_SOURCE_NOT_A_BASE`) | Y | | M2 | access-security.audit-log-browser | A1 | S | delete row withheld from every non-system reader → served to a view_all_audit_log holder, withheld from a non-holder | spec identity/eval-user.zod.ts:152 (`view_all_audit_log` in ADMIN_FULL_ACCESS_CAPABILITIES) | Y | | M3 | access-security.audit-log-browser | A5 (+ A3, steps 5-6) | S | API/page agree minus the delete → row-for-row for the admin, delete included; FE7: AuditLogPage has an Action select | AuditLogPage.tsx:128, :212-218 | Y | | M4 | identity-auth.auth-method-matrix | A10 (+ step 9) | S | delete-user succeeds authed → deliberately unwired, 404 to every caller | plugin-auth auth-route-ledger.ts:148-157 (`disabled`) | Y | | M5 | identity-auth.workspace-org-switch | A6 (+ step 8) | AD | active org unchanged → 403 USER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION; the foreign org never active (prior or none) | better-auth 1.7.3 plugins/organization/routes/crud-org.mjs:424-425 | Y | | M6 | records-forms.cascading-options | A3 (+ personas, step 4, fixture) | IP | the admin persists restricted → an org_admin persona (invited with role 'admin', accepted) | cascading-select.object.ts:100; spec identity/eval-user.zod.ts:201-207 (`mapMembershipRole`) | Y | | M7 | records-forms.field-group-visible-when | A1 (+ step 3, knownGap, A4, title, source) | AD | GET reads back the envelope → served as authored; the parse normalizes (batch20 pin) | spec shared/expression.zod.ts:184-187; metadata-protocol protocol.ts:766-780; plugin-form fieldGroups.ts:74; plugin-detail buildDefaultPageSchema.ts:669-723 | Y for A1; FE12 differs (D5) | | M8 | automation.flow-run-step-nesting | A2 (+ fixture) | S | send_reminder is the loop-body step → guard_reminder is the loop-body step; send_reminder is in the try region under it | showcase flows/index.ts:919-926; spec execution.zod.ts:180-185 | Y | | M9 | automation.rollup-summary-filter | A5 (+ step 7, fixture, source) | S | editor half on a duplicated package → on a scratch base via POST /api/v1/packages | packages.ts:560-577; spec kernel/metadata-plugin.zod.ts:726 | Y, minus the overlay alternative (D6) | | M10 | automation.flow-node-type-matrix | A1 | AD | palette offers all 21 → the 19 non-structural | spec automation/flow.zod.ts:72 (`FLOW_STRUCTURAL_NODE_TYPES`) | Y | | M11 | automation.flow-node-type-matrix | A2 (+ FE13 knownGap) | AD | every variant logs a step → non-structural + start log steps; end is proven by a completed run | service-automation engine.ts:10897 (return on `end`), :10960-10980 (`start` step) | Y | | M12 | automation.trigger-type-matrix | A1 (+ step 9, title) | AD | trigger.type records the kind → kind via the run's flow; trigger.type is the launch event | engine.ts:1175-1191 (`buildRunTrigger`); execution.zod.ts:376 | Y | | M13 | platform-core.app-management-toggle | A6 (+ steps 2/6/7, title) | S | client-only stub → writes through PUT/DELETE: 403 NOT_OVERRIDABLE on a code app, Delete 200 reset:false | AppManagementPage.tsx:207, :246, :274-278 | Y | | M14 | platform-core.app-management-toggle | N1 (+ the stub knownGap) | S | ticking off the stub toast → no control reports success for a refused write | same | Y | ## Divergences worth a reviewer's eye - **D1 (B5).** VF4's "no package fetch" holds on the package page, whose two fetch effects are `isAdmin`-gated. On the catalog page, `MarketplacePage` `load()` runs whenever the marketplace is enabled (MarketplacePage.tsx:139-145), so a member's visit still fires the list fetch. The clause keeps VF4's words, and the verify text says which page they are read on. The ungated catalog fetch is reported to the card, not filed. - **D2 (B7).** The card has the wizard rendering as steps only on `/new`. `RecordFormPage` passes the form type in both `create` and `edit` modes, and both page routes exist (app-shell console AppContent.tsx:1097, :1113). Only the New/Edit modal degrades a wizard, through `resolveFormViewLayout`. The steps name both page routes. - **D3 (B7).** The card keeps the `modal` variant blocked(fixture). `examples/app-showcase/src/ui/pages/task-desk.page.ts` hosts both an `ObjectForm formType="drawer"` (edit) and an `ObjectForm formType="modal"` (create), so the modal presentation can be scored there. blocked(fixture) stays only as the fallback. - **D4 (B8).** The #21772 pin range renamed the grid's config key `sort_field` to `sortField` at both ends: GridField reads it, MasterDetailForm writes it. The mechanism the card describes is unchanged. The source entry cites the new spelling. - **D5 (M7).** FE12 says both console adapters copy `visibleWhen` at the pin. At both 9dfaca6543 and 2e818d0b51ec, the form adapter copies it (fieldGroups.ts:74), but the detail adapter `deriveFieldGroupDetailSections` does not. That fits the card's own note that the detail page still fails (objectui#11630). Clause A4 scores the form half live and the detail half against that issue. - **D6 (M9).** VF4 offers "or overlay per ADR-0005" as a second route. The `object` type declares `supportsOverlay: false, allowOrgOverride: false` (metadata-plugin.zod.ts:726), so an overlay cannot carry a summary field's filter. The clause names the scratch base only. - **Persona wording.** Card entry 1 asks for "invite + sign-up" in place of "fresh sign-up", because the stock posture is `invite_only`. The same phrase sat on three other items this PR already bumps (home-admin-cluster-links, marketplace-console-honesty, app-management-toggle), so it is corrected there too. The other items carrying the phrase are not touched. - **Invalid pairings (B8, B9, M6).** The card gives corrected clause wording for these, so the clause is revised along with the item's fixture text. Nothing under `examples/**` changed. ## Not changed here - The fixture changes the verifiers name are `examples/**` changes, so they are reported on the card rather than made: - B9: a showcase_task page with a record header, and a parent page rendering the synthesized Tasks related list. - M6: VF4's alternative of widening the cascade predicate to org_owner. - The conditional entry (platform-core.shell-nav-personalization A3) waits on the maintainers' ruling on objectui#11626. - The two "Not parked" wording notes (identity-auth.email-verification-loop A3; integration-system.external-schema-browser-ui columnCount) are outside the card's 25 entries, so they are left for a follow-up. ## Verification (at f5d1e37) - `pnpm check:platform-checklist` exits 0: `OK — 15 areas, 270 items (266 active, 2 planned)`, line citations 0, symbol anchors **653/663** resolved. On the base 18c2ddc the same check reads 652/662. The anchors added are `flow.zod.ts#FLOW_STRUCTURAL_NODE_TYPES` and `packages.ts#requireDuplicableSource`, and both resolve. The unresolved count stays at the named residual of 10. - `node scripts/pm/dispatch-gates.mjs --commands` derived 13 families, and all 13 ran with exit 0. `check:doc-formula-expressions` first refused with PREREQUISITE NOT MET (exit 3, nothing measured). After building `@objectstack/formula` and `@objectstack/lint` it exited 0. `--ran` reconciliation: `13 derived, 13 run, 0 NOT-MEASURED, 0 UNRUN`, a derived zero with every exit code recorded. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 75ddcd1 commit 18c7dfd

8 files changed

Lines changed: 280 additions & 187 deletions

File tree

‎docs/qa/platform-checklist/areas/access-security.json‎

Lines changed: 27 additions & 20 deletions
Large diffs are not rendered by default.

‎docs/qa/platform-checklist/areas/ai.json‎

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -593,10 +593,10 @@
593593
},
594594
{
595595
"id": "ai.console-ai-surface-gating",
596-
"title": "Console AI affordances gate on the access-filtered agent catalog: an agent-less boot hides FAB / chat dock / ⌘⇧I / top-bar link and redirects a stale /ai bookmark without flash — with the UNgated SystemHub 'AI Approvals' card pinned as an expected-fail probe",
596+
"title": "Console AI affordances gate on the access-filtered agent catalog: an agent-less boot hides FAB / chat dock / ⌘⇧I / top-bar link and redirects a stale /ai bookmark without flash — with the ungated bare system/ai-approvals route pinned as an expected-fail probe",
597597
"since": "v16",
598598
"status": "active",
599-
"revision": 1,
599+
"revision": 2,
600600
"priority": "P2",
601601
"surface": "browser",
602602
"personas": ["admin (form sign-in — the pending-actions poll sends the COOKIE half only, credentials:'include' with no bearer header, so an injected-token session reads as anonymous there)"],
@@ -619,8 +619,8 @@
619619
"sweep the gated affordances on the settled shell: floating chatbot FAB, the right-docked chat rail, the AppHeader AI/assistant entry, the Home layout's AI CTAs — screenshot each region, THEN read the DOM to confirm absence",
620620
"press ⌘⇧I (the chat-dock toggle) and confirm nothing mounts — ConsoleLayout only arms the listener when dockEnabled",
621621
"navigate directly to /ai (the stale-bookmark path): capture that a loading fallback holds while the catalog resolves, then the redirect to home lands with the splash preserved — record whether any frame of chat UI flashed",
622-
"as admin open the System hub (SystemHubPage): screenshot the admin card cluster and record whether the 'AI Approvals' card renders on this agent-less boot (expected at head: it DOES — the card is built unconditionally, SystemHubPage.tsx)",
623-
"click the card through to system/ai-approvals; capture the network for ~15s: GET /api/v1/ai/pending-actions?status=pending firing every ~5s and answering 501 each time (poll never stops on error — usePendingActions clears nothing and re-arms)",
622+
"as admin, sweep the surfaces that could advertise AI Approvals — the app sidebars, the settings hub (/apps/setup/system, which forwards to …/system/settings) and Home — and record whether any offers an 'AI Approvals' entry on this agent-less boot (expected: none; the SystemHubPage card wall that carried one was retired by objectui#3743, carried out in objectui#10507)",
623+
"navigate directly to the bare /apps/setup/system/ai-approvals route (still registered with no AI-surface gate in the console route table — AppContent systemRoutes); capture the network for ~15s: GET /api/v1/ai/pending-actions?status=pending firing every ~5s and answering 501 each time (poll never stops on error — usePendingActions clears nothing and re-arms)",
624624
"capture what the page renders: the destructive alert's text (must carry the Cloud/EE remedy sentence from the 501 body), AND whether the 'No actions waiting / When the AI proposes a sensitive action it will appear here for review' empty state renders beneath it as if a live queue exists",
625625
"capture the browser console for the whole session"
626626
],
@@ -644,10 +644,10 @@
644644
"evidence": "the navigation capture / frame notes"
645645
},
646646
{
647-
"clause": "EXPECTED FAIL at head (defect K2, sweep 2026-08-30): the SystemHub 'AI Approvals' card follows the same gate as every other AI affordance — i.e. it is absent on an agent-less boot. At head it is NOT: SystemHubPage.tsx builds the card unconditionally (no useAiSurfaceEnabled read, unlike FAB/dock/header/Home), so it renders and advertises a dead surface. A run that sees the card must score this clause FAIL with the screenshot — do not tick it green, and do not re-file the defect (the sweep's FOLLOW-UPS row owns it)",
647+
"clause": "on an agent-less boot no console surface (nav, settings hub, Home) advertises an 'AI Approvals' entry; the bare /system/ai-approvals route is scored by A6.",
648648
"oracle": "dom",
649-
"verify": "screenshot the hub first, then read the card grid: the designed contract is no 'AI Approvals' card on an empty catalog; observed-at-head is the ungated card",
650-
"evidence": "the hub screenshot + card-grid DOM"
649+
"verify": "screenshot each surface first, then read its entries: no 'AI Approvals' entry on the app sidebars, the settings hub or Home. The route itself stays reachable by URL and is clause 6's subject — reaching it by typing the URL is not an advertisement",
650+
"evidence": "the surface screenshots + their entry DOM reads"
651651
},
652652
{
653653
"clause": "the landing page degrades with the REMEDY, not a fault: the poll's 501 body message (the single-sourced Cloud/EE sentence — see ai.open-edition-honest-degradation clause 2) surfaces verbatim in the page's destructive alert (usePendingActions call() throws body.error.message; AiPendingActionsInbox renders error.message)",
@@ -665,7 +665,7 @@
665665
"negative": [
666666
"ticking any AI capability as PRESENT from the 200 empty-agents courtesy is a recording error — the empty catalog is the hide signal (same negative as ai.open-edition-honest-degradation)",
667667
"scoring an affordance ABSENT from a DOM read taken before a settled screenshot is the hydration-race trap — the gated controls are hidden during load BY DESIGN, so a too-early read proves nothing",
668-
"the two expected-fail clauses must not flip to PASS silently: if a run observes the card gated / the empty-queue suppressed, the defect was fixed — revise this item (drop the expected-fail wording, bump revision) rather than quietly ticking",
668+
"the expected-fail clause (clause 6) must not flip to PASS silently: if a run observes the empty-queue panel suppressed under the error and the poll bounded, the defect was fixed — revise this item (drop the expected-fail wording, bump revision) rather than quietly ticking",
669669
"an anonymous probe of /ai/pending-actions answering 501 instead of 401 would be a REGRESSION of the #7653 anonymous-deny ordering (auth gate precedes every capability answer) — file it, it is not this item's expected 501"
670670
],
671671
"traps": ["hydration-race", "stale-console-bundle"],
@@ -674,13 +674,14 @@
674674
"objectui packages/app-shell/src/layout/ConsoleLayout.tsx (FAB + chat dock + ⌘⇧I all gated on showChatbot/dockEnabled)",
675675
"objectui packages/app-shell/src/layout/AppHeader.tsx (top-bar AI entry gated on the same hook) + console/home/HomeLayout.tsx (Home CTAs)",
676676
"objectui packages/app-shell/src/console/ConsoleShell.tsx (RequireAiSurface — waits for resolve, splash-preserving redirect, objectui#6507)",
677-
"objectui apps/console/src/pages/system/SystemHubPage.tsx (the 'AI Approvals' card built UNconditionally — the K2 gap) + AppContent.tsx (the system/ai-approvals route) + pages/system/AiPendingActionsPage.tsx (thin wrapper, 'Polled every 5 seconds')",
677+
"objectui apps/console/src/AppContent.tsx (systemRoutes: system/ai-approvals still registered with no AI-surface gate — the K2 route half; the bare …/system landing forwards to the settings hub since SystemHubPage and its 'AI Approvals' card were retired, objectui#3743 / objectui#10507) + pages/system/AiPendingActionsPage.tsx (thin wrapper, 'Polled every 5 seconds')",
678678
"objectui packages/plugin-chatbot/src/usePendingActions.ts,217-300 (cookie-only call(), error → error.message, pollInterval 5000 re-arming regardless of errors) + AiPendingActionsInbox.tsx (destructive alert + the error-blind 'No actions waiting' empty state)",
679679
"packages/runtime/src/domains/ai.ts#capabilityUnavailable (#7653 anonymous-deny first; the /ai/agents empty-catalog courtesy #4058/#4053; every other /ai/* → capabilityUnavailable 501) + domains/unavailable.ts (single-sourced remedy sentence)",
680680
"ai.open-edition-honest-degradation (the API half this item mirrors in the browser — 501 body/discovery parity is proven THERE, not re-proven here)"
681681
],
682682
"history": [
683-
{ "revision": 1, "date": "2026-08-30", "change": "new item (sweep 2026-08-30, cross-angle hit 1+4): the console-side AI gating had no coverage — the catalog-signal design (useAiSurfaceEnabled) hides FAB/dock/⌘⇧I/header/Home affordances and no-flash-redirects /ai on an agent-less boot, while SystemHubPage's 'AI Approvals' card is built ungated and its page polls the dead /ai/pending-actions endpoint every 5s (501) rendering a fake empty queue beside the error alert. Authored as a NEW browser sibling of ai.open-edition-honest-degradation (which keeps the API half) rather than extending it, so neither surface double-covers the other; the two K2 defect clauses are expected-fail probes, the defect row itself is the sweep's FOLLOW-UPS entry", "ref": "#sweep-2026-08-30" }
683+
{ "revision": 1, "date": "2026-08-30", "change": "new item (sweep 2026-08-30, cross-angle hit 1+4): the console-side AI gating had no coverage — the catalog-signal design (useAiSurfaceEnabled) hides FAB/dock/⌘⇧I/header/Home affordances and no-flash-redirects /ai on an agent-less boot, while SystemHubPage's 'AI Approvals' card is built ungated and its page polls the dead /ai/pending-actions endpoint every 5s (501) rendering a fake empty queue beside the error alert. Authored as a NEW browser sibling of ai.open-edition-honest-degradation (which keeps the API half) rather than extending it, so neither surface double-covers the other; the two K2 defect clauses are expected-fail probes, the defect row itself is the sweep's FOLLOW-UPS entry", "ref": "#sweep-2026-08-30" },
684+
{ "revision": 2, "date": "2026-10-05", "change": "clause 4, steps 6-7, negative 3, the title and the source re-pointed (stale, #21782 VF4). objectui 9eea12a2b (objectui#10507, fixing objectui#3743) removed SystemHubPage and its 'AI Approvals' card before 17.6.0, so K2's card half is moot. The clause now asserts that no console surface advertises the entry (verifier's wording). The route half of K2 remains: system/ai-approvals is still registered ungated (AppContent systemRoutes), so steps 6-7 reach it by URL and clause 6 keeps its expected-fail scoring. Negative 3 names clause 6 only. Re-read at objectui 9dfaca6543: unchanged", "ref": "#21797" }
684685
]
685686
},
686687
{

‎docs/qa/platform-checklist/areas/approvals.json‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -174,22 +174,23 @@
174174
"title": "Inbox actions are metadata-driven and gated by the viewer's relationship",
175175
"since": "v16",
176176
"status": "active",
177-
"revision": 3,
177+
"revision": 4,
178178
"priority": "P1",
179179
"surface": "browser",
180-
"personas": ["approver who is NOT the submitter", "same user viewing a request they submitted"],
180+
"personas": ["an approver who is NOT the submitter and NOT an override actor (no platform-admin or tenant-admin standing), so record.viewer.can_override is false on the request", "the dev admin: the submitter of the invoice request (the own-request case) and an OVERRIDE actor (a platform admin — record.viewer.can_override is true on any PENDING request), for the override half of clause 2"],
181181
"fixtures": {
182182
"app": "showcase",
183183
"requires": [
184184
"requests with a real (non-null) submitter — seeded since #3411 (invoice submitted by admin, others by a no-position persona)",
185-
"seed-approval-demo.ts: the invoice dual sign-off request is submitted BY the admin (their own request, so 我发起的 is non-empty), while EXP-2001 / EXP-DEMO are submitted by Mei Phone (other-submitter requests the admin approves)"
185+
"seed-approval-demo.ts: the invoice dual sign-off request is submitted BY the admin (their own request, so 我发起的 is non-empty), while EXP-2001 / EXP-DEMO are submitted by Mei Phone (other-submitter requests the admin approves)",
186+
"a non-override approver on an other-submitter PENDING request: the seeded approver of EXP-2001 / EXP-DEMO is the dev admin, an override actor, so invite a plain member and make them an approver of one such request (for example through the request's reassign action as the admin) before step 2"
186187
]
187188
},
188189
"steps": [
189-
"boot showcase isolated (dogfood §0); sign in as the dev admin",
190-
"other-submitter case: reach the inbox the way a user does — the Approvals entry in the account app's Inbox group (/_console/apps/com.objectstack.account/component/approvals/inbox), NOT the bare /system/approvals deep link (#7234) — open the EXP-DEMO request from the 待我审批 tab; screenshot, then read the rendered action set from the drawer DOM",
190+
"boot showcase isolated (dogfood §0); sign in as the dev admin for the own-request case and the override half, and as the non-override approver (personas) for the other-submitter case",
191+
"other-submitter case, as the NON-override approver: reach the inbox the way a user does — the Approvals entry in the account app's Inbox group (/_console/apps/com.objectstack.account/component/approvals/inbox), NOT the bare /system/approvals deep link (#7234) — open the other-submitter request from the 待我审批 tab; screenshot, then read the rendered action set from the drawer DOM. Then open the SAME pending request as the dev admin and read its action set too (the override half)",
191192
"own-request case: open the invoice request from the 我发起的 tab; screenshot, then read its action set",
192-
"GET /api/v1/approvals/requests/:id for both and record the server-computed viewer flags (can_act, is_submitter) and status",
193+
"GET /api/v1/approvals/requests/:id for both requests, as each viewer, and record the server-computed viewer flags (can_act, is_submitter, can_override) and status",
193194
"cross-check each rendered action against the declared metadata actions on sys_approval_request (visibility expressions gate on record.viewer.can_act / record.viewer.is_submitter / record.status)",
194195
"spot-check an approver action: approve EXP-DEMO from the drawer; capture the POST and re-read the request",
195196
"spot-check a submitter action: send a reminder on the invoice request; capture POST /api/v1/approvals/requests/:id/remind",
@@ -203,15 +204,15 @@
203204
"evidence": "screenshot + DOM action list"
204205
},
205206
{
206-
"clause": "submitter-side actions (send-reminder, recall) appear ONLY on the viewer's own request",
207+
"clause": "send-reminder appears only on the viewer's own pending request; Recall appears on the viewer's own pending/returned request and, for an override actor (platform/tenant admin), on any pending request; a non-admin approver sees neither on another submitter's request.",
207208
"oracle": "dom",
208-
"verify": "own-request drawer shows the two extra actions; other-submitter drawer does not",
209-
"evidence": "side-by-side action lists for the two requests"
209+
"verify": "the own-request drawer shows both; the non-override approver's drawer on the other-submitter request shows neither; the dev admin's drawer on that same pending request shows Recall and NOT send-reminder — approval_recall's second visibility arm is record.viewer.can_override (the #3424 override lever, button added by #12716), and approval_remind stays submitter-only. Read each viewer's flags beside its drawer",
210+
"evidence": "action lists for the three (viewer, request) pairs + the viewer flags each was rendered against"
210211
},
211212
{
212213
"clause": "the rendered gating mirrors the server's viewer flags, which are computed on the request read — not a client heuristic",
213214
"oracle": "api",
214-
"verify": "for both requests, the API's viewer flags (can_act / is_submitter) predict exactly which action groups rendered; declared visibility expressions on sys_approval_request gate on those flags plus status",
215+
"verify": "for both requests and every viewer, the API's viewer flags (can_act / is_submitter / can_override) predict exactly which action groups rendered; declared visibility expressions on sys_approval_request gate on those flags plus status",
215216
"evidence": "request reads + the rendered sets"
216217
},
217218
{
@@ -233,14 +234,15 @@
233234
"traps": ["automation-input", "hydration-race"],
234235
"source": [
235236
"#3358 §1", "#3411",
236-
"packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (declared actions + viewer-flag visibility expressions)",
237+
"packages/plugins/plugin-approvals/src/sys-approval-request.object.ts (declared actions + viewer-flag visibility expressions — approval_recall's submitter arm OR record.viewer.can_override, #12716; approval_remind submitter-only)",
237238
"packages/rest/src/rest-route-ledger.ts (the approvals action route family)",
238239
"objectui apps/console/src/pages/system/ApprovalsInboxPage.tsx (server-declared actions rendered; 待我审批 / 我发起的 tabs)"
239240
],
240241
"history": [
241242
{ "revision": 1, "date": "2026-08-07", "change": "initial import from the #3358 evidence run (7-action table proven after #3411 stamped real submitters)", "ref": "#3358" },
242243
{ "revision": 2, "date": "2026-08-07", "change": "expanded to deep-test contract: concrete steps, multi-clause acceptance, negatives, variants", "ref": "claude/platform-test-checklist-ocwugl" },
243-
{ "revision": 3, "date": "2026-08-10", "change": "entry path refreshed: the inbox is reached through the account app's Approvals nav entry (component route) instead of the bare /system/approvals deep link, so the item exercises the post-#7213 user path", "ref": "#7331" }
244+
{ "revision": 3, "date": "2026-08-10", "change": "entry path refreshed: the inbox is reached through the account app's Approvals nav entry (component route) instead of the bare /system/approvals deep link, so the item exercises the post-#7213 user path", "ref": "#7331" },
245+
{ "revision": 4, "date": "2026-10-05", "change": "clause 2 re-pointed, with the personas, steps 1, 2 and 4, the clause-3 verify and a fixtures line (stale, #21782 VF4). In sys-approval-request.object.ts, approval_recall's visible is the submitter arm OR record.viewer.can_override == true (the #3424 override lever, button added by #12716), which platform/tenant admins hold on any pending request (isOverrideActor); approval_remind stays submitter-only. Revision 3 (2026-08-10) predates #12716. The verifier's wording is used for the clause. The run's approver persona was the dev admin, an override actor, so against that persona the old clause also read as an invalid pairing. The personas now name a non-override approver, and the admin's override view is asserted as its own leg", "ref": "#21797" }
244246
]
245247
},
246248
{

0 commit comments

Comments
 (0)