Repository navigation
Commit 72217cd
Fixes #21458
Clause-②: yes
## What this does
`ApprovalActionRow` in `@objectstack/spec/contracts` gains one optional
member, `acted_as?: string`, declared next to `via_override`. This is
the row type of an approval request's action log:
`IApprovalService.listActions` returns it, `GET
/api/v1/approvals/requests/:id/actions` passes the rows through as they
are, and the client SDK types them.
The docblock takes `via_override`'s form:
- **What it records.** The pending-approver slot the action was admitted
under, in the slot's stored spelling as it stood in the request's
`pending_approvers`: a `position:NAME` address (or `role:NAME`, the
deprecated pre-rename spelling), an email, or a user id.
- **That it is never a person.** The person who acted is `actor_id`,
which under ADR-0118 D1 holds a `sys_user` id or nothing. A slot
addressed by a user id carries that id in `acted_as` as the slot's
address. That makes no claim about who acted.
- **When it is absent.** Either the action was not admitted through a
slot (a submitter's own action, a system action, or an admin override,
which `via_override` marks), 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`; the `pm:retriage` answer Q1, `5960329631`). The
approvals card's PR then writes it from `rowFromAction`.
## The member's name
The name is `acted_as`. It is read from the #21411 dev report
`5959282267`:
- `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 `5958740584` names no spelling ("a plugin-object field
recording the slot"). Its branch
`claude/issue-21411-approval-actor-person` holds no commit past
`53fd35e3e`, 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/spec` `minor`, `Clause-②: yes`. Its level and header
follow the precedent's spec changeset
(`.changeset/21333-spec-boolean-comparand-contract.md` in `9f13c949b0`).
## Pins (the card's three)
1. **The member is optional and string-typed.** Two exported type
aliases, which `check:test-typecheck` compiles:
- `ActedAsIsOptional` asks whether an empty object type extends the
`Pick` of `ApprovalActionRow` on `acted_as`. An `expectTypeOf` equality
against `string | undefined` cannot tell an optional member from a
REQUIRED `acted_as: string | undefined`, and this alias can.
- `ActedAsIsAString` asserts the indexed type
`ApprovalActionRow['acted_as']` equals `string | undefined` exactly.
2. **A row without it still type-checks.** A row literal with no
`acted_as` is assigned to `ApprovalActionRow`. 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 as `ApprovalActionRow[]`. No `keyof
ApprovalActionRow` or `Required` of it exists anywhere in the repo.
3. **The `api-surface` row is present.** `api-surface/contracts.json`
carries `ApprovalActionRow (interface)`, and
`export-origins/contracts.json` carries its origin. Regenerating both
after a real build changed **no** byte, because these artifacts record
export names, not members. `check:api-surface` reports "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), with `pnpm check:test-typecheck` as the measurement:
- `acted_as?: string` → `acted_as: string` (required): anchor 1 → 0,
blob `6ed13aee9a82` → `04260649fc42`. **RED**:
"src/contracts/approval-service.test.ts: 5 type error(s)". It was
restored with blob == HEAD `6ed13aee9a82` and `git diff HEAD` empty.
- `acted_as?: string` → `acted_as?: string | null`: blob `6ed13aee9a82`
→ `675062e3d238`. **RED**: 3 type errors. It was restored the same way.
The test file imports `./approval-service` from `src`, so neither
ablation goes through `dist/`.
**Reverse check against the rebuilt `.d.ts`.** This proves that a
consumer reads the rebuilt type. A scratch module loaded
`@objectstack/spec/contracts` from
`packages/spec/dist/contracts/index.d.mts` (`--traceResolution`) and ran
two legs:
- `acted_as: 'position:finance'` is green.
- `acted_as: 42` is red with `TS2322: Type 'number' is not assignable to
type 'string'`.
A stale `.d.ts` would have rejected both legs with an excess-property
error instead.
## Verification at `cb86024a83`
`cb86024a83` is this branch with `origin/main` `49524f6906` merged in,
which touched other spec contracts and no file of this diff. Every build
and test ran under `os-verify-lock`, and each exit code was written to
disk before any pipe.
| command | exit | verdict line |
|---|---|---|
| `pnpm --filter @objectstack/spec build` (with the objectql and lint
closures) | 0 | `VERDICT command-exit 0` |
| `pnpm --filter @objectstack/spec check:generated` | 0 | `All 15
generated artifacts are up to date` |
| `pnpm --filter @objectstack/spec check:api-surface` | 0 | `public API
surface + factory signatures unchanged` |
| `pnpm --filter @objectstack/spec test` | 0 | `Test Files 602 passed
(602)`, `Tests 17738 passed, 1 todo` |
| `pnpm --filter @objectstack/spec typecheck` | 0 |
`check:test-typecheck: OK`, 52 files / 246 errors / 135 pinned
signatures held |
| `vitest run src/contracts/approval-service.test.ts` (at `828d3f2db3`;
the full suite above includes it) | 0 | 7 passed (6 before this diff) |
| `node scripts/check-changeset-no-major.mjs --base origin/main` | 0 |
`This diff introduces no major bump` |
| the same, with `--event` carrying this PR body | 0 | `LEVEL AXIS: this
PR declares clause-② yes, and no package whose packages/**/src/** it
moves is graded patch` |
| `node scripts/check-adr-0087-registration.mjs --base origin/main` | 0
| `this PR adds no declared-breaking changeset (1 non-breaking
changeset(s) seen)` |
| `node scripts/check-empty-changeset.mjs --base origin/main` | 0 | `No
empty-frontmatter changeset introduced by this diff (1 declaring
changeset(s) added)` |
| `pnpm check:doc-authoring` | 0 | `17285 customer-facing string(s)
across 1236 spec sources clean` |
| `pnpm check:nul-bytes` | 0 | `scanned 9810 text file(s) ... no raw
ASCII control bytes` |
| `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--ran FILE` | 0 | `85 derived famil(ies) accounted for — 85 run, 0
NOT-MEASURED` |
The derivation (`--commands`, no paths) gave the same 85 commands at
`828d3f2db3`, `d576d5b4c7` and `cb86024a83`, and all 85 exited 0 at
`cb86024a83`. In the first pass (`d576d5b4c7`), two gates exited 3
(PREREQUISITE NOT MET):
- `check:lean-entry-closure` passed after an objectql-closure build.
- `check:dual-build-cjs-loads` passed in the final pass.
The dists that the final pass of `check:dual-build-cjs-loads` read 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/main` has 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
- **The window before the producer lands.** Until the approvals card's
PR merges, no producer writes `acted_as`, and every row omits it. That
is the member's declared absent case. On slot-gated rows, `actor_id`
still 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
when `listActions` starts returning the member.
- **No REST or client edit.** REST passes `listActions` rows through as
`{ data: rows }`, and `packages/client` re-exports the spec type. No
gate asked for an edit.
- **The DTO's `actor_id` has 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.
- A contract-tier review is owed before the queue, as the claim says (a
published response contract widens).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01YDt3PzwfrkuFzUBF89WPmM)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7097859 commit 72217cd
3 files changed
Lines changed: 85 additions & 2 deletions
File tree
- .changeset
- packages/spec/src/contracts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
63 | 63 | | |
64 | 64 | | |
65 | 65 | | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
66 | 113 | | |
67 | 114 | | |
68 | 115 | | |
| |||
82 | 129 | | |
83 | 130 | | |
84 | 131 | | |
85 | | - | |
86 | | - | |
87 | 132 | | |
88 | 133 | | |
89 | 134 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
513 | 513 | | |
514 | 514 | | |
515 | 515 | | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
516 | 539 | | |
517 | 540 | | |
518 | 541 | | |
| |||
0 commit comments