Repository navigation
Commit 7343229
docs(qa): 18.0 sweep anchor round 18 — approvals.record-page-decisions re-derived against the shipped record page, the packaged-automation board's Activation cell and 22 own-port boot respellings (#25018)
Refs #24579
Clause-②: no
Round 18 of the 18.0 sweep's wave anchor (claim `6304655523`, PM loop
round 33). It applies round 17's three carried checklist-text findings,
PR #25003's out-of-scope findings as its ACCEPT `6304389370` and landing
record `6304641541` carry them: `approvals.record-page-decisions`
re-derived against the record page objectui ships,
`automation.setup-packaged-automation-board` `steps[3]`, and 22 other
texts that named the own-port boot or its DB "isolated".
Only checklist text changes. 19 items in 6 area files each take one
`revision` bump and one `history` row, and each row names PR #25003 and
its finding. There is no product code, test, pin, changeset text or
`RUNNER.md` change, no new item, and every item id is kept.
- Base `93e6c1a5f8`. `origin/main` `62bc129308` is merged in
`1fa304d42f`. Its one commit touches `packages/cli` only: no
`docs/qa/**` path and none of the sources these edits cite.
- Every item was read at the base, and every objectui source at the
console pin `cf153eb2e535` (`.objectui-sha` on `main`), with `git show`
from a full scratch clone (`--is-shallow-repository` false). objectui
`99782f961` (objectui#3055) is an ancestor of the pin: exit 0. Control,
the pin as an ancestor of `99782f961`: exit 1.
- A scripted span editor made every edit. It is round 17's editor,
unchanged (same sha256), and it never re-serialises a file. For each
area file it re-parses the result and asserts four things, and all six
files passed:
- A1: the new document equals the old one with the declared ops applied.
- A2: every other item and top-level key is value-equal.
- A3: each edited item's changed paths lie inside its declared fields
(array edits by exact index), plus `revision` and the one new history
row.
- A4: the text outside the edited items is byte-identical.
- A whole-branch check against the base agrees: 19 items changed, each
with revision +1 and exactly one appended row, and no other file or path
changed.
- The new history rows carry the placeholder ref `#PENDING-24579R18`,
because a row's `ref` names this PR and the PR had no number yet. The
next commit on this branch swaps in this PR's number and changes nothing
else. The gates are re-run on that head, and the report comment on
#24579 records them.
- The live verdicts at the pin are NOT MEASURED. The objectui and server
sources were read, not run.
## 1. `approvals.record-page-decisions` (revision 3 → 4)
At `cf153eb2e535` the record page draws no header Approve/Reject.
`RecordDetailView.tsx` (blob `64a7c675e50d`) mounts `DeclaredActionsBar`
(blob `f27b4ab0efff`) above the page body whenever the record has a
pending request. The dispatch record is the pending
`sys_approval_request` row, the location is `record_section`, and
`approval_remind` is excluded (`recordApprovalActions.ts`, blob
`f6aabe078ea0`). The bar reads `sys_approval_request`'s declared actions
and evaluates each one's `visible` predicate, failing closed, against
the row's `viewer` block. The server computes that block per caller
(`ApprovalService.attachViewers`: `can_act`, `is_submitter`,
`can_override`). `useRecordApprovals.ts` (blob `b65a91f4a9a4`) only
reads, and holds no `canDecide`.
The steps, numbered from 1 as the item's own text numbers them:
| step | old | new | source read |
|:--|:--|:--|:--|
| 1 (`steps[0]`) | boot on its own port and DB; sign in as the dev admin
| unchanged | — |
| 2 (`steps[1]`) | PATCH budget 200000; the flow parks at
manager_review, lockRecord:false | the same, plus: the admin is the
request's submitter as well as its approver | `approval-node.ts` passes
the run's trigger user as `submitterId`; `seed-approval-demo.ts`
`ADMIN_APPROVAL_POSITIONS` |
| 3 (`steps[2]`) | screenshot the header: Approve and Reject in the
primary slot (`buildApprovalDecisionActions`, order -100 / -99,
`record_header`) | screenshot the decision bar above the page body. For
the admin: Approve (filled, declared `primary`), Reject (destructive,
`danger`), Reassign, Send back, Request info, Recall and Reply, in
declaration order, each with a `declared-action-` testid. Send reminder
is excluded and Resubmit is hidden. Capture the approvals read and its
`viewer` block | `RecordDetailView.tsx`, `DeclaredActionsBar.tsx`,
`recordApprovalActions.ts`; `sys-approval-request.object.ts` (the
declarations and their `visible`) |
| 4 (`steps[3]`) | inline edit stays live under manager_review | the
same, plus: the row reads `lock_record: false` | `approval-service.ts`
(`lock_record` from the node's `lockRecord`); `useRecordApprovals.ts`
`recordLockedByApproval` |
| 5 (`steps[4]`) | click Reject: ONE dialog, titled by the Reject label,
with the confirm question as its description | the same, named from the
declaration: title Reject, description "Reject this request? A rejection
is final for every approver.", Comment and Attachments inline, no
confirm in front of it | `useConsoleActionRuntime.tsx` (title from
`label`, description from `description`); `approval_reject` declares a
`description` and no `confirmText` |
| 6 (`steps[5]`) | Confirm; capture the POST to reject; re-read
status=rejected | the same, plus: the POST's `:id` is the request's id;
the comment is in the body and on the `GET …/:id/actions` row; the bar
draws nothing once no request is pending | `approvals-routes.ts` (the
decide and actions routes); `DeclaredActionsBar.tsx` (the row rides
`params._rowRecord`) |
| 7 (`steps[6]`) | approve path: Approve, one dialog, POST to approve,
status approved | the same, with the declared title and description; at
or under 500000 the run ends approved | `approval_approve` in
`sys-approval-request.object.ts`; the flow's `needs_exec` edges |
| 8 (`steps[7]`) | drive a budget above 500000 to exec_review; inline
edit hidden | PATCH above 500000 and approve manager_review first:
`needs_exec` routes to exec_review (position `exec`, held by the admin),
a new request with `lock_record: true`; inline edit hidden, and the
approval band shows its locked state | `BudgetApprovalFlow` (edge e4,
`budget > 500000`); `resolveApprovalLock` (objectui `packages/react`
`InlineEditContext.tsx`) |
| 9 (`steps[8]`) | a non-approver member: the header offers no
Approve/Reject (`approvals.canDecide` false); not the dev admin | the
member's own approvals read serves no row for the record, so the page
draws no decision action; a forged POST to approve answers 403
`FORBIDDEN`, and the request stays pending; not the dev admin, who holds
the override | `request-read-gate.ts` header (participants and the
override actor; the record-reader tier is off, and no showcase object
declares `approvalsVisibleToReaders`); `ApprovalService.decide`;
`approvals-routes.ts` (`FORBIDDEN` → 403) |
| 10 (`steps[9]`, new) | — (step 9 named the override pair only as the
reason not to use the admin) | the off-slate admin: reassign the step
away and re-read `viewer` (`can_act` false, `can_override` true). The
bar shows Override Approve / Override Reject / Override Reassign (amber,
`data-override-decision`), no Send back or Request info, and a plain
Recall and Reply | `DeclaredActionsBar.tsx` and `approvalOverride.ts`
(`isOverrideDecision`); the declarations' `can_override` arms |
The clauses:
| clause | old | new | source read |
|:--|:--|:--|:--|
| `acceptance[0]` (screenshot) | Approve and Reject in the record
header's primary slot | the pending approver is offered the declared
decisions in the bar above the page body, not in the header. Each one is
kept or dropped by its `visible` over the row's `viewer` block |
`RecordDetailView.tsx` (its header-actions comment: no approval action
is injected there); `DeclaredActionsBar.tsx` |
| `acceptance[1]` (network) | Reject after ONE dialog, comment inline |
the same clause, named from the declaration: the confirm question rides
`description` and there is no `confirmText`, so the runtime chains no
confirm | `useConsoleActionRuntime.tsx`;
`sys-approval-request.object.ts` |
| `acceptance[2]` (api) | the decision POSTs and re-reads; the header
collected the comment (`actionParams`) | the POST goes to the REQUEST's
id; the comment is the declared `comment` param and round-trips onto the
`/actions` row | `approvals-routes.ts`; `DeclaredActionsBar.tsx` |
| `acceptance[3]` (screenshot) | a locking approval hides inline edit
(the clause is unchanged) | verify and evidence add each row's
`lock_record` and the band's locked state | `approval-service.ts`;
`RecordDetailView.tsx` (`resolveApprovalLock` gates `canEdit`) |
| `acceptance[4]` (dom → api) | the header DOM of a non-approver has no
approve_request/reject_request while `approvals.canDecide` is false |
gated by the SERVER's viewer block: the member's approvals read serves
no row, no `declared-action-*` button renders, and a forged POST answers
403 `FORBIDDEN`. The off-slate admin's Override framing is recorded
separately | `ApprovalService.attachViewers` and `decide`;
`request-read-gate.ts`; `DeclaredActionsBar.tsx` |
The item's other fields:
- `title`: "Approve/Reject render in the record header; …" →
"Approve/Reject render on the record page from sys_approval_request's
declared actions; …". The intent (deciding an approval from the record
page), the id and the priority (P2) are kept.
- `fixtures.requires[0]` now says that exec_review is reached only after
manager_review approves, and that the admin holds `exec` and submits
each request. `fixtures.requires[1]`: `buildApprovalDecisionActions` →
`DeclaredActionsBar` over the pending row, `recordApprovalActions.ts`,
and the server's declarations.
- `fixtures.knownGaps[0]`, round 17's note about the retired text, is
removed. It was the only entry, so `fixtures.knownGaps` goes.
- `negative[2]`: "Approve/Reject rendered on the header for a viewer who
is not a pending approver (canDecide false)" → a declared decision
action rendered for a viewer whose `viewer` block admits none.
- `source[0]` and `source[1]` now cite `RecordDetailView.tsx` and
`DeclaredActionsBar.tsx` (with `useRecordApprovals.ts` and
`useConsoleActionRuntime.tsx`) at the pin. `source[4]` (the declarations
and `attachViewers`) and `source[5]` (the read gate and `decide`) are
added. `source[2]` (the two pins) and `source[3]` (the flow) are
unchanged.
- No `automated` is added. The item had no `automated` field at the
base: round 17 re-pointed `source[2]`. Neither pin holds a clause whole
at its layer. objectui's
`RecordDetailView.approvalDeclaredActions.test.tsx` (blob
`9519ae70f743`) mounts the page in jsdom over a fetch double, with a
hand-written `sys_approval_request` definition and the param dialog
mocked. `sys-approval-request.object.test.ts` reads the declarations.
## 2. `automation.setup-packaged-automation-board` `steps[3]` (revision
6 → 7)
Old: "confirm every switch reads On except a flow authored status:
'obsolete' (showcase_approver_bindings, …), whose switch correctly reads
Off — 31 On / 1 Off at QA run #24545's subject".
New: the Activation cell is read as two facts. The switch shows the
activation flag. The badge beside it (`data-testid`
`packaged-flow-activation`, with a `data-reading` attribute) reads 'On'
for an enabled flow whose `_status` row carries no `unboundCause`, 'Not
running here' with the switch still checked for one that carries one,
and 'Off' with the switch unchecked for a disabled flow. On a default
boot `showcase_scheduled_digest` and `showcase_task_due_reminder` carry
`deployment_policy`. Sources at the pin: `packagedFlows.ts`
`activationReading` (blob `a85c96d28e5a`) and
`PackagedAutomationPage.tsx` (blob `f4aa90e11d32`). At the base:
`describeUnboundReason` (service-automation `engine.ts`),
`FLOW_UNBOUND_CAUSES` (spec `automation-service.ts`) and RUNNER.md's
scheduled-work entry.
The count is stated with its derivation from the 32 flows `allFlows`
declares at `93e6c1a5f8`
(`examples/app-showcase/src/automation/flows/index.ts`), each classified
by its authored `status` and its start trigger:
| flows | count | reads on a default boot |
|:--|--:|:--|
| record-triggered (18) and `api`-triggered (1), all of which bind | 19
| On |
| no trigger: screen flows and invoked subflows | 10 | On |
| `type: 'schedule'`: `showcase_scheduled_digest` (interval),
`showcase_task_due_reminder` (time_relative) | 2 | Not running here |
| `status: 'obsolete'`: `showcase_approver_bindings` | 1 | Off |
That is 29 On / 2 Not running here / 1 Off, and 31 On / 1 Off with
`OS_AUTOMATION_SCHEDULED_WORK_ENABLED` exported. The 19 that bind agree
with RUNNER.md's measured banner, `32 flow(s) 19 bound` on a default
boot and `21 bound` with the switch. Only `obsolete` and `invalid`
disable a flow (`flowStatusDisabled` in the engine), so the 10 flows
with no authored `status` (`draft`) are enabled. The step tells the run
to reconcile against `_status` rather than these digits.
## 3. The 22 other "isolated" texts
**22 respelled, in 21 strings across 17 items; 43 left.** At the base,
65 non-history occurrences of "isolated" remained after round 17. Each
one was read in its item:
- 22 name the own-port boot or its DB. They are respelled.
- 41 name the `isolated` tenancy posture (`OS_TENANCY_POSTURE=isolated`,
"group/isolated", walled boots). They are untouched. One of them sits in
an edited item: `access-security.platform-owner-email-anchor`
`steps[0]`, "a walled posture (group / isolated)".
- 2 are other senses and are untouched: "Saved reports are
owner-isolated" (`dashboards.saved-report-ownership`, its title) and
"unroutable from the isolated env" (`automation.flow-error-handling`).
At the head the scan reads 43 occurrences, which are those 41 and 2.
| item | field | old | new |
|:--|:--|:--|:--|
| `automation.connector-dispatch-matrix` | `fixtures.requires[2]` | "so
an isolated boot without it dials 3000" | "so a boot on its own port
without it dials 3000" |
| `automation.connector-dispatch-matrix` | `fixtures.knownGaps[1]` | "so
a dogfood-§0 isolated boot on a non-3000 port" | "so a dogfood-§0 boot
on its own non-3000 port" |
| `automation.packaged-flow-subflow-disable-refusal` | `steps[6]` | "(or
discard the isolated DB)" | "(or discard the run's own DB)" |
| `automation.packaged-flow-clone-contract` | `steps[8]` | "(or discard
the isolated DB)" | "(or discard the run's own DB)" |
| `access-security.platform-owner-email-anchor` |
`fixtures.knownGaps[0]` | "its own isolated scratch database" | "its own
scratch database" |
| `platform-core.boot-health` | `preconditions[0]` | "isolated run per
dogfood-verification §0:" | "the run's own boot per dogfood-verification
§0:" |
| `platform-core.seed-integrity` | `fixtures.requires[0]` | "a FRESH
isolated boot (new file DB)" | "a FRESH boot on its own port and a new
file DB" |
| `platform-core.interrupted-migration-boot-report` |
`fixtures.requires[1]` | "an isolated persistent sqlite DB (own port," |
"the run's own persistent sqlite DB (own port," |
| `platform-core.manifest-install-contract` | `steps[9]` | "or simply
discard the isolated file DB" | "or simply discard the run's own file
DB" |
| `platform-core.manifest-install-contract` | `steps[9]` | "the cheaper
path an isolated boot makes free" | "the cheaper path a boot on its own
port and DB makes free" |
| `platform-core.activation-ledger-registration-home` |
`fixtures.requires[0]` | "need nothing beyond an isolated stock boot" |
"need nothing beyond a stock boot on its own port and DB" |
| `platform-core.activation-ledger-registration-home` | `steps[1]` |
"(stock `objectstack dev`, isolated port/DB)" | "(stock `objectstack
dev`, on its own port and DB)" |
| `platform-core.activation-ledger-row-contract` |
`fixtures.requires[0]` | "an isolated boot on a file DB (-d" | "a boot
on its own port and file DB (-d" |
| `platform-core.activation-ledger-row-contract` | `steps[0]` | "boot
isolated on the file DB;" | "boot on its own port with that file DB;" |
| `platform-core.marketplace-install-local-lifecycle` |
`fixtures.requires[0]` | "TWO isolated boots of the same app:" | "TWO
boots of the same app, each on its own port and DB:" |
| `platform-core.runtime-config-boot-read` | `fixtures.requires[0]` |
"any stock isolated boot —" | "any stock boot on its own port and DB —"
|
| `platform-core.runtime-config-boot-read` | `steps[0]` | "on a stock
isolated boot, with NO session" | "on a stock boot on its own port and
DB, with NO session" |
| `platform-core.lifecycle-retention-sweep` | `fixtures.requires[0]` |
"an isolated boot on a file DB (-d" | "a boot on its own port and file
DB (-d" |
| `platform-core.seed-mode-matrix` | `steps[7]` | "or discard the
isolated DB" | "or discard the run's own DB" |
| `records-forms.field-group-visible-when` | `fixtures.requires[0]` |
"and an isolated boot — the item" | "and a boot on its own port and DB —
the item" |
| `records-forms.field-unique-enforcement` | `fixtures.requires[0]` |
"an isolated dev boot on the stock SQLite store" | "a dev boot on its
own port and DB, on the stock SQLite store" |
| `studio-authoring.metadata-diagnostics-sweep` |
`fixtures.knownGaps[0]` | "ON AN ISOLATED SCRATCH FILE DB" | "ON THE
RUN'S OWN SCRATCH FILE DB" |
None of the 22 named RUNNER's canonical boot line, so each takes the
"own port and DB" family of spellings, with "the run's own DB" where the
text is about a teardown or a database. None of the 22 sits in a clause,
and no posture text is touched.
## Acceptance notes
- Some edits sit beyond the dispatch's field list, inside the item it
names, to keep that item consistent with its own re-derivation: `title`
(it said "render in the record header"), `fixtures.requires[0]`
(exec_review is reached only after manager_review approves),
`negative[2]` (it named `canDecide`), the new `steps[9]`, and
`source[4]`–`[5]`.
- The dispatch says to keep `automated.ref` only for clauses a pin
asserts whole at their layer. The item carries no `automated` at the
base, because round 17 re-pointed `source[2]`, and neither pin holds a
clause whole at its layer. So none is added.
- `acceptance[4]`'s oracle moves from `dom` to `api`. The clause is the
server's gate, and RUNNER.md rule 4 makes the server the authority. The
UI absence is still read, after a screenshot.
- On a stock showcase the closed-side member reads no request at all.
The approvals door serves requests to participants and to the override
actor, and the record-reader tier is off. So the member's page draws no
bar, rather than a bar with every button hidden, and the forged POST is
the server-side proof.
- The off-slate admin also keeps a plain Recall and Reply, because the
admin submitted the request. `isOverrideDecision` leaves a lever the
viewer holds through its submitter arm unframed (objectui#13225).
## Gates
The derived list is `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack`. It is computed on the actual change
set: 6 paths against merge base `62bc12930`, at head `1fa304d42f`. It
gives 13 commands. Each one ran at that head with its exit code captured
before any pipe, and each exited 0:
`node scripts/check-ci-filter-parity.mjs` · `node
scripts/check-closing-keyword-parity.mjs` · `node
scripts/check-closing-keyword-parity.mjs --self-test` · `node
scripts/check-comment-mask-corpus.mjs` · `pnpm --filter
@objectstack/lint run check:doc-formula-expressions` · `pnpm
check:cross-package-test-inputs` · `pnpm check:doc-authoring` · `pnpm
check:driver-memory-census` · `pnpm check:gitlink-declared` · `pnpm
check:nul-bytes` · `pnpm check:platform-checklist` · `pnpm
check:refd-timer-probe` · `pnpm check:watch-hint-literal`
The same 13 were run before the merge, at `46f612b0a1`. There, the first
run of `check:doc-formula-expressions` refused with exit 3, a
prerequisite: `@objectstack/formula` and `@objectstack/lint` were not
built. Both were built under the shared verify lock (`VERDICT
command-exit 0`, held 11 s, waited 0 s), and the rerun exited 0. The
`--ran` reconciliation reads "13 derived, 13 run, 0 NOT-MEASURED, 0
UNRUN" at both heads.
No per-PR CI job runs `pnpm check:platform-checklist`, so this local
reading is its evidence:
| reading | base `93e6c1a5f8` | head `1fa304d42f` |
|:--|:--|:--|
| items | 16 areas, 328 items (323 active, 1 planned) | 16 areas, 328
items (323 active, 1 planned) |
| line citations | 0 survive across 21 swept documents | 0 survive
across 21 swept documents |
| symbol anchors | 975/985 against 433 cited sources, 10 on the named
residual (#16898), 18 file floors held | 979/989 against 434 cited
sources, 10 on the named residual (#16898), 18 file floors held |
The four new anchors all resolve:
`seed-approval-demo.ts#ADMIN_APPROVAL_POSITIONS`,
`sys-approval-request.object.ts#approval_reject`,
`approval-service.ts#attachViewers` and `approval-service.ts#decide`. No
line citation is added.
`git merge-tree --write-tree origin/main HEAD` exited 0 against
`62bc129308`. No open PR touches `docs/qa/platform-checklist/`: all 16
open PRs' file lists were read over REST, and the three with more than
100 files were read page by page, at 349, 367 and 235 files, each equal
to its `changed_files`. So there was no second merge-tree to run.
There is no ablation, because no tested behaviour changed.
`skip-changeset` was re-measured: no `files[]` entry of the 73
non-private manifests (86 tracked `package.json` files) reaches outside
its own package or names `docs/qa`, and the repo root is private.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 094a39c commit 7343229
6 files changed
Lines changed: 171 additions & 77 deletions
File tree
- docs/qa/platform-checklist/areas
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2919 | 2919 | | |
2920 | 2920 | | |
2921 | 2921 | | |
2922 | | - | |
| 2922 | + | |
2923 | 2923 | | |
2924 | 2924 | | |
2925 | 2925 | | |
| |||
2933 | 2933 | | |
2934 | 2934 | | |
2935 | 2935 | | |
2936 | | - | |
| 2936 | + | |
2937 | 2937 | | |
2938 | 2938 | | |
2939 | 2939 | | |
| |||
3052 | 3052 | | |
3053 | 3053 | | |
3054 | 3054 | | |
| 3055 | + | |
| 3056 | + | |
| 3057 | + | |
| 3058 | + | |
| 3059 | + | |
| 3060 | + | |
3055 | 3061 | | |
3056 | 3062 | | |
3057 | 3063 | | |
| |||
0 commit comments