Repository navigation
Commit f9ecf95
docs(skills): objectstack-automation teaches the record-anchored approval lock read, with an eval prompt (#26495)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8VSG6s7uuGB6TBQmif421jd)_
Fixes #26362
Clause-②: no
Metadata-protocol: none
## 维护者速读
-
**改了什么**:`skills/objectstack-automation/references/state-machines-and-approvals.md`
新增一小节「Reading the lock」(12 行):什么时候读锁(不在审批上的读者拿不到 request 行)、路由 `GET
/api/v1/approvals/records/:object/:recordId/lock` 答 `{ success: true,
data: { locked } }`、SDK `client.approvals.getRecordLock(object,
recordId)`、只答 `locked`(request 内容仍在对象的 `approvalsVisibleToReaders`
之后)、读不到的记录与不存在的记录同答 `404 RECORD_NOT_FOUND`。同文件两处复述 Send-back
节的文字削成指针。eval 文件加第 10 题。棘轮脚本两行上限按实测调整,裁决原文写在行旁。
- **为什么改**:PR #26361 把锁读落进了服务、路由与 SDK,文档也写了,技能没教。AI 作者给带审批的对象做记录页时,只会从
request 行的 `lock_record` 读锁;不在审批上的读者拿不到 request 行,页面就给他一个服务端会用 `409
RECORD_LOCKED` 拒掉的编辑按钮。
- **风险与回滚**:只改已发布技能包的文字、一个 eval 夹具和棘轮脚本的两行上限,不改运行时;回滚即 revert 本
PR(上限行随之回落)。
- **先看**:① `references/state-machines-and-approvals.md` L222–L235 新节;②
`scripts/check-skills-token-ratchet.mjs` 两行上限及注释;③
`evals/flows-triggers-approvals.json` 第 10 题。
- **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` → 看到
`state-machines-and-approvals.md is 5730 tokens (ceiling 5730; headroom
0)` 与 `flows-triggers-approvals.json is 2427 tokens (ceiling 2427;
headroom 0)`,末行 `81 authored bundle file(s) within their ceilings`。
- **需要你做的**:Tier H(`skills/**`),请 approve。
## What changes
Card #26362's 「Who acts」 paragraph, and nothing wider:
1. **`references/state-machines-and-approvals.md`, one section, "Reading
the lock"** (after *Recording a decision*, before *Approver Types*; 12
lines of prose, no code block): a page over a record under approval must
not offer an edit the server refuses with `409 RECORD_LOCKED`; the
request row tells only a participant, or any reader once the object
declares `enable.approvalsVisibleToReaders`, so a non-participant is
served no row; the lock itself is `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 content behind that switch, the lock
hook's own judgement; the record is read as the caller, so one they
cannot read answers `404 RECORD_NOT_FOUND`, the same body an id that
does not exist gets. Every clause was read from the merged code, not the
card: `packages/plugins/plugin-approvals/src/approval-service.ts`
`getRecordLock` (the record is `engine.find`-read as the caller; an
unreadable row and a missing id answer the same `null`; the judgement is
`recordHeldByLockingApproval`, the hook's own; `recordReaderTierOn`
reads `enable.approvalsVisibleToReaders`),
`record-lock-read.integration.test.ts` (the non-participant reader is
told `{ locked: true }` and still served no request row; `_b` and
`MISSING` answer alike), `packages/rest/src/approvals-routes.ts` +
`rest-approvals-record-lock-route.test.ts` (one `404 RECORD_NOT_FOUND`
for every `null`, `{ locked }` alone), `packages/client/src/index.ts`
`getRecordLock`, and `content/docs/automation/approvals.mdx` 「Reading
the lock」. The deployment-without-the-member `501` is not taught (not
authoring guidance).
2. **`evals/flows-triggers-approvals.json`**: entry 10 — an `expense`
record page offers Edit to members not on the approval and their save is
refused `409 RECORD_LOCKED`; the ideal answer reads the lock
(`client.approvals.getRecordLock`, the route) for anyone who can read
the record, hides or disables Edit while `locked`, names the request row
a non-participant is never served and the `approvalsVisibleToReaders`
switch that is not flipped for this, and the `404` for an unreadable
record. `must_contain`: `getRecordLock`, `locked`; `must_not_contain`:
`lock_record` (the request-row lookup the card names),
`approvalsVisibleToReaders: true` (the other wrong fix: exposing request
content to every reader). The expected output is written so that it
never spells either `must_not_contain` string (checked by script).
3. **Two genuine restatements cut to pointers**, same file, both of the
*Send-back for revision* section, which is their surviving home: the
Node Config table's `maxRevisions` row (it restated item 3 — default
`3`, `0` disables, only with a `revise` edge — now "Send-backs per run
before auto-reject (ADR-0044) — see *Send-back for revision* above", −76
bytes) and the Branching section's send-back bullet (it restated the
section's three pieces — the `revise` edge into `approval_revise`, the
`type: 'back'` resubmit edge — now the pointer alone, −134 bytes).
Nothing else in the file restates another passage.
4. **`scripts/check-skills-token-ratchet.mjs`**, the two
objectstack-automation rows this PR touches and no other, under the
maintainer's ruling on the seat's decision card 「skills 修复卡的教学和 eval 都撞
token 天花板,怎么付?」, tap **A** (2026-10-27 01:21Z), verbatim and
untranslated: 「A:先削重复,余额按实测提高」. Step one, duplicates cut first: the two
above (−210 bytes); the eval file's nine fixtures share no sentence (0
bytes to cut). Step two, each row follows its measurement (`ceil(utf8
bytes / 4)`): `state-machines-and-approvals.md` 22346 → 22918 bytes
(+782 added, −210 cut, +572 net), 5587 → **5730** (headroom was 2 bytes,
0 tokens; +143); `evals/flows-triggers-approvals.json` 8561 → 9705 bytes
(+1144), 2141 → **2427** (headroom was 3 bytes, 0 tokens; +286). Both
pinned AT the measurement, each comment quoting the tap. Rows measured
against `origin/main` `cc0d77b3` (after #26446 landed as `d1a31d0c`),
the branch's base.
`SKILL.md` is untouched: its pointer line already routes every approvals
question to this reference, and line 1 belongs to #25640.
`references/_index.md` is generator-owned (`gen:skill-refs`) and
unchanged (no schema moved). The section is prose with inline code, so
no `os:check` block was added and the `check:skill-examples` count stays
as on `main`.
**Two readings** (lines, and tokens as the ratchet counts them):
| File | Lines before → after | Tokens before → after |
|:--|:--|:--|
| `references/state-machines-and-approvals.md` | 380 → 392 (+12) | 5587
→ 5730 (+143) |
| `evals/flows-triggers-approvals.json` (not .md, reported for the
raise) | 95 → 105 (+10) | 2141 → 2427 (+286) |
| `SKILL.md` | 441 → 441 (0) | 6164 → 6164 (0) |
| **Whole package** (`SKILL.md` + `references/**/*.md`, 4 files; the
package has no `rules/`) | **1001 → 1013 (+12)** | **14091 → 14234
(+143)** |
Removed: the two Send-back restatements only (−210 bytes, −2 lines).
`skip-changeset`: the published skills packages carry no `package.json`;
nothing is released from any package.
## Why
PR #26361 (#26314) landed `IApprovalService.getRecordLock`, the route
and the SDK call, and documented them; the owning skill did not teach
them, so an AI author building a page over an object with approvals
learned the lock only from the request row, which a reader not on the
approval is never served — the page then offered an edit the server
refused with `409 RECORD_LOCKED`. `Metadata-protocol: none`: the
protocol already carries the route; the gap was the teaching.
## Tests
- `node scripts/check-skills-token-ratchet.mjs` — exit 0: both touched
rows at headroom 0, `81 authored bundle file(s) within their ceilings;
10 generator-owned file(s) measured, not ratcheted`.
- `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0,
`79 cases pass`.
- `pnpm --filter @objectstack/spec run check:skill-docs`,
`check:skill-refs`, `check:skill-examples` — see Verification.
- Eval 10 is a fixture (the repo has no eval runner): a script checked
every `must_contain` string occurs in its expected output and neither
`must_not_contain` string occurs in the prompt or the expected output.
- `grep -i -w role` over the added lines: no hit (`check:role-word`
green); no control byte in the three files.
## Acceptance notes
- `SKILL.md`'s pointer line to this reference names only the
state-introspection route; the lock-read route now lives in the
reference's new section. Not changed here (the dispatch order allows a
`SKILL.md` edit only if the approvals row must point at the section; the
pointer already routes approvals questions to the file): observation
only.
- The PR body's first two lines are the harness-required attribution
block (the os-dev exception); `Fixes #26362` follows immediately.
## Verification
Gate list derived with `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` on the final tree (base `cc0d77b3`,
head `0f8b9708`), each run locally with the exit code captured before
any pipe; `--ran` reconciliation below. No commit followed the gate
runs, so every reading is of `0f8b9708`.
**Reconciliation** (`node scripts/pm/dispatch-gates.mjs --ran ran.list
--repo objectstack-ai/objectstack`): exit 0 — `52 derived famil(ies)
accounted for — 51 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit
3)`, 0 UNRUN.
The one NOT MEASURED is `pnpm check:dual-build-cjs-loads` (exit 3: its
prerequisite is a whole-repo build; this diff touches no package source,
and CI's `Lint & Repo Gates` runs it — a declared narrowing). The
closures for `check:skill-examples` and `check:doc-formula-expressions`
(`@objectstack/client...`, `@objectstack/client-react...`,
`@objectstack/lint...`, `@objectstack/formula...`) were built under
`scripts/pm/os-verify-lock.sh` (slot `issue-26362`, `VERDICT
command-exit 0`, 298 s held, 0 s waited); `check:dts-closure` and
`check:lean-entry-closure` ran after that build (`38 built package(s)
swept - 160/160 declared declaration file(s) present`). `node
scripts/check-comment-mask-corpus.mjs` ran in the serial cheap runner
and had finished before `check:skill-examples` started.
`check:pm-dispatch-gates` ran detached on the final head: `fast tier,
1935 cases pass`, exit 0. Two runs are outside the derivation and
recorded anyway: `check:skill-docs` and `check:skill-examples`, the
card's acceptance gates (the section is prose, so no marked block was
added).
<details><summary>Every command with its exit code on 0f8b970 (54
lines: the 52 derived, plus `check:skill-docs` and
`check:skill-examples`)</summary>
- `node scripts/check-ci-filter-parity.mjs` — exit 0
- `node scripts/check-closing-keyword-parity.mjs` — exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` — exit 0
- `node scripts/check-comment-mask-corpus.mjs` — exit 0
- `node scripts/check-declaration-mirrors.mjs` — exit 0
- `node scripts/check-declaration-mirrors.mjs --self-test` — exit 0
- `node scripts/check-doc-route-spelling.mjs --advisory` — exit 0
- `node scripts/check-doc-route-spelling.mjs --self-test` — exit 0
- `node scripts/check-dts-emitted.mjs --self-test` — exit 0
- `node scripts/check-scripts-symbol-anchors.mjs` — exit 0
- `node scripts/check-scripts-symbol-anchors.mjs --self-test` — exit 0
- `node scripts/check-self-test-wired.mjs` — exit 0
- `node scripts/check-self-test-wired.mjs --self-test` — exit 0
- `node scripts/check-self-test-workflow-commands.mjs` — exit 0
- `node scripts/check-self-test-workflow-commands.mjs --self-test` —
exit 0
- `node scripts/check-skills-token-ratchet.mjs` — exit 0
- `node scripts/check-skills-token-ratchet.mjs --self-test` — exit 0
- `node scripts/check-whole-set-label-write.mjs` — exit 0
- `node scripts/check-whole-set-label-write.mjs --self-test` — exit 0
- `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` — exit 0
- `node scripts/ci/plan-test-core-shards.mjs --self-test` — exit 0
- `node scripts/ci/prune-turbo-cache.mjs --self-test` — exit 0
- `node scripts/pm/bare-root-worklist.mjs --self-test` — exit 0
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions` —
exit 0
- `pnpm --filter @objectstack/spec run check:skill-refs` — exit 0
- `pnpm check:agent-test-spelling` — exit 0
- `pnpm check:bash32-floor` — exit 0
- `pnpm check:cli-command-ids` — exit 0
- `pnpm check:corpus-claim-drift` — exit 0
- `pnpm check:cross-package-test-inputs` — exit 0
- `pnpm check:doc-authoring` — exit 0
- `pnpm check:driver-memory-census` — exit 0
- `pnpm check:dts-closure` — exit 0
- `pnpm check:dual-build-cjs-loads` — NOT MEASURED (exit 3, PREREQUISITE
NOT MET: this gate reads built output and some package has no dist/;
CI's Lint & Repo Gates runs it)
- `pnpm check:entry-guard` — exit 0
- `pnpm check:gitlink-declared` — exit 0
- `pnpm check:lean-entry-closure` — exit 0
- `pnpm check:nul-bytes` — exit 0
- `pnpm check:parse-guard` — exit 0
- `pnpm check:pm-dispatch-gates` — exit 0
- `pnpm check:pm-governed-merges` — exit 0
- `pnpm check:pm-skill-id-lint` — exit 0
- `pnpm check:pnpm-filter-targets` — exit 0
- `pnpm check:pnpm-flag-spellings` — exit 0
- `pnpm check:ratchet-remedy-authority` — exit 0
- `pnpm check:refd-timer-probe` — exit 0
- `pnpm check:role-word` — exit 0
- `pnpm check:skill-compatibility` — exit 0
- `pnpm check:skill-frame-sync` — exit 0
- `pnpm check:skill-identifier-liveness` — exit 0
- `pnpm check:sourcemap-no-sources-content` — exit 0
- `pnpm check:watch-hint-literal` — exit 0
- `pnpm --filter @objectstack/spec run check:skill-docs` — exit 0 (card
acceptance gate, outside the derivation)
- `pnpm --filter @objectstack/spec run check:skill-examples` — exit 0
(card acceptance gate, outside the derivation)
</details>
<details><summary>Token ratchet on 0f8b970</summary>
-
`skills/objectstack-automation/references/state-machines-and-approvals.md
is 5730 tokens (ceiling 5730; headroom 0)`
- `skills/objectstack-automation/evals/flows-triggers-approvals.json is
2427 tokens (ceiling 2427; headroom 0)`
- `81 authored bundle file(s) within their ceilings; 10 generator-owned
file(s) measured, not ratcheted.`
- `--self-test`: `79 cases pass.`
</details>
<details><summary>check:skill-examples on 0f8b970</summary>
`265 prose examples type-check across 3 surface(s) — every marked block
parsed, so tsc ran the SEMANTIC pass on all of them` (this diff adds no
`os:check` block: the new section is prose with inline code).
</details>
## Human Minimum
Each item is one rule from [CONTRIBUTING.md › Human
Minimum](https://github.com/objectstack-ai/objectstack/blob/main/CONTRIBUTING.md#human-minimum).
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(No schema change.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` is CI's run (repo-wide scan).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. Nothing publishes (`skip-changeset`); both declaration lines
are filled in above.
- [x] 6. `packages/spec` not touched.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block and
the quoted ruling are the required exceptions).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZpLcVkA9KV78eHZYr)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 659f2d9 commit f9ecf95
3 files changed
Lines changed: 50 additions & 6 deletions
File tree
- scripts
- skills/objectstack-automation
- evals
- references
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
796 | 796 | | |
797 | 797 | | |
798 | 798 | | |
799 | | - | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
800 | 811 | | |
801 | 812 | | |
802 | 813 | | |
| |||
882 | 893 | | |
883 | 894 | | |
884 | 895 | | |
885 | | - | |
| 896 | + | |
| 897 | + | |
| 898 | + | |
| 899 | + | |
| 900 | + | |
| 901 | + | |
| 902 | + | |
| 903 | + | |
| 904 | + | |
| 905 | + | |
| 906 | + | |
| 907 | + | |
886 | 908 | | |
887 | 909 | | |
888 | 910 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
93 | 103 | | |
94 | 104 | | |
95 | 105 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
219 | 219 | | |
220 | 220 | | |
221 | 221 | | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
222 | 236 | | |
223 | 237 | | |
224 | 238 | | |
| |||
350 | 364 | | |
351 | 365 | | |
352 | 366 | | |
353 | | - | |
| 367 | + | |
354 | 368 | | |
355 | 369 | | |
356 | 370 | | |
| |||
362 | 376 | | |
363 | 377 | | |
364 | 378 | | |
365 | | - | |
366 | | - | |
367 | | - | |
| 379 | + | |
368 | 380 | | |
369 | 381 | | |
370 | 382 | | |
| |||
0 commit comments