Skip to content

feat(spec): ApprovalRequestRow declares current_step_label, the sys_approval_request column's own name for the step label - #24329

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24309-approval-current-step-label
Oct 18, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24309-approval-current-step-label

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24309

Clause-②: yes

What this changes

ApprovalRequestRow (packages/spec/src/contracts/approval-service.ts) now declares an optional current_step_label?: string in its display-enrichment block, beside step_label. Its docblock says what it is: the declared sys_approval_request.current_step_label column under its own name, served so that a page bound to sys_approval_request can list the step by its label. Such a page names the object's declared fields, and step_label is not one of them, so today the request page's strip can list only current_step, the node's machine id.

step_label stays, so no reader moves.

This is the spec half of the split that triage took on the consumer card (option A). The services half serves the member from plugin-approvals' rowFromRequest and lists it on the strip. That half is the consumer card's own work: #24281 remains open, and this PR does not touch packages/plugins/plugin-approvals/** or the request page.

Files

  • packages/spec/src/contracts/approval-service.ts: the member and its docblock.
  • packages/spec/src/contracts/approval-service.test.ts: four pins beside the existing acted_as and organization_id pins:
    • a {} extends Pick alias that turns red on a required member;
    • an exact string | undefined alias;
    • step_label at the same shape;
    • a runtime read-back on a row without the member and on a row with it.
  • .changeset/24309-approval-current-step-label.md: minor on @objectstack/spec, Clause-②: yes (widening).

No generated artefact moved. After a rebuild, check:generated reports all 14 artefacts up to date. api-surface/ records that the interface exists, not its members.

Premise, re-read on origin/main 52ff308b8

  • ApprovalRequestRow is at approval-service.ts:138, and step_label?: string is at :223, with no current_step_label.
  • sys_approval_request declares the column current_step_label (sys-approval-request.object.ts:330, a readonly Field.text).
  • plugin-approvals' rowFromRequest serves the label only as step_label (approval-service.ts:1183).

Who reads the type

  • This repo (git grep ApprovalRequestRow):

    • @objectstack/spec: the contract and its test;
    • @objectstack/client;
    • @objectstack/plugin-approvals: the producer, action-link-pages.ts, and seven test files.

    No keyof ApprovalRequestRow map or exhaustive Record over its keys exists, so an optional member breaks no reader.

  • objectui, read at both its pin before the merge (5e15928b4631) and its pin after it (0707b4ae3712):

    • packages/app-shell/src/views/approval-decision/ApprovalDecisionPanel.tsx imports the spec type, and only through a Pick of payload and payload_display;
    • the console inbox (apps/console/src/services/approvalsApi.ts) declares its own local ApprovalRequestRow and reads step_label from it.

    Neither is affected.

Verification

