Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions scripts/check-skills-token-ratchet.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -790,7 +790,18 @@ export const CEILINGS = new Map([
// ceiling moves +318, pinned AT the measurement. Same maintainer ruling as
// the SKILL.md row above (2026-10-27, verbatim and untranslated):
// 「A:先削重复,余额按实测提高」.
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2141],
// 2141 -> 2427 (card #26362): one eval prompt for the record-anchored
// approval lock read — a record page that offers an edit to a member who is
// not on the approval, whose save the lock hook then refuses; the ideal
// answer reads the lock (`client.approvals.getRecordLock`) rather than a
// request row the non-participant is never served, written so it never
// spells either `must_not_contain` string. Paid down first: the file's nine
// fixtures share no sentence, nothing to cut (0 bytes). 8561 -> 9705 bytes
// (+1144) -> ceil(9705 / 4) = 2427; headroom was 3 bytes, 0 tokens, so the
// ceiling moves +286, pinned AT the measurement. Same maintainer ruling as
// the SKILL.md row above (2026-10-27, verbatim and untranslated):
// 「A:先削重复,余额按实测提高」.
['skills/objectstack-automation/evals/flows-triggers-approvals.json', 2427],
// 894 -> 1095 (#26179): one regression case for the field permission `when`
// (the contract prompt: lock the legal terms for finance from approval on,
// legal keeps the edit) — expected: `when` on the finance set's entries with
Expand Down Expand Up @@ -871,7 +882,18 @@ export const CEILINGS = new Map([
// bullets and were deleted (~73 tokens). Landed 5555 -> 5587: +32 net, 14
// absorbed by headroom, ceiling +18. Same maintainer ruling as the SKILL.md
// row above (2026-10-27, verbatim and untranslated): 「A:先削重复,余额按实测提高」.
['skills/objectstack-automation/references/state-machines-and-approvals.md', 5587],
// 5587 -> 5730 (card #26362): one short section, "Reading the lock" — the
// record-anchored approval lock read every reader may ask (the route, the
// SDK call, `locked` alone with request content behind
// `approvalsVisibleToReaders`, an unreadable record answered as a missing
// one), +782 bytes. Paid down first, two genuine restatements of the
// Send-back section cut to pointers: the config table's `maxRevisions` row
// (-76 bytes) and the Branching section's send-back bullet (-134 bytes).
// 22346 -> 22918 bytes (+572 net) -> ceil(22918 / 4) = 5730; headroom was 2
// bytes, 0 tokens, so the ceiling moves +143, pinned AT the measurement.
// Same maintainer ruling as the SKILL.md row above (2026-10-27, verbatim and
// untranslated): 「A:先削重复,余额按实测提高」.
['skills/objectstack-automation/references/state-machines-and-approvals.md', 5730],
// 1779 -> 1769 (#26297): RE-LOCK at the landed count after a net cut — two
// stale `{ type: 'lookup', reference: 'user' }` fields (an object that does
// not exist) became `type: 'user'`; -10 tokens, the row follows the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,16 @@
"must_contain": ["record_link", "reference", "defaultValue", "outputVariable", "dialect: 'cel'"],
"must_not_contain": ["type: 'record'", "]("]
}
},
{
"id": 10,
"prompt": "The `expense` record page shows an Edit button to every member who can read the record. While an expense is under approval, members who are not on the approval still get the button and their save is refused with `409 RECORD_LOCKED`. Make the page know the record is locked before it offers the edit.",
"expected_output": "Read the lock itself: `client.approvals.getRecordLock('expense', recordId)` (`GET /api/v1/approvals/records/expense/:recordId/lock`) answers `{ locked }` to anyone who can read the record, participant or not — the same judgement the lock hook applies to the write — so the page hides or disables Edit while `locked` is `true`. Not the request row: a non-participant is served none, and request content stays behind the object's `approvalsVisibleToReaders`, which is not switched on for this. A record the caller cannot read answers `404 RECORD_NOT_FOUND`, the same as a missing one.",
"files": [],
"assertions": {
"must_contain": ["getRecordLock", "locked"],
"must_not_contain": ["lock_record", "approvalsVisibleToReaders: true"]
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,20 @@ reject) — enforced before any write, with no elevation bypass, so the run
cannot resume past the node with the key a later `expression` approver reads
still missing.

### Reading the lock

A page over a record under approval must not offer an edit the server then
refuses with `409 RECORD_LOCKED`. The request row tells only a participant,
or any reader once the object declares `enable.approvalsVisibleToReaders`;
everyone else is served no row. Ask the lock itself —
`GET /api/v1/approvals/records/:object/:recordId/lock`, in the SDK
`client.approvals.getRecordLock(object, recordId)` — answered to anyone who
can read the record as `{ success: true, data: { locked } }`: `locked` and
nothing else (request, approvers and history stay behind that switch), the
same judgement the lock hook applies to an edit. The record is read as the
caller: one they cannot read answers `404 RECORD_NOT_FOUND`, the same body
an id that does not exist gets.

### Approver Types

| `type` | Resolves to |
Expand Down Expand Up @@ -350,7 +364,7 @@ Object-hook `ctx` is a different vocabulary — see **objectstack-data**
| `excludeSubmitter` | `true` drops the run's submitter (`submitter_id`) from the resolved slate, whatever approver type produced them — segregation of duties; a slate it empties takes the `onEmptyApprovers` route, never a silent pass. `onEmptyApprovers: 'auto_approve'` is **refused** beside it (it would approve exactly the request this exists to stop): use `admin_rescue`, `fail` or `fallback`. Default `false` |
| `decisionOutputs` | Decision outputs a decision may carry (author declares, approvers fill values). Entries are bare keys (free-text input) **or typed declarations** `{ key, label?, type: 'text'\|'user'\|'department'\|'position'\|'team', multiple?, required? }` — a typed entry renders the matching record picker in the decision dialog (`multiple` collects an id array). Accepted outputs resume the run as `<nodeId>.<key>` variables; undeclared keys reject the decision; `decision`/`requestId` reserved |
| `escalation` | Optional per-node SLA — `{ enabled, timeoutHours, action: reassign\|auto_approve\|auto_reject\|notify, escalateTo?, notifySubmitter }`. `timeoutHours` is **calendar (wall-clock) hours** — nights, weekends and holidays count; the platform ships no business-hours calendar. `escalateTo` is a **position machine name** (expanded to its holders via `sys_user_position`, ADR-0090 D3) or a specific user id — never a membership tier. `reassign` without `escalateTo` degrades to notify (linted) |
| `maxRevisions` | ADR-0044 — max **send-backs-for-revision** per run before auto-reject. Default `3`; `0` disables send-back. Only meaningful when the node has a `revise` out-edge |
| `maxRevisions` | Send-backs per run before auto-reject (ADR-0044) — see *Send-back for revision* above |

### Branching, side-effects & rejection

Expand All @@ -362,9 +376,7 @@ These are wired on the **graph**, not in node config:
`http`, a `notify` node, …) to the `approve` / `reject` out-edge.
- **Roll back on reject** — route the `reject` edge as a **back-edge** to an
earlier node so the submitter can revise (the old `back_to_previous`).
- **Send back for revision (ADR-0044)** — distinct from a plain reject: a
`revise` out-edge into an **`approval_revise`** window, closed by a
`type: 'back'` resubmit edge. See *Send-back for revision* above.
- **Send back for revision (ADR-0044)** — see *Send-back for revision* above.
- **Hard reject** — route the `reject` edge to an `end` node (the old
`reject_process`).

Expand Down
Loading