Skip to content

spec(contracts): ApprovalActionRow declares the slot an approval action was taken as (an optional member), so the action log can show it once actor_id holds the person (#21411 ruling B) #21458

Closed

Description

@objectstack-fleet

Split from #21411 by the triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U, answering that card's pm:retriage Q1. ⛔ Not a claim.

Grade: enhancement · priority:p1 · domain:spec · area:workflow · pm:queue. It is p1 because it gates #21411 (p1), whose PR consumes it and lands after it.

Why a card of its own

#21411's ruling B puts the deciding person in sys_approval_action.actor_id, and the slot an action was taken as in its own field. The ruled bullet "listActions shows the slot separately" needs the slot on the wire:

  • listActions returns ApprovalActionRow (packages/spec/src/contracts/approval-service.ts);
  • REST passes the rows through verbatim;
  • the client SDK types them.

A new member on a published response contract widens the public surface. That is 强制条款², and so spec-lane work (references/execution-duties.md; the precedent is #21333).

The change

Pins

  • The member is optional and typed as a string.
  • A row without it still parses (all existing producers).
  • The api-surface row is present.

After this

#21411's PR writes the member from rowFromAction, so no undeclared wire member is ever needed.

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01YDt3PzwfrkuFzUBF89WPmM
    Account: os-tesla (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21458-approval-action-acted-as
    Worktree: objectstack-issue-21458
    Domain: domain:spec
    Seat: domain:spec#2
    File surface (at cfa4d740b7): packages/spec/src/contracts/approval-service.ts, where ApprovalActionRow gains one OPTIONAL string member, the slot address an approval action was taken as. Its name follows #21411's claim (acted_as), and its docblock takes via_override's form: what it records, when it is absent, and that it is never a person.

    Container & model: S, mode:subagent, model: opus (dispatch-gates --tier at cfa4d740b7: no path-derived mandate; floor sonnet · default opus · ceiling CONTRACT_REVIEW_TIER). A published response contract widens, so the at-tier contract review runs as a separate isolated subagent after the PR opens.
    Clause-②: yes
    Thread-read: none
    Serial constraints cleared: at 2026-10-02T20:34Z, no open PR touches contracts/approval-service.ts or the regenerated contract rows. #21411 (domain:services, os-bill) consumes this member, and its PR lands after this one, as the card orders.

    Seat terms: p1, ordered ahead of this seat's p3 work under the maintainer's terms (「只处理 p0 p1 p2」, widened by 5952442849), batch 3.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "pr": "#21479",
    "issue": 21458,
    "status": "done",
    "branch": "claude/issue-21458-approval-action-acted-as",
    "session": "session_01YDt3PzwfrkuFzUBF89WPmM (subagent: the parent's id; the relay read the same id from CLAUDE_CODE_REMOTE_SESSION_ID)",
    "premise_still_valid": true,
    "summary": "ApprovalActionRow (packages/spec/src/contracts/approval-service.ts) gains one optional string member, acted_as, declared next to via_override. Its docblock says what it records: the pending-approver slot the action was admitted under, in the slot's stored spelling (position:NAME, the deprecated role:NAME, an email or a user id). It says the member is never a person, because the person is actor_id (ADR-0118 D1). It says when the member is absent: the action was not admitted through a slot (a submitter action, a system action or an admin override), or the row predates the slot. So absent alone never proves that no slot was involved. The docblock names no tracker number. There is no authorable surface, no Zod schema, no request-side change, no producer, and no REST or client edit. The draft PR #21479 opens with 'Fixes #21458' and then 'Clause-②: yes'. It carries one @objectstack/spec minor changeset, modelled on the precedent's spec changeset. Premise checked at cfa4d74: there were zero acted_as hits under packages/ (the control via_override had hits in 16 files). The pinned branch of the approvals card had no member.",
    "a1_name": "acted_as. Read from the #21411 dev report 5959282267: plan_on_yes.column ('sys-approval-action.object.ts gets acted_as: Field.text, maxLength 255') and open_questions[0] ('add one optional member, acted_as?: string, to ApprovalActionRow'). The seat claim 5958740584 names no spelling ('a plugin-object field recording the slot'). The branch claude/issue-21411-approval-actor-person holds no commit past 53fd35e, its empty claim marker (git ls-remote). The card's spelling and the approvals side's spelling agree, so there is no difference to report.",
    "pins": [
    "Optional: the exported type alias ActedAsIsOptional asserts that an empty object type extends the Pick of ApprovalActionRow on acted_as. An expectTypeOf equality cannot tell an optional member from a required 'acted_as: string | undefined', and this alias can. check:test-typecheck compiles the file (it is not in test-typecheck-debt.json).",
    "String-typed: the exported alias ActedAsIsAString asserts Eq(ApprovalActionRow['acted_as'], string | undefined). The test also has an expectTypeOf equality.",
    "A row without the member still conforms: a row literal with no acted_as is assigned to ApprovalActionRow, and reading the member answers undefined. No Zod schema exists for this row type (it is a TS interface), so 'parses' is read as 'type-checks'. The existing producer (rowFromAction) and the client SDK name the type only as ApprovalActionRow[]. No 'keyof' or 'Required' of the type exists in the repo.",
    "The api-surface row is present: api-surface/contracts.json line 34 'ApprovalActionRow (interface)'; export-origins/contracts.json line 34 names its origin. Regenerating both (gen:api-surface, gen:export-origins) after a real build changed no byte, because these artifacts record export names, not members. check:api-surface reports 'unchanged'. This gate cannot see a member being added, which is why pins 1 to 3 carry the member."
    ],
    "semver": {
    "level": "@objectstack/spec minor, 'Clause-②: yes', header 'feat(spec): ...'. This is the shape of the precedent's spec changeset .changeset/21333-spec-boolean-comparand-contract.md (commit 9f13c94: '@objectstack/spec: minor', 'Clause-②: yes').",
    "gate_lines": [
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: 'This diff introduces no major bump.' Run locally, the level axis printed NOT APPLICABLE (no pull_request payload).",
    "node scripts/check-changeset-no-major.mjs --base origin/main --event (a pull_request payload carrying this PR body) :: exit 0 :: 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch.' · declaration line 'Clause-②: yes' · direction arm: none declared.",
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: 'this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen).'",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).' and 'No changeset from the merge base modified or deleted by this diff.'"
    ],
    "refusals": "none"
    },
    "gates": [
    {
    "command": "pnpm --filter @objectstack/spec build (plus the objectql and lint closures), under os-verify-lock, at cb86024",
    "exit": 0,
    "verdict": "VERDICT command-exit 0"
    },
    {
    "command": "pnpm --filter @objectstack/spec check:generated at cb86024",
    "exit": 0,
    "verdict": "All 15 generated artifacts are up to date"
    },
    {
    "command": "pnpm --filter @objectstack/spec run check:api-surface at cb86024",
    "exit": 0,
    "verdict": "@objectstack/spec public API surface + factory signatures unchanged"
    },
    {
    "command": "pnpm --filter @objectstack/spec test at cb86024",
    "exit": 0,
    "verdict": "Test Files 602 passed (602); Tests 17738 passed, 1 todo (17739)"
    },
    {
    "command": "pnpm --filter @objectstack/spec typecheck at cb86024",
    "exit": 0,
    "verdict": "check:test-typecheck: OK, 52 file(s) / 246 error(s) / 135 pinned signature(s) held"
    },
    {
    "command": "pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2 src/contracts/approval-service.test.ts at 828d3f2",
    "exit": 0,
    "verdict": "Tests 7 passed (7)"
    },
    {
    "command": "pnpm check:doc-authoring",
    "exit": 0,
    "verdict": "17285 customer-facing string(s) across 1236 spec sources clean, no internal issue-id references"
    },
    {
    "command": "pnpm check:nul-bytes",
    "exit": 0,
    "verdict": "scanned 9810 text file(s), no raw ASCII control bytes"
    },
    {
    "command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE, at cb86024",
    "exit": 0,
    "verdict": "85 derived famil(ies) accounted for, 85 run, 0 NOT-MEASURED (a DERIVED zero: all 85 recorded an exit code and none of them is 3)"
    }
    ],
    "gates_note": "The derivation (--commands, no paths) gave the same 85 commands at 828d3f2, d576d5b and cb86024. All 85 ran at cb86024, each exit written to disk before any pipe, and all 85 exited 0. In the first pass at d576d5b, two exited 3 (PREREQUISITE NOT MET). check:lean-entry-closure passed once the objectql closure was built. check:dual-build-cjs-loads passed in the final pass, but some of the dists it read (for example packages/client/dist) had been built by an earlier gate from the tree before the merge. This diff emits no JavaScript (an interface member), and CI builds fresh. origin/main has since moved one more commit, aa46322 (a page-block spec change). It touches no file of this diff and was not merged in. dispatch-gates reported that none of the commits it can see touched what it derives from.",
    "tests": "Spec package at cb86024: test 602 files, 17738 passed, 1 todo, exit 0; typecheck exit 0; check:generated exit 0. Ablations used scripts/ablation-replace.mjs in WRAP mode (anchor must hit, blob-change proof, trap restore), at d576d5b with the fix committed, measured by pnpm check:test-typecheck. (a) acted_as?: string changed to acted_as: string: anchor 1 to 0, blob 6ed13aee9a82 to 04260649fc42, RED 'src/contracts/approval-service.test.ts: 5 type error(s)'; restored to blob == HEAD 6ed13aee9a82 with git diff HEAD empty. (b) changed to acted_as?: string | null: blob to 675062e3d238, RED with 3 type errors; restored the same way. The test imports ./approval-service from src, so dist is not on the ablation path. Reverse check that the rebuilt .d.ts is what a consumer reads: a scratch module loaded @objectstack/spec/contracts from packages/spec/dist/contracts/index.d.mts (--traceResolution). The leg acted_as: 'position:finance' exited 0. The leg acted_as: 42 exited 2 with 'TS2322: Type number is not assignable to type string'. A stale .d.ts would have given an excess-property error on both legs.",
    "line_budget": "87 changed lines (+85 / -2) across 3 files against the human-merge threshold of 5000: under. No skills/** file and no governed surface (.claude/, docs/adr/, AGENTS.md and the rest) is touched, so no skill line ratchet applies, and the PR is not governed.",
    "files_changed": [
    "packages/spec/src/contracts/approval-service.ts (+23 -0: the member and its docblock)",
    "packages/spec/src/contracts/approval-service.test.ts (+47 -2: the pins; the shared Eq/Assert helpers moved up so both blocks use them)",
    ".changeset/21458-spec-approval-action-acted-as.md (+15: @objectstack/spec minor, Clause-②: yes)"
    ],
    "deviations": [
    "Merged origin/main 49524f6 into the branch (merge commit cb86024) before opening the PR, per AGENTS.md section 10, because the incoming commits touched packages/spec (other contract files). The derived gate union, the spec build, check:generated, test and typecheck were all re-run at cb86024.",
    "I stopped the first full gate pass at d576d5b partway (my own runner, process group 15130), because I tightened the docblock and changeset wording after it had started. Its results are superseded by the pass at cb86024.",
    "The dispatch's A3 says 'the regenerated artifact carries it'. api-surface records names, not members, so regeneration was a zero-byte diff. The existing 'ApprovalActionRow (interface)' row is the reading; the member itself is held by the type pins.",
    "The card's 'a row without it still parses' has no parser to run, because there is no Zod schema for this interface. It is pinned as type-checks.",
    "Commit trailers use AGENTS.md's model-free pair (Claude-Session plus 'Co-authored-by: Claude'), not the model-named Co-Authored-By from the harness reminder. The PR footer uses AGENTS.md's session-URL form. AGENTS.md takes precedence, and the pre-push gate passed."
    ],
    "mcp_calls": "0",
    "api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create, which is POST /repos/objectstack-ai/objectstack/pulls (draft, base main), giving #21479 (read-back: 8917 bytes sent, 8917 stored, identical); (2) label-write --issue 21479 --assign os-tesla, which is POST /repos//issues/21479/assignees (read-back matches; size/s was set by another actor and left alone); (3) this os-dev-report comment, which is POST /repos//issues/21458/comments through post-stamped.mjs. git push (4 pushes of this branch) is not a REST write. No label was added: the dispatch named none, and skip-changeset does not apply because the diff publishes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "carrier: none · noted, not filed: the DTO's actor_id member on ApprovalActionRow has no docblock. The ADR-0118 D1 description that the approvals card plans is for the object field (sys-approval-action.object.ts), not for this row type. It is recorded in the PR's Acceptance notes."
    ],
    "conflicts_noted": [
    "The window before the producer lands: until the approvals card's PR merges, no producer writes acted_as (every row omits it, which is the declared absent case), and actor_id still holds the slot literal on slot-gated rows. Triage ruled this order (land first, then consume). The changeset says that the approvals service's own changeset states when listActions starts returning the member."
    ]
    }


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21479 @ cb86024a

    domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5961025256 · 2026-10-02T22:44Z

    Out-of-scope finding: the DTO's actor_id member has no docblock. → noted, not filed: it rides along naturally with #21411's PR, which documents the object field under ADR-0118 D1.

    Next: ready, auto-merge, the queue.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21479 → 72217cd6ff

    domain:spec seat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim 5961025256 · 2026-10-02T23:15Z


    Generated by Claude Code

  5. added 3 commits that reference this issue on Oct 7, 2026
    72217cd
    6f17d1d
    8256a4b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itdomain:specenhancementNew feature or requestpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions