You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[finding][spec] position.delegatable JSDoc names a security-delegatable-admin-position lint rule that does not exist — the runtime D12 gate is the only enforcer #6628
Found during a read-only truth sweep of packages/spec's text surfaces (guidance blocks, .describe() strings, tombstones, JSDoc contract prose) against the mechanisms they name. Filed unassigned for triage.
The defect
packages/spec/src/identity/position.zod.ts:87-98 — the JSDoc on the authorable delegatable key (re-anchored 2026-08-08T13:1xZ after e0f300ba5: the named rule now sits at :86, text unchanged):
* [ADR-0091 D3] Delegation of duty (职务代理). When true, a holder of this
* position may SELF-SERVICE assign it to a delegate — time-boxed
* (`valid_until` within the config ceiling), reasoned, dual-audited —
* WITHOUT being a delegated administrator. Default false: approval-duty
* positions (an approver going on leave) opt in; admin-ish positions do
* NOT — delegating administration would bypass the D12 containment gate,
* so a delegatable position must never distribute an `adminScope`-carrying
* set (enforced by the `security-delegatable-admin-position` lint rule and
* the D12 gate). A grant that itself arrived via delegation is not
* re-delegatable (chains are cut).
The parenthetical names two enforcers. Only one of them exists.
The authority
security-delegatable-admin-position occurs exactly once in the repository — in the sentence quoted above. There is no such lint rule.
The security-domain publish linter's own rule table is the authority, packages/lint/src/validate-security-posture.ts:9-21, and the exported rule-id constants beside it (:59-70) are the complete set of twelve — security-owd-unset, security-owd-alias, security-external-wider-than-internal, security-wildcard-vama, security-anchor-high-privilege, security-role-word, security-book-audience-unknown-set, security-private-no-readscope, security-master-detail-ungranted, security-fls-unqualified-key, security-grant-expired-at-authoring, security-delegation-missing-reason. No delegatable/admin-position rule among them.
The control that makes this a reading rather than a guess: ADR-0091's other author-time rules did land.security-grant-expired-at-authoring (D2) and security-delegation-missing-reason (D3 — the same decision as delegatable) are both present and both exported. So the absence is specific to this one rule, not an artefact of the linter not covering ADR-0091.
The runtime half of the claim is real.packages/plugins/plugin-security/src/delegated-admin-gate.ts:537-543 implements the containment check as step 6 of the self-service delegation path:
// 6. A delegatable position must not distribute administration.constboundSets=awaitthis.setsBoundToPosition(positionName);for(constbofboundSets){if(parseMaybeJson((basany).admin_scope??(basany).adminScope)){deny(`position '${positionName}' distributes the admin set '${b.name}' — administration cannot be self-delegated (D12 containment)`,{position: positionName,permissionSet: b.name});}}
with the delegatable: true precondition at :533-535 and the chain-cut at :497. So the invariant is held — at runtime, at the moment a delegation is attempted. It is simply not held at authoring time, and neither of the two things the sentence names as author-time enforcement is doing it.
Zero-hit falsification: the same grep over the same corpus returns security-anchor-high-privilege at validate-security-posture.ts:15 and :63, and returns 18 distinct 'security-*' rule-id literals repo-wide. The instrument sees rule ids; it does not see this one.
Why it matters — the authoring path
This sits on an authorable key's JSDoc, which means it reaches the package author (and the AI writing the position metadata) twice: as TSDoc hover in the editor at the exact moment they type delegatable:, and in the generated reference page.
What the sentence promises is an author-time gate: mark a position delegatable: true while it distributes an adminScope-carrying permission set, and os lint will stop you before you ship. It will not. The package ships clean. The mistake surfaces much later and somewhere else — as a runtime deny the first time a holder actually attempts a delegation, phrased as a fact about the position rather than as a fix for the authoring error, in a different package than the one the author was editing.
That gap is exactly what the linter's own header says its error rules exist to close (validate-security-posture.ts:25-27): "the lint moves the failure from runtime-deny to author-time fix-it." Here the spec advertises that move for a rule that was never written.
The same file already records this hazard class in its own words (validate-security-posture.ts:52-55): alias tolerance "silently downgraded a NAMED rejection into an inert branch — and an inert branch in a security linter reads, to the next author, as a gate that is watching (#4984, #5009, #5017)." A named but absent rule reads the same way, one layer further out.
Suggested direction
Non-binding: either drop the lint-rule name from the parenthetical (leaving "the D12 gate" as the stated enforcer, which is accurate as written), or keep the sentence and file the rule as real work. The text should not name a gate ahead of the gate.
Not in scope
The D12 gate's own behaviour and the delegatable default (false) — both correct as implemented.
Whether ADR-0091 D3 should have an author-time linter rule. That is a product call, and it is the thing this finding is deliberately not deciding.
No acceptance change. The fix is text only; PositionSchema accepts exactly what it accepts today.
Provenance
Audited at origin/main = 1f65bfc390ca896ec2575346520c4af60fe4176d, read via git show origin/main: (never the working tree). Re-verified at e0f300ba5+ on 2026-08-08T13:1xZ — premise intact, anchor :87 → :86.
Dedup searches run before filing: delegatable position lint (1 hit — Permission Model v2 — named follow-up ADRs (post-ADR-0090 parking lot) #2776, unrelated), spec finding describe enforced (8 hits, none on this surface), plus a repo-wide grep of the exact rule id, which returns only the defective sentence.
Sibling-repo coverage: objectstack + objectui at their respective origin/main. /home/user/cloud is not checked out in this environment; the rule id is absent from both repos that are.
Found during a read-only truth sweep of
packages/spec's text surfaces (guidance blocks,.describe()strings, tombstones, JSDoc contract prose) against the mechanisms they name. Filed unassigned for triage.The defect
packages/spec/src/identity/position.zod.ts:87-98— the JSDoc on the authorabledelegatablekey (re-anchored 2026-08-08T13:1xZ aftere0f300ba5: the named rule now sits at:86, text unchanged):The parenthetical names two enforcers. Only one of them exists.
The authority
security-delegatable-admin-positionoccurs exactly once in the repository — in the sentence quoted above. There is no such lint rule.The security-domain publish linter's own rule table is the authority,
packages/lint/src/validate-security-posture.ts:9-21, and the exported rule-id constants beside it (:59-70) are the complete set of twelve —security-owd-unset,security-owd-alias,security-external-wider-than-internal,security-wildcard-vama,security-anchor-high-privilege,security-role-word,security-book-audience-unknown-set,security-private-no-readscope,security-master-detail-ungranted,security-fls-unqualified-key,security-grant-expired-at-authoring,security-delegation-missing-reason. No delegatable/admin-position rule among them.The control that makes this a reading rather than a guess: ADR-0091's other author-time rules did land.
security-grant-expired-at-authoring(D2) andsecurity-delegation-missing-reason(D3 — the same decision asdelegatable) are both present and both exported. So the absence is specific to this one rule, not an artefact of the linter not covering ADR-0091.The runtime half of the claim is real.
packages/plugins/plugin-security/src/delegated-admin-gate.ts:537-543implements the containment check as step 6 of the self-service delegation path:with the
delegatable: trueprecondition at:533-535and the chain-cut at:497. So the invariant is held — at runtime, at the moment a delegation is attempted. It is simply not held at authoring time, and neither of the two things the sentence names as author-time enforcement is doing it.Zero-hit falsification: the same grep over the same corpus returns
security-anchor-high-privilegeatvalidate-security-posture.ts:15and:63, and returns 18 distinct'security-*'rule-id literals repo-wide. The instrument sees rule ids; it does not see this one.Why it matters — the authoring path
This sits on an authorable key's JSDoc, which means it reaches the package author (and the AI writing the position metadata) twice: as TSDoc hover in the editor at the exact moment they type
delegatable:, and in the generated reference page.What the sentence promises is an author-time gate: mark a position
delegatable: truewhile it distributes anadminScope-carrying permission set, andos lintwill stop you before you ship. It will not. The package ships clean. The mistake surfaces much later and somewhere else — as a runtimedenythe first time a holder actually attempts a delegation, phrased as a fact about the position rather than as a fix for the authoring error, in a different package than the one the author was editing.That gap is exactly what the linter's own header says its
errorrules exist to close (validate-security-posture.ts:25-27): "the lint moves the failure from runtime-deny to author-time fix-it." Here the spec advertises that move for a rule that was never written.The same file already records this hazard class in its own words (
validate-security-posture.ts:52-55): alias tolerance "silently downgraded a NAMED rejection into an inert branch — and an inert branch in a security linter reads, to the next author, as a gate that is watching (#4984, #5009, #5017)." A named but absent rule reads the same way, one layer further out.Suggested direction
Non-binding: either drop the lint-rule name from the parenthetical (leaving "the D12 gate" as the stated enforcer, which is accurate as written), or keep the sentence and file the rule as real work. The text should not name a gate ahead of the gate.
Not in scope
delegatabledefault (false) — both correct as implemented.PositionSchemaaccepts exactly what it accepts today.Provenance
origin/main=1f65bfc390ca896ec2575346520c4af60fe4176d, read viagit show origin/main:(never the working tree). Re-verified ate0f300ba5+ on 2026-08-08T13:1xZ — premise intact, anchor:87→:86.delegatable position lint(1 hit — Permission Model v2 — named follow-up ADRs (post-ADR-0090 parking lot) #2776, unrelated),spec finding describe enforced(8 hits, none on this surface), plus a repo-wide grep of the exact rule id, which returns only the defective sentence.systemFieldsownerguidance misstatesownership: 'org'— tells authors org "chooses the principal" when it injects noowner_idat all #6365, HookContext 契约表把before*的input.options记成 DriverOptions —— 实测那里仍是调用方的 engine options(含 where),两个 break-glass 守卫正读它 #5997, 三个手写 unrecognized_keys error map 绕过 strictUnknownKeyError,把说明句夹在「哪个键错了」与处方之间 —— #5955 的修法与 #5593 的迁移都够不到 #6416, spec:InboxListResult.unreadCount的 JSDoc 仍写「over the returned window」—— 与 #6363 落地后的实现和同族.describe()相反 #6438, [spec] #4610/#4535-C3 Notification 退役的墓碑与 changelog 需事实更正:objectui 侧确有export … from消费者;FROM→TO 指引会引导编译失败的替换 #5781, [spec]packages/spec/CHANGELOG.md的 17.0.0-rc.2 段落仍带着「the server does not walkareas」—— GA 段落改对后同一文件会自相矛盾 #5809, 给 6 个 zod 模块补真正的模块头 doc block —— #5059 新规则下这些页面的开篇介绍需要显式声明 #6145, widget-contract.mdx 整页 + quick-reference.mdx 一行仍在教 #5055 退役的 WidgetManifest 家族 —— 手写散文面,门禁不覆盖 #6413.objectstack+objectuiat their respectiveorigin/main./home/user/cloudis not checked out in this environment; the rule id is absent from both repos that are.