Final head 11acbc435 (the claim's commit ef7bb1a93, plus a merge of origin/main 2ce2e3699 made with os-regen-merge.sh). Every build and test ran through os-verify-lock.sh, except the one-file probe tsc below.

  • pnpm --filter @objectstack/spec build: VERDICT command-exit 0. The member reaches only dist/contracts/index.d.ts and index.d.mts; no emitted JS carries it.
  • pnpm --filter @objectstack/spec check:generated: exit 0, "All 14 generated artifacts are up to date". Nothing was regenerated.
  • pnpm --filter @objectstack/spec typecheck: VERDICT command-exit 0. This includes check:test-typecheck, which compiles the new pin file (counted with --listFiles): "52 file(s) / 242 error(s) / 132 pinned signature(s) held", unchanged from the ledger.
  • pnpm --filter @objectstack/spec test: VERDICT command-exit 0. "Test Files 694 passed (694)", "Tests 21042 passed, 1 todo". The pin file alone: 9 of 9 passed (8 before this PR).
  • Consumers. First turbo run build --filter='@objectstack/plugin-approvals^...' --filter='@objectstack/client^...' --concurrency=2: "34 successful, 34 total". Then pnpm --filter @objectstack/plugin-approvals --filter @objectstack/client typecheck: VERDICT command-exit 0, tsc --noEmit plus check:test-typecheck OK for both. The build filter is the two consumers' dependency closure, @objectstack/spec included.
  • Reverse verification, at ef7bb1a93: a probe file in plugin-approvals typed two rows as ApprovalRequestRow. With current_step_label: 'Manager Review' the row compiled clean. With current_step_label: 42, tsc printed exactly one error, TS2322 "Type 'number' is not assignable to type 'string'". A stale .d.ts would have answered TS2353 on both rows, so the consumer reads the rebuilt declaration. The probe was deleted, and git status is clean.
  • Ablation, at 11acbc435, through scripts/ablation-replace.mjs: the anchor went from 1 hit to 0 and the blob from e1e2e927 to 9515560b. The member deleted, check:test-typecheck went red: "src/contracts/approval-service.test.ts: 7 type error(s)". Restored: the blob equals the HEAD blob e1e2e927, and git diff HEAD is empty. The pin is a compile-time pin: its teeth are check:test-typecheck, inside the spec typecheck that the required TypeScript Type Check job runs.
  • Gates: dispatch-gates --commands (no paths) derived 86 families at 11acbc435. All 86 ran with exit 0. dispatch-gates --ran: "86 derived, 86 run, 0 NOT-MEASURED, 0 UNRUN".

Acceptance notes


Generated by Claude Code

…p_label

The declared sys_approval_request column's own name for the step's label,
so a page bound to sys_approval_request can list the step by its label
instead of by current_step, the node's machine id. step_label stays, so no
reader moves. Optional member of a TypeScript interface: no runtime accept
set changes.

Claude-Session: https://claude.ai/code/session_019oFY6Hi35J7atvgVFnmoaK
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Oct 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1ef0ee89d490bd243a78d370a8b464dd00778844 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 3d7cd6fa1b24b99ffa7a35bb0bb6bf470d443d11 — the merge of head 11acbc4350ea7f70eb912d7b02d6716e3f42f9ad into base 1ef0ee89d490bd243a78d370a8b464dd00778844, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3d7cd6fa1b24b99ffa7a35bb0bb6bf470d443d11 && git checkout 3d7cd6fa1b24b99ffa7a35bb0bb6bf470d443d11
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1ef0ee89d490bd243a78d370a8b464dd00778844 11acbc4350ea7f70eb912d7b02d6716e3f42f9ad && git checkout -B drift-repro 1ef0ee89d490bd243a78d370a8b464dd00778844 && git merge --no-ff 11acbc4350ea7f70eb912d7b02d6716e3f42f9ad

node scripts/docs-audit/affected-docs.mjs --json 1ef0ee89d490bd243a78d370a8b464dd00778844

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 (seat check) · PR #24329 · domain:spec seat 2 · zhuangjianguo · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T12:52Z

ACCEPT at head 11acbc4350ea7f70eb912d7b02d6716e3f42f9ad (claim 6249288585, report 6250113147 on #24309). A contract review at CONTRACT_REVIEW_TIER is owed by face (a member of a published contract in non-test packages/spec/src/**, Clause-②: yes). It runs on this head, isolated from the seat; needs:contract-review is on the PR until its record is adopted. The PR lands after a PASS there and a fully green head.

item reading
Shape Draft, base main, assignee zhuangjianguo. The body's line 1 is Fixes #24309, line 3 is Clause-②: yes; no other closing keyword, and #24281 stays open. 3 files, +78/-0: the contract, its pin test, and the changeset (minor on @objectstack/spec, Clause-②: yes (widening)). Two commits over merge base 2ce2e36998: ef7bb1a93 and a merge of main.
The member, against the card's "Done when" ApprovalRequestRow.current_step_label?: string sits beside step_label in the display-enrichment block (approval-service.ts:224–:237). step_label is unchanged. The seat re-read the cited code on origin/main: the column is a readonly Field.text at sys-approval-request.object.ts:330; the engine writes it at request open (approval-service.ts:3568, labelOrMachineName(nodeLabel, nodeId)); rowFromRequest serves it only as step_label (:1183).
Pins Three compile-time aliases (optional, exactly string | undefined, the same shape as step_label) and one runtime read-back. The dev's ablation (the member deleted) turned check:test-typecheck red with 7 errors, and the restore was proven by blob hash.
Accepted deviation Declared ahead of its producer. Until #24281 lands, no door serves the member. This is triage's split (option A on #24281, which is Blocked-by: #24309), and the docblock says so. It is not a key without a planned writer: the writer is a named, queued card.
Local runs (dev) spec build, check:generated (14/14 up to date), typecheck, and 694 test files passed. The consumers plugin-approvals and client were built and typechecked green. Gates: 86 derived, 86 run with exit 0, reconciled with --ran.
Out of scope None reported. objectui typecheck is NOT MEASURED locally (it resolves the published spec): Acceptance notes.
Landing After the contract review PASS and a fully green head: pr_ready then automerge_enable. The merge closes #24309 and unblocks #24281 (domain:services).

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 11acbc4350ea7f70eb912d7b02d6716e3f42f9ad
Local-runs: none
Scope: packages/spec/src/contracts/approval-service.ts,packages/spec/src/contracts/approval-service.test.ts,.changeset/24309-approval-current-step-label.md + packages/plugins/plugin-approvals/src/sys-approval-request.object.ts, packages/plugins/plugin-approvals/src/approval-service.ts, packages/plugins/plugin-approvals/src/display-labels.ts, packages/plugins/plugin-approvals/src/sys-approval-request.page.ts, packages/plugins/plugin-approvals/src/sys-approval-request-page.test.ts, packages/plugins/plugin-approvals/src/action-link-pages.ts, packages/rest/src/approvals-routes.ts, packages/client/src/index.ts, docs/adr/0049-no-unenforced-security-properties.md

Inputs: card #24309 (body and every comment), PR #24329 (body, file list, the net diff of 3 files, +78/-0, against the merge base 2ce2e36), and the check-runs on the head. Every cited line below was read at that merge base with git show. The seat's ACCEPT comment on the PR was not weighed. This record is adversarial to the dispatch by design.

① Derived judgments

  1. Accept set: none moves — right. The member lands on a TypeScript interface (ApprovalRequestRow, @objectstack/spec/contracts), not on a Zod schema. No author writes it, no runtime parses it, no authorable key joins the liveness ledger (check-run Spec property liveness: success). The changeset's sentence "no Zod schema and no runtime accept set changes" is true.
  2. Public surface: one optional member on a published interface — right, and no artefact moves. The api-surface/ snapshot records that an export exists, not its members (AGENTS.md, Touched packages/spec), so the PR's "0 of 14 stale" is what the gate design predicts, not a reading to doubt. @objectstack/client (src/index.ts:149 and :8359) and @objectstack/plugin-approvals (src/index.ts:51) re-export the type by reference, so their declaration surfaces widen through the dependency with no code change in either: the shape the acted_as precedent (commit 72217cd, PR feat(spec): ApprovalActionRow declares acted_as, the slot an approval action was taken as #21479) landed with a spec-only changeset.
  3. Readers: an optional member breaks none — right. A git grep at the merge base finds no keyof ApprovalRequestRow map and no exhaustive Record over its keys under packages/**. The in-repo readers are the client SDK (approvals.getRequest, listRequests and the decision verbs), action-link-pages.ts:63 to :70 (req.step_label || req.current_step), and the producer itself, whose rowFromRequest returns as any (plugin-approvals approval-service.ts:1208) and so was never bound by the interface. This record did not read objectui; the brief's inputs end at this repo, and an optional member cannot break a Pick of other members or a locally declared row type, which is what the PR body reports there.
  4. The docblock's claims, each read in the cited code — right, with one precision note.
    • "the declared sys_approval_request.current_step_label column": sys-approval-request.object.ts:330 to :338, a readonly Field.text, maxLength 255, required false. Right.
    • "the Approval node's authored label as it stood when the request reached the step (the value step_label carries)": the writer at approval-service.ts:3568 stores labelOrMachineName(input.nodeLabel, input.nodeId) ?? null, which is the authored label ELSE the humanized machine name (display-labels.ts:48); rowFromRequest at :1183 serves step_label from requestDisplayLabels(row).current_step_label (display-labels.ts:64 to :73: the column when non-blank, else the snapshot's __nodeLabel, else humanized current_step). The first half inherits the object file's own shorthand (object.ts:327 to :329 says "authored label" too); the parenthetical makes the claim exact: the member carries the chain's value. Right.
    • "a page bound to sys_approval_request names the object's declared fields, and step_label is not one": sys-approval-request-page.test.ts:178 to :188 pins every strip field to Object.keys(SysApprovalRequest.fields); current_step_label is a declared field and step_label is not; the strip lists current_step (page.ts:83). Right.
    • The approvals get door: packages/rest/src/approvals-routes.ts:308 to :330 answers GET /api/v1/approvals/requests/:id with svc.getRequest (approval-service.ts:7922), which is loadRequest, which is rowFromRequest at :7949, serialised as is. So today the door serves step_label and no current_step_label, exactly as the docblock says. Right.
  5. The type, string | undefined, against what the producer can serve — right. The column is nullable in storage (?? null at :3568; required false), but the member's stated value is the one step_label carries, and that chain's return type is string | undefined (display-labels.ts:64 to :67), never null; rowFromRequest already coerces every nullable column it serves with ?? undefined. A producer that served the raw column would owe the same coercion every sibling member gets. Not nullable is the right shape for the named value. The StepLabelStays alias additionally holds the two members to one shape, so they cannot drift apart silently: intended, and consistent with "the value step_label carries".
  6. The pins — right, and their teeth are real. {} extends Pick for optionality (the reasoning the acted_as block states at test.ts:75 to :78: expectTypeOf cannot tell an optional member from a required string | undefined), Eq for the exact shape, and a runtime literal with and without the member. The file is in the tsconfig.test.json program (the existing acted_as aliases are compiled by check:test-typecheck inside the spec typecheck), so a required or a nullable member reds the required TypeScript Type Check job. The ablation the dev reports (7 type errors with the member deleted) is the expected direction for that program.
  7. Declared ahead of its producer — acceptable here, and right; the brief's question answered.

② Semver level

  • .changeset/24309-approval-current-step-label.md: "@objectstack/spec": minor, body line Clause-②: yes (widening). One optional member added to a published interface, nothing removed, renamed or narrowed: a widening, and a widening takes at least minor (AGENTS.md Post-Task Checklist 3). Right. Not breaking, so no ADR-0087 disposition marker is owed and none is written. Check Changeset: success on both triggers.
  • The PR body's line 3 is Clause-②: yes; the changeset's arm is (widening). Per clause2-line.mjs the arm is optional and yes (widening) is the same reading as yes, so body and changeset agree, and the arm names the right direction.
  • No changeset on @objectstack/client or @objectstack/plugin-approvals: right, no code in either moves; the re-exports widen through the dependency, as the acted_as changeset did.
  • The changeset text makes no claim that the member is served today ("serving the member there is that package's own change"): the CHANGELOG sentence an upgrading reader greps will be true at publish time whether or not plugin-approvals: the approval request page's highlights strip shows the current step's machine id (current_step, such as approve_step) where the approvals get serves the step's label only as step_label #24281 has landed.

③ Boundary flags

Implemented-by: claude/issue-24309-approval-current-step-label
Reviewed-by: session_019oFY6Hi35J7atvgVFnmoaK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review adopted · domain:spec seat 2 · session session_019oFY6Hi35J7atvgVFnmoaK · 2026-10-18T13:03Z


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 18, 2026 13:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 18, 2026 13:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 18, 2026
Merged via the queue into main with commit 370833a Oct 18, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24309-approval-current-step-label branch October 18, 2026 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

2 participants