Repository navigation
Commit 3beb224
fix(lint): approver verdicts hand an empty slate to onEmptyApprovers instead of saying the request stalls (#27248)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBDiHvKSDRPiRqt9k9Arw6pw)_
Fixes #27101
Claim: 6468213761
Clause-②: no
Metadata-protocol: none
Skills-docs: none — REASON: copy-only change to two lint verdicts, one
`os explain` paragraph and one guide sentence. `git grep -n -e "request
stalls" -e approval-approver-type-unsupported -e
approval-approver-not-membership-tier -- skills` on base finds 0 hits,
so no skill teaches either verdict or the stalled outcome.
## 维护者速读
- **改了什么:** `os validate`
的两条审批人警告(`approval-approver-not-membership-tier`、`approval-approver-type-unsupported`)不再说「请求会卡住」,改为只陈述这个槽位匹配不到人,并以「onEmptyApprovers
decides an empty slate」收尾;不支持类型的提示改为从审批人绑定表推导可用路由;同步修正 `os explain`
一段与审批指南一句。
- **为什么改:** 自 `onEmptyApprovers` 默认值变为 `admin_rescue`
起,空审批人名单会转给组织管理员(或按节点声明的策略走),「卡住」是错误描述,会误导作者去「修」正常行为。
- **风险与回滚:** 只改文案与提示,规则触发条件不变,运行时行为不变;回滚 = revert 本 PR。
- **先看:**
`packages/lint/src/validate-approval-approvers.ts`(`EMPTY_SLATE_DECIDES`
与 `EXPANDABLE_GROUP_TYPES`
两处)、`packages/lint/src/validate-approval-approvers.unsupported-type.test.ts`。
- **两分钟验证:** `pnpm --filter @objectstack/lint exec vitest run
src/validate-approval-approvers.test.ts
src/validate-approval-approvers.unsupported-type.test.ts` → 全绿;`git grep
-n "and the request stalls" -- packages/lint/src
content/docs/automation` → 0 行(main 上 3 行)。
- **需要你做的:** 无,席位落地。
## What changes
File surface (claim 6468213761, plus the three additions seat 1
approved):
- `packages/lint/src/validate-approval-approvers.ts`. The two verdicts
that ended on "the request stalls" now state the slot and end on one
shared clause, `EMPTY_SLATE_DECIDES` = "onEmptyApprovers decides an
empty slate":
- `approval-approver-not-membership-tier`: "approver { type:
'org_membership_level', value: 'sales_manager' } names no membership
tier (owner/admin/delegated_admin/member/external), so it matches
nobody; onEmptyApprovers decides an empty slate".
- `approval-approver-type-unsupported`: "… the slot resolves to nobody,
and onEmptyApprovers decides an empty slate."
- Why this wording and not a per-policy outcome: the triage asked for
copy that cannot drift. One approver slot cannot see the rest of the
slate, so the request's fate is not the verdict's to state. The clause
names no outcome and no default, so it stays true under all four
`onEmptyApprovers` values and under any later change to the default.
This matches the runtime's own expansion warnings in
`resolveApproverSpec`, which "state the SLOT, never the request's fate".
A per-policy outcome would also not fit: the membership-tier verdict is
pinned to 200 characters, and the tier list (now five tiers) leaves no
room for it.
- "org-membership tier" became "membership tier" in that verdict, to
stay within the same pin. The type name `org_membership_level` is
already in the sentence.
- The `approval-approver-type-unsupported` hint now builds its route
list from `APPROVER_VALUE_BINDINGS` (`EXPANDABLE_GROUP_TYPES`: the
group-routed types whose binding is not `unsupported`). The hardcoded
list used to offer `queue` while `queue` was the one unsupported type.
With this change the hint can never offer an unsupported type, including
the flagged one. `queue` is supported today (ADR-0142 D6), so it stays
in the list.
- Code comments: the arm's comment now says no binding is `unsupported`
on main and that the arm stays as the guard for the next one. The file
header no longer describes a "stuck request".
- `packages/lint/src/rule-explanations.ts` (addition 1). The
`approval-approver-not-membership-tier` explanation said "the request
waits on an approver that can never act". It now says the literal is a
slot no one can act on, and a slate that names no one else is decided by
the node's `onEmptyApprovers`.
- `packages/lint/src/validate-approval-approvers.test.ts`. The
membership-tier verdict's exact-string pin is updated. A new case drives
the verdict under an omitted policy and under `admin_rescue`, `fail` and
`auto_approve`, and asserts that it names no outcome under any of them.
-
`packages/lint/src/validate-approval-approvers.unsupported-type.test.ts`
(addition 2, new). No binding is `unsupported` on main, so this arm is
reachable only against a binding table that declares one. The file mocks
`@objectstack/spec/automation` with `team` marked `unsupported`, in its
own module graph, and pins the message and the hint. The hint pin is
`'position' | 'department' | 'queue'`, so the flagged `team` is not
offered.
- `content/docs/automation/approvals.mdx` (addition 3, seat 1's
request). The `position` vs `org_membership_level` callout said "matches
nobody and the request stalls". It now says the approver adds no one,
and a slate left naming no one goes to the node's `onEmptyApprovers`.
- `.changeset/27101-approver-lint-empty-slate-copy.md`:
`@objectstack/lint` patch, with both declaration lines.
No rule's firing condition changes.
## Why
The default `onEmptyApprovers: 'admin_rescue'` opens a slate that names
no one on the organization's administrators (`openNodeRequest` in
`packages/plugins/plugin-approvals/src/approval-service.ts`,
`slateNamesAPerson` branch). Other policies fail the node, auto-approve,
or open on `fallbackApprovers`. "The request stalls" is true under none
of them. An author who believes it either "fixes" something that works
or misreads what happens at runtime.
## Tests
<details><summary>Readings (head 484c616, base origin/main
dfe85c9)</summary>
- `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2
src/validate-approval-approvers.test.ts
src/validate-approval-approvers.unsupported-type.test.ts
src/rule-explanations` → Test Files 3 passed (3), Tests 73 passed (73).
- `pnpm --filter @objectstack/lint test` → Test Files 173 passed (173),
Tests 7533 passed, 5 skipped (7538).
- `pnpm --filter @objectstack/lint typecheck` → exit 0 (`tsc --noEmit`
and `check:test-typecheck` OK).
- Ablation, one time, on the committed head: `node
scripts/ablation-replace.mjs` changed the `EXPANDABLE_GROUP_TYPES`
filter to `… !== 'unsupported' || true` (anchor 1 → 0, blob 7ee40cc
→ 510c23d1fd9a). The unsupported-type test went red on "never offers an
unsupported type as the route, the flagged one included". After the
restore, `git diff HEAD` was empty. The suite imports
`./validate-approval-approvers.js` from source, so no dist rebuild was
involved.
</details>
## Acceptance notes
- `content/docs/automation/approvals.mdx` L58 still says the tier's
"only values are `owner`/`admin`/`member`". `BUILTIN_MEMBERSHIP_ROLES`
also has `delegated_admin` and `external`. This is a separate stale
fact, not touched here.
- Sibling verdicts in the same file still spell the default outcome as
fixed text. The `approval-approvers-may-resolve-empty` group and manager
arms say "the request opens on the organization's administrators" even
when the node declares another `onEmptyApprovers`.
`approval-expression-no-empty-policy` restates `'admin_rescue'`, but it
fires only when the policy is omitted. Neither is in this card's scope,
and their messages are pinned, so they are noted here only.
## Verification
<details><summary>Derived gates (dispatch-gates --commands at 484c616,
87 families)</summary>
`dispatch-gates --ran` → "87 derived famil(ies) accounted for — 83 run,
4 NOT-MEASURED (4 DERIVED from a recorded exit 3)".
- 83 exit 0. That includes `check-plugin-teardown-shape --self-test` (48
cases), which passed after fetching its pinned fixture commit, and `lint
check:doc-formula-expressions` and `check:doc-security-posture`, which
passed after `pnpm --filter @objectstack/lint build`.
- NOT MEASURED (exit 3, a full-repo build is missing): `spec
check:skill-examples`, `check:docs-transcript-drift`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure`. CI runs them.
- Dogfood: n/a. The title has no `!` and there is no `(narrowing)` arm.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (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.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzifjQqjY4ZFCkVmUgJ
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WGufzifjQqjY4ZFCkVmUgJ)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7858c1d commit 3beb224
6 files changed
Lines changed: 124 additions & 17 deletions
File tree
- .changeset
- content/docs/automation
- packages/lint/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
60 | | - | |
61 | | - | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
62 | 63 | | |
63 | 64 | | |
64 | 65 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2779 | 2779 | | |
2780 | 2780 | | |
2781 | 2781 | | |
2782 | | - | |
2783 | | - | |
| 2782 | + | |
| 2783 | + | |
| 2784 | + | |
2784 | 2785 | | |
2785 | 2786 | | |
2786 | 2787 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
784 | 784 | | |
785 | 785 | | |
786 | 786 | | |
787 | | - | |
788 | | - | |
| 787 | + | |
| 788 | + | |
789 | 789 | | |
790 | 790 | | |
791 | 791 | | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
792 | 812 | | |
793 | 813 | | |
794 | 814 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
8 | 8 | | |
9 | 9 | | |
10 | 10 | | |
11 | | - | |
12 | | - | |
13 | | - | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
14 | 15 | | |
15 | 16 | | |
16 | 17 | | |
| |||
231 | 232 | | |
232 | 233 | | |
233 | 234 | | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
234 | 256 | | |
235 | 257 | | |
236 | 258 | | |
| |||
458 | 480 | | |
459 | 481 | | |
460 | 482 | | |
461 | | - | |
462 | | - | |
| 483 | + | |
| 484 | + | |
463 | 485 | | |
464 | 486 | | |
465 | 487 | | |
| |||
483 | 505 | | |
484 | 506 | | |
485 | 507 | | |
486 | | - | |
487 | | - | |
488 | | - | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
489 | 514 | | |
490 | 515 | | |
491 | 516 | | |
492 | 517 | | |
493 | 518 | | |
494 | 519 | | |
495 | 520 | | |
496 | | - | |
497 | | - | |
498 | | - | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
499 | 526 | | |
500 | 527 | | |
501 | 528 | | |
| |||
| 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 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
0 commit comments