feat(spec): ApprovalActionRow declares acted_as, the slot an approval action was taken as - #21479
Conversation
… action was taken as One optional string member on the published action-log row type, beside via_override: the pending-approver slot the action was admitted under, in the slot's stored spelling. It is never a person (that is actor_id, per ADR-0118 D1). Absent means not admitted through a slot, or not recorded. Pins: optional (a type alias that a required member turns red), string-typed, and a row without it still conforms. Changeset: @objectstack/spec minor. Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
… do, and say what absent cannot prove Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 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
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bb18c2fcd85eb3669a458a7cb2d898c80485d813 && git checkout bb18c2fcd85eb3669a458a7cb2d898c80485d813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa4632235ba571ef800b95e6bc18d00a30aa1d57 cb86024a83682b382726b50317655331efef1fc8 && git checkout -B drift-repro aa4632235ba571ef800b95e6bc18d00a30aa1d57 && git merge --no-ff cb86024a83682b382726b50317655331efef1fc8
node scripts/docs-audit/affected-docs.mjs --json aa4632235ba571ef800b95e6bc18d00a30aa1d57 |
Contract reviewServed-tier: Read-only, against these inputs and nothing else: card #21458 (body, the claim ① Derived judgmentsThe member — right. The docblock — right, sentence by sentence, against ruling B and the #21411 plan.
The name — right. The pins — right, and the type-level pins really distinguish optional from required-with-
The No producer, REST or client edit — right. The file list is three files: the contract, its test, one changeset. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
Fixes #21458
Clause-②: yes
What this does
ApprovalActionRowin@objectstack/spec/contractsgains one optional member,acted_as?: string, declared next tovia_override. This is the row type of an approval request's action log:IApprovalService.listActionsreturns it,GET /api/v1/approvals/requests/:id/actionspasses the rows through as they are, and the client SDK types them.The docblock takes
via_override's form:pending_approvers: aposition:NAMEaddress (orrole:NAME, the deprecated pre-rename spelling), an email, or a user id.actor_id, which under ADR-0118 D1 holds asys_userid or nothing. A slot addressed by a user id carries that id inacted_asas the slot's address. That makes no claim about who acted.via_overridemarks), or the row was written before the slot was recorded. So absent alone never proves that no slot was involved.There is no authorable surface, no Zod schema, no request-side change, and no producer. Triage ordered this member to land first (#21411 ruling B,
5954022700; thepm:retriageanswer Q1,5960329631). The approvals card's PR then writes it fromrowFromAction.The member's name
The name is
acted_as. It is read from the #21411 dev report5959282267:plan_on_yes.column: "sys-approval-action.object.ts gets acted_as: Field.text, maxLength 255";open_questions[0]: "add one optional member, acted_as?: string, to ApprovalActionRow".The seat's claim
5958740584names no spelling ("a plugin-object field recording the slot"). Its branchclaude/issue-21411-approval-actor-personholds no commit past53fd35e3e, the empty claim marker. The card's spelling and the approvals side's spelling agree, so there is nothing to reconcile.Files
packages/spec/src/contracts/approval-service.ts: the member and its docblock. The docblock names no tracker number.packages/spec/src/contracts/approval-service.test.ts: the pins..changeset/21458-spec-approval-action-acted-as.md:@objectstack/specminor,Clause-②: yes. Its level and header follow the precedent's spec changeset (.changeset/21333-spec-boolean-comparand-contract.mdin9f13c949b0).Pins (the card's three)
check:test-typecheckcompiles:ActedAsIsOptionalasks whether an empty object type extends thePickofApprovalActionRowonacted_as. AnexpectTypeOfequality againststring | undefinedcannot tell an optional member from a REQUIREDacted_as: string | undefined, and this alias can.ActedAsIsAStringasserts the indexed typeApprovalActionRow['acted_as']equalsstring | undefinedexactly.acted_asis assigned toApprovalActionRow. No Zod schema exists for this row type (it is a TypeScript interface), so "parses" means type-checks. The existing producer (rowFromAction) and the client SDK reference the type only asApprovalActionRow[]. Nokeyof ApprovalActionRoworRequiredof it exists anywhere in the repo.api-surfacerow is present.api-surface/contracts.jsoncarriesApprovalActionRow (interface), andexport-origins/contracts.jsoncarries its origin. Regenerating both after a real build changed no byte, because these artifacts record export names, not members.check:api-surfacereports "public API surface + factory signatures unchanged". This gate cannot see a member being added, so the pins in 1 and 2 are what hold the member.Ablation (one-shot, from the committed state)
Each mutation went through
scripts/ablation-replace.mjs(WRAP mode, trap restore), withpnpm check:test-typecheckas the measurement:acted_as?: string→acted_as: string(required): anchor 1 → 0, blob6ed13aee9a82→04260649fc42. RED: "src/contracts/approval-service.test.ts: 5 type error(s)". It was restored with blob == HEAD6ed13aee9a82andgit diff HEADempty.acted_as?: string→acted_as?: string | null: blob6ed13aee9a82→675062e3d238. RED: 3 type errors. It was restored the same way.The test file imports
./approval-servicefromsrc, so neither ablation goes throughdist/.Reverse check against the rebuilt
.d.ts. This proves that a consumer reads the rebuilt type. A scratch module loaded@objectstack/spec/contractsfrompackages/spec/dist/contracts/index.d.mts(--traceResolution) and ran two legs:acted_as: 'position:finance'is green.acted_as: 42is red withTS2322: Type 'number' is not assignable to type 'string'.A stale
.d.tswould have rejected both legs with an excess-property error instead.Verification at
cb86024a83cb86024a83is this branch withorigin/main49524f6906merged in, which touched other spec contracts and no file of this diff. Every build and test ran underos-verify-lock, and each exit code was written to disk before any pipe.pnpm --filter @objectstack/spec build(with the objectql and lint closures)VERDICT command-exit 0pnpm --filter @objectstack/spec check:generatedAll 15 generated artifacts are up to datepnpm --filter @objectstack/spec check:api-surfacepublic API surface + factory signatures unchangedpnpm --filter @objectstack/spec testTest Files 602 passed (602),Tests 17738 passed, 1 todopnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK, 52 files / 246 errors / 135 pinned signatures heldvitest run src/contracts/approval-service.test.ts(at828d3f2db3; the full suite above includes it)node scripts/check-changeset-no-major.mjs --base origin/mainThis diff introduces no major bump--eventcarrying this PR bodyLEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patchnode scripts/check-adr-0087-registration.mjs --base origin/mainthis PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)node scripts/check-empty-changeset.mjs --base origin/mainNo empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)pnpm check:doc-authoring17285 customer-facing string(s) across 1236 spec sources cleanpnpm check:nul-bytesscanned 9810 text file(s) ... no raw ASCII control bytesnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE85 derived famil(ies) accounted for — 85 run, 0 NOT-MEASUREDThe derivation (
--commands, no paths) gave the same 85 commands at828d3f2db3,d576d5b4c7andcb86024a83, and all 85 exited 0 atcb86024a83. In the first pass (d576d5b4c7), two gates exited 3 (PREREQUISITE NOT MET):check:lean-entry-closurepassed after an objectql-closure build.check:dual-build-cjs-loadspassed in the final pass.The dists that the final pass of
check:dual-build-cjs-loadsread were partly built by an earlier gate from the tree before the merge. This diff emits no JavaScript (an interface member), and CI builds fresh.origin/mainhas since moved one more commit (aa4632235b, a page-block spec change). It touches no file of this diff and was not merged in. CI runs on the merge ref.Acceptance notes
acted_as, and every row omits it. That is the member's declared absent case. On slot-gated rows,actor_idstill holds the slot literal. That is the ADR-0118 D1 violation the approvals card exists to remove, and it is not touched here (the card forbids a producer change). The docblock describes the ruled contract. The changeset says that the approvals service's own changeset states whenlistActionsstarts returning the member.listActionsrows through as{ data: rows }, andpackages/clientre-exports the spec type. No gate asked for an edit.actor_idhas no docblock. The ADR-0118 D1 description the approvals card plans is for the object field (sys-approval-action.object.ts), not for this row type. Noted, not filed. Carrier: none.Generated by Claude Code