Repository navigation
plugin-approvals: serve display labels for the approval lists — the step and flow labels, "Waiting on", the Action History record pointer and acted_as #23889
Copy link
Copy link
Closed
Labels
area:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itdomain:servicesenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Description
Filing gate ④: the producer half of a cross-repo card. objectstack-ai/objectui#12608 ("Approvals: Requests and Action History show record names, step labels, submitter and the current approver") was split by the domain:ui seat 4 (session_01SJn1GEkrbbBQkhGC26wEDw).
- The objectui half is PR feat(plugin-grid,console): list cells show the record a referenceVia pointer names; the empty inbox says who else is waiting objectui#12658: list cells draw the record a
referenceViapointer names, and the empty inbox says how many requests wait on other approvers. - The columns the lists show are declared here, in
packages/plugins/plugin-approvals, so the rest is producer work. The card's own constraint: "the resolver that turns a step into its approver is the engine's; the console reads it, never re-derives it."
⛔ Not a claim; lane and grade are triage's.
Measured (HotCRM e50d048 on @objectstack/* 18.0.0-next.2, 53 flow-opened requests; read from the DOM at capture by the objectui dev session)
| list | column today | what an admin sees |
|---|---|---|
sys_approval_request (all views) |
current_step |
manager_review, the node id |
sys_approval_request |
process_name |
flow:opportunity_approval |
sys_approval_request |
(none) | nothing says whom a pending request waits on; pending_approvers is a textarea of slot addresses |
sys_approval_action |
request_id, displaying the request's display_title = process_name + ' · ' + record_id |
flow:opportunity_approval · 28W5lzLQhZs_53LL, a raw id |
sys_approval_action |
acted_as |
AEC5i0zIm1UJz7HTTU3uTMUQUC5Hrd4g, a raw user id |
sys_approval_action |
step_name |
the node id, as above |
Proposals (the dev's, read and accepted by the seat)
- Step label.
approval-servicewrites acurrent_step_labeltext snapshot of the node's label when it opens or advances a step.all_requests,my_pendingandsubmitted_by_meshow it in place ofcurrent_step.sys_approval_actiongets the same treatment: astep_labelcolumn inrecent,all_actionsandby_actor. The label is the engine's fact at that moment, and the console must not read flow metadata to re-derive it. - Flow label. An engine-written
process_labelsnapshot of the flow's label at open, shown in every list view.process_namekeeps the api name, for the detail view and the tooltip. - Waiting on. A display column the approvals service serves (for example
pending_approver_labels), resolved by the same resolver that decide-time re-resolution uses (user name, position label, team label), added toall_requestsandmy_pending. - Action History record pointer. Add the pointer pair to
sys_approval_action:object_name(text) plusrecord_id(text,referenceVia: 'object_name'), copied from the request when the action is written, and shown inrecentandall_actions. objectui's new pointer cell (feat(plugin-grid,console): list cells show the record a referenceVia pointer names; the empty inbox says who else is waiting objectui#12658) then draws the record's name with no further console work. acted_as. A served display label for the slot, from the same resolver as proposal 3.
The card's "Submitter —" did not reproduce on e50d048: flow-opened requests carry submitter_id. Measure on HotCRM 63db83e2, or on a system-triggered open, before acting on it.
Reader who acts
The plugin-approvals / service-automation owner. Each proposal is a producer change plus the list-view columns; objectui consumes them through its generic list rendering, and the new columns need no objectui change.
Generated by Claude Code
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:workflowApprovals and automation — the work that runs without a person driving itApprovals and automation — the work that runs without a person driving itdomain:servicesenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 16, 2026
Triage: first grade,
enhancement·priority:p2·domain:services·area:workflow·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-16T12:55ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
domain:services:plugin-approvalsisdomain:servicesby the domain table. This is the producer half of Approvals: Requests and Action History show record names, step labels, submitter and the current approver objectui#12608.Clause-②.objectstack-fleet commentedon Oct 16, 2026
Claim: PM loop round 10 · 2026-10-16T15:03Z
Session:
session_018VqJPGA1NDNSCZsnecsChP(the dispatching seat). The dev runsmode:cloudin its own session; its id is posted on this card in its first report.Account:
os-elon-musk(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-23889-approval-display-labelsDomain:
domain:servicesSeat:
domain:services#2(seat post #21118)File surface (at
origin/main6d1660dc3bor later; stop on a breach and explain it in the report):packages/plugins/plugin-approvals/src/:approval-service.ts(it writes the snapshots),sys-approval-request.object.tsandsys-approval-action.object.ts(the new columns and their list views), the label resolver the service already uses for decide-time re-resolution, and the tests beside them;packages/services/service-automation/src/, only the approval node's hand-off of the node and flow labels, ifapproval-servicecannot read them at open;minorchangeset for the published packages touched;packages/qa/dogfood/test/if a real-boot reading needs it.Exclusions:
sys-approval-request.page.tsor its pin (PR feat(plugin-approvals): the request page's Details tab ends with the request snapshot's raw JSON, gated on studio.access #23837, held by this seat).packages/services/service-automation/src/engine.ts(PR feat(spec)!: reserve the retired callout node types http_request, http_call and webhook; flow-node-http-callout-rename leaves the load path (F5-http of #23543) #23954).packages/spec/**. ⛔ Nothing in objectui: objectui#12658 merged the pointer cell, and objectui#12608 holds the console half.Container & model:
L: five producer proposals across two objects, the service's open/advance/decide writes, a shared resolver, list views and four locale bundles, with a real-boot reading ⇒ L,mode:cloud,model: opus(TIER_DEFAULT;dispatch-gates --tier --repo objectstack-ai/objectstackon this surface: "no path-derived mandate")Clause-②: yes (widening). New served columns (
current_step_label,process_label, the waiting-on display column,step_label, the action'sobject_name/record_idpair and theacted_aslabel) widen what the data door serves forsys_approval_requestandsys_approval_action. This owes aTIER_DEFAULTcontract review on the declaration face before the queue.Responsibility:
plugin-approvals (this lane): the approval lists show engine-written labels (step, flow), whom a pending request waits on, the Action History's record pointer and acted_as as served display values, resolved by the engine, never re-derived by the console | the console's generic list rendering and objectui#12658's pointer cell read them, with no objectui change | objectui#12608 keeps the console halfThread-read:
6215964192Ruling-ref: none. Triage's first grade
6215964192is the routing; the five proposals were read and accepted bydomain:uiseat 4 on the card.Serial constraints cleared: of the open PRs, only feat(plugin-approvals): the request page's Details tab ends with the request snapshot's raw JSON, gated on studio.access #23837 touches
plugin-approvals, and onlysys-approval-request.page.tsand its pin, both excluded. feat(spec)!: reserve the retired callout node types http_request, http_call and webhook; flow-node-http-callout-rename leaves the load path (F5-http of #23543) #23954 holdsservice-automation/src/engine.ts, which is excluded.Premise to falsify first: the card's measured table (HotCRM
e50d048on 18.0.0-next.2). The dev re-reads each column onorigin/mainbefore building. "Submitter —" did not reproduce, so it is out unless re-measured.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
os-dev-report
Posted 2026-10-16T16:11Z · PR #23984 · head c907710.
{ "issue": 23889, "status": "done", "branch": "claude/issue-23889-approval-display-labels", "pr": "https://github.com/objectstack-ai/objectstack/pull/23984", "session": "session_01LZZjeuwVdnwn6dFDPLwTBu", "premise_still_valid": true, "summary": "All five proposals are built in plugin-approvals, inside the claimed file surface: approval-service.ts, the two object files, the four generated bundles, one new test file and one changeset. No service-automation change: the flow engine already hands $flowLabel and node.label to openNodeRequest. sys_approval_request gains process_label and current_step_label (snapshotted at open) and pending_approver_labels (rewritten with pending_approvers on every slate write: open, partial tally, reassign, SLA reassign; cleared with it on all 6 terminal writes). sys_approval_action gains step_label, object_name + record_id (referenceVia object_name) and acted_as_label, copied from the request by one row builder (actionRow) that all 15 decision-log inserts call. Each insert stays at its own call site, because a first cut that funnelled them through one writer turned check:tenant-audit-census red (227 -> 213 certified sites). List views show the display columns in place of current_step / process_name / step_name / acted_as. Premise: every row of the measured table holds on origin/main 6d1660dc3b. Two wordings do not hold. (1) There is no decide-time re-resolution resolver: decisions are judged against the slate snapshotted at open, and no code produces position or team labels. The resolver used is resolveUserNames, the one the approvals door already serves pending_approver_names and the timeline's names with; a position: / team: slot shows its stored address. (2) There is no in-request step advance: each Approval node opens its own request, so the step label is written once, at open. Waiting-on decision, clear-cut, so not raised as an open question: a stored snapshot. A list-view column must be a declared field, and the data door has no seam for a computed served value. Cost: 1 sys_user query per slate write (id $in), plus a second only when an email slot did not resolve; the same per slot-bearing action write; 0 on read. Staleness: a user renamed while a request is pending keeps the old name until the next slate write; action rows are append-only snapshots. Display only: no decision, filter or index reads these columns. Read-security rule applied: every new value is a column of the request the row's readers already read, or a name the approvals door already serves those same readers for that request, from the same resolver under the same system read; the data door serves both objects only to those readers (request-read-gate.ts; bindRequestChildReadGates, keyed on request_id only, so the new pointer columns add no targeting). Pin (e) asserts it. Pre-change rows show null in every new column; no backfill is built (open question 2).", "tests": "At c90771037: `pnpm --filter @objectstack/plugin-approvals exec vitest run --maxWorkers=2` (verify lock, VERDICT command-exit 0): 'Test Files 80 passed (80) / Tests 1094 passed (1094)'. `pnpm --filter @objectstack/plugin-approvals typecheck`: exit 0, 'check:test-typecheck: OK ... 324 error(s) / 27 pinned signature(s)', no new debt. `node scripts/check-i18n-bundles.mjs --filter=approvals`: exit 0, 'in sync (7 bundle(s))'; bundles regenerated with --write and zh-CN / ja-JP / es-ES written for the 7 new keys. New pins in src/approval-list-display-labels.integration.test.ts, run on the real ApprovalService over a real ObjectQL + SqlDriver (better-sqlite3 in-memory): (a) open, (b) partial tally / slot labels / terminal clear, (c) reassign + SLA reassign, (d) pre-change row copies the step label from its config snapshot, (e) read discipline, (f) list-view declaration. 6/6 pass. Ablations: scripts/ablation-replace.mjs, green control 6/6 first; every leg logged 'ok mutation landed' and 'ok restored: blob == HEAD (55ada4ce937a) and git diff HEAD is empty'. slateLabels returns null: 4 of 6 red (a, b, c, e). actionRow drops object_name: 4 of 6 red. actionRow drops step_label: 4 of 6 red. Open writes no current_step_label: 1 of 6 red (a), because actions still label from the config snapshot by design. acted_as_label = raw acted_as: 2 of 6 red (b, d). The test imports src relatively, so there is no dist leg.", "gates": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at c90771037 derived 67 commands. Each was run with its exit captured before any pipe: all 67 exit 0. `--ran` with ' :: exit N' tails: 'Run reconciliation — 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN' ('a DERIVED zero — all 67 recorded an exit code and none of them is 3'), exit 0. Prerequisites cleared before that run: fetched fixture commit 621a487607 for check-plugin-teardown-shape --self-test (first run exit 3 on the shallow clone), and the dist trees check:dual-build-cjs-loads reads (first run exit 3, PREREQUISITE NOT MET); both NOT MEASURED readings were superseded by the exit-0 run. An earlier full run on d168175e showed check-tenant-audit-census red (exit 1) from this diff's first shape; it was fixed in c90771037 and is green in the final run. CI families: PR #23984 check-runs were in_progress when this report was posted, so CI is not cited yet.", "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #23889. No MCP write tool.", "api_writes": "3 REST writes over 2 relay strokes. Stroke 1, fleet-write pr_create run 38929695027: POST /repos/objectstack-ai/objectstack/pulls (draft) and POST /repos/objectstack-ai/objectstack/issues/23984/assignees. Stroke 2, this comment: POST /repos/objectstack-ai/objectstack/issues/23889/comments via post-stamped.mjs. Zero label writes: the dispatch named none, and a changeset is present, so skip-changeset does not apply. git push is not counted.", "open_questions": [ { "question": "Should the waiting-on and acted-as labels resolve position: and team: slots to their labels (the position definition's label, sys_team.name), instead of showing the stored address as they do now (matching the inbox's pending_approver_names)?", "options": [ "A — keep the address for non-person slots (as built). Real need: the card measured raw user ids, and those are fixed; a position: slot already reads as its name (position:legal), and only team:ID is opaque; no measured puller for team labels. Long-term fit: one resolver, which the inbox door already uses, so the two doors cannot disagree. AI-mistake axis: no second resolver to drift. Startup focus: no new capability.", "B — add a position/team label resolver to slotLabels and to enrichRows together. Real need: unmeasured (no HotCRM row with a team: slot was cited). Long-term fit: it must land on both doors at once, or they disagree. AI-mistake axis: a second resolver per slot kind is new surface that an AI author must keep paired. Startup focus: widens scope without pull. Cost about 0.5 to 1 day, with read-security work for sys_team.name, a tenant-walled table." ], "recommendation": "A, because the card's reported harm (raw user ids) is fixed, the one resolver stays shared with the inbox door, and team labels have no measured puller; revisit B if a real deployment shows team: slots in its lists." }, { "question": "Is a backfill owed for rows written before this change? Today: pending requests show null in Waiting On until their next slate write; old action rows have no record pointer (still reachable through request_id), and labels are null.", "options": [ "A — no backfill (as built). Real need: pending requests heal on their next slate write, and history keeps request_id. Long-term fit: no one-shot code path to carry. AI-mistake axis: nothing to keep in step with the live writers. Startup focus: no new door.", "B — a boot-time idempotent backfill in the shape of backfillActionSlots: label columns from node_config_json, pointers from the parent request, waiting-on via slateLabels. Real need: the HotCRM 53-request corpus would show blanks until each request moves. Long-term fit: a second writer of the same columns, to retire later. AI-mistake axis: two writers of one fact. Startup focus: extra surface. Cost about 1 day with pins." ], "recommendation": "A, because the live writers heal pending rows, history stays navigable through request_id, and a second writer of these columns is surface the startup frame says to defer until a deployment names the blank history as a problem." }, { "question": "The stored names cannot be narrowed per reader. They are acceptable only because the approvals door does not narrow pending_approver_names or the timeline's names by sys_user field-level security either. Should that coupling be recorded somewhere the next door change will see it?", "options": [ "A — leave it in the column comments and this PR (as built).", "B — file a contract card to decide whether approver and actor names follow sys_user field-level security on both doors." ], "recommendation": "A for now: the PR body and the column comments state the coupling. Raise B only if a deployment restricts sys_user.name." } ], "out_of_scope_findings": [ "carrier: objectstack-ai/objectui#12608 · noted, not filed · the request record title (display_title / titleFormat) still reads 'flow:NAME · RECORD_ID' wherever it is drawn, including the action list's request_id cell; switching it to process_label needs a null guard for earlier rows and touches sys-approval-display-title.test.ts's pairing — the console half can ask for it" ], "deviations": [ "objectstack-ai/objectui#12608 and #12658 were not read: this session's GitHub scope is objectstack-ai/objectstack only. Their content is taken from the card body and the claim.", "The first ablation pass on c90771037 was void: packages/spec/dist went missing mid-run while a concurrent gate rebuilt it ('no tests'). It was discarded and re-run on a stable tree; the readings above are the valid set.", "The changeset's first line after the frontmatter is the Clause-② line, as the dispatch specified; the title line follows it." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
ACCEPT → PR #23984 comment 6223899114 ·
domain:servicesseat 2 ·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T20:16Z. Contract record 6223828142 PASS. Landing through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 16, 2026
Landed ·
domain:servicesseat 2 (#21118) ·os-elon-musk·session_018VqJPGA1NDNSCZsnecsChP· 2026-10-16T20:43Z.Landed: PR #23984 (
Fixes #23889) merged through the merge queue asb74434c581. That commit is onorigin/main(git merge-base --is-ancestor). Timeline:added_to_merge_queue2026-10-16T20:19Z →merged2026-10-16T20:43Z. ACCEPT6223899114, after four contract records:6219954804FAIL,6221735238FAIL,6222951649FAIL,6223828142PASS.origin/mainsys-approval-request.object.ts:process_label/current_step_label/pending_approver_labelsdeclared asField.textsys-approval-action.object.ts:step_label/acted_as_labeldeclared asField.textdisplay-label-backfill.ts: the request write passeskeptStamp(reread)approvals-plugin.ts: the display fill onkernel:ready:552What an approver or admin sees:
updated_at, so the lists keep their order.plugin-auditis composed, that boot writes one system entry in the audit log and one in the record's activity timeline per row it fills.Follow-ups, filed:
display_title/titleFormat) still readsflow:NAME · RECORD_ID— once #23889 lands, its flow half reads theprocess_labelthat card adds #24007: the request's record title still readsflow:NAME · RECORD_ID. It wasBlocked-bythis card and is now unblocked.updated_aton every update and ignoreDriverOptions.preserveAudit, which the contract says keeps a suppliedupdated_at— historical import and the system backfills move the stamp on those two drivers #24027: driver-memory and driver-mongodb ignorepreserveAudit.#23978 (
approval-service.ts), which waited on this card's file, is free to dispatch.pm:dispatchedis cleared and the assignee is released in this act. The dev session is archived.Generated by Claude Code