Skip to content

fix(spec): position.delegatable names only the enforcer that exists (#6628) - #6759

Merged
os-project-manager merged 1 commit into
mainfrom
claude/issue-6628-delegatable-phantom-lint-rule
Aug 8, 2026
Merged

fix(spec): position.delegatable names only the enforcer that exists (#6628)#6759
os-project-manager merged 1 commit into
mainfrom
claude/issue-6628-delegatable-phantom-lint-rule

Conversation

@os-project-manager

Copy link
Copy Markdown
Collaborator

Fixes #6628

What was wrong

The JSDoc on the authorable delegatable key closed with:

so a delegatable position must never distribute an adminScope-carrying set
(enforced by the security-delegatable-admin-position lint rule and the D12 gate).

The parenthetical names two enforcers. Only the second one exists.

Premise re-verified on origin/main @ 61282f906 (the card was last re-anchored at e0f300ba5):

  • security-delegatable-admin-position occurs exactly once in the repository — in that sentence (git grep -c over origin/main returns packages/spec/src/identity/position.zod.ts:1, and no other file).
  • The authority is packages/lint/src/validate-security-posture.ts — its rule table (:9-21) and the twelve exported rule-id constants beside it (:59-70). No delegatable/admin-position rule is 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. The absence is specific to this one rule, not an artefact of the linter skipping ADR-0091.
  • The runtime half is real: packages/plugins/plugin-security/src/delegated-admin-gate.ts:537-543 implements the D12 containment check as step 6 of the self-service delegation path, denying with the offending permission set named.

So the invariant holds. What was false is when it holds. The sentence promised an author-time gate, so an author marking a position delegatable: true while it distributes an adminScope-carrying set believed os lint would stop them before shipping. It does not: the package publishes clean, and the mistake surfaces later, in a different package, as a runtime deny phrased as a fact about the position rather than as a fix for the authoring error.

That is the validate-security-posture.ts header's own hazard one layer out. It records how 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 rule that is named but absent reads the same way, and is cheaper to write by accident: prose costs nothing to add and no compiler checks it.

The fix — text only

Per the direction ruling on the card, this corrects the text to name only the enforcer that exists; it deliberately does not write the missing lint rule. Whether ADR-0091 D3 should grow an author-time rule is a product decision the finding explicitly declined to make, and it stays open.

The corrected JSDoc keeps three properties the card asked for:

  1. the invariant is real and enforced — "That invariant IS enforced";
  2. enforcement is at delegation time, not publish time — the D12 gate "refuses the delegation the moment a holder attempts it";
  3. the failure the author will actually see is a deny, not a lint error — stated in those words.

It also points at the one author-time rule ADR-0091 D3 does have (security-delegation-missing-reason) and says what that one actually checks (a seeded delegation row carries its dual-audit reason), so "no lint rule for this combination" cannot be misread as "this invariant is unenforced" — the opposite lie.

Zero acceptance-surface bytes. Every changed line in position.zod.ts is inside the JSDoc block — mechanically confirmed by filtering the diff to non-comment lines, which returns nothing. PositionSchema accepts exactly what it accepted before.

The pin

packages/spec/src/identity/position-delegatable-enforcer.pin.test.ts, modelled on expression-dialect-docs.pin.test.ts (#6085) — the repo's existing "TSDoc prose versus a machine-readable authority" pin shape.

No pin asserted the phantom name or the parenthetical (swept: the name's single occurrence was the defect), so this adds one. The authority is never a hand-copied list: it reads the security-* rule-id constants off packages/lint/src/ the way rule-id-barrel-exports.test.ts (#5648) reads that directory, so a security rule added in a new file counts the moment it exists. Four cases:

  • the rule table is real (floor of 12 ids, and contains the security-delegation-missing-reason control the finding used);
  • self-test — the historical sentence's shape with a deliberately synthetic rule name is reported as unbacked, proving the predicate has teeth regardless of what the prose currently says;
  • the live JSDoc names no rule packages/lint does not declare;
  • the JSDoc still locates the D12 check at runtime (both halves: naming D12 without placing it at runtime is the sentence this pin exists for; placing it at runtime without naming D12 reads as unenforced).

Two deliberate choices, both documented in the file:

  • The self-test uses a synthetic name rather than the literal security-delegatable-admin-position. Asserting that specific name is unbacked would quietly make this test the thing that breaks the day someone implements the rule — the open product decision. What needs pinning is the predicate, not what an unwritten rule would be called.
  • The rule-id slug pattern requires a multi-segment slug, which is what separates a rule id from prose: the cloud product name security-enterprise appears backticked in two spec files and is not a rule.

Scope kept narrow, deliberately. A repo-wide sweep was measured rather than assumed: the prose construct "backticked slug + lint rule" has exactly one instance repo-wide, and this PR removes it. A new repo-wide gate would therefore guard a class with zero remaining instances, at the cost of a CI step and a false-positive surface over other agents' in-flight prose. The narrow pin on the actual surface is the better trade; if the class ever recurs elsewhere, the generalisation is a separate card.

Reverse verification — direction predicted before running

Predicted red, and specifically 2 of 4 cases: restoring the original sentence should fail the unbacked-rule-id assertion (the phantom is not in lint's exported set) and the runtime-location assertion (the old block says "D12" but never "runtime"), while the rule-table floor and the self-test are independent of the file and must stay green.

Measured, after git checkout origin/main -- packages/spec/src/identity/position.zod.ts:

Tests  2 failed | 2 passed (4)

FAIL  ... > names no rule that `packages/lint` does not declare
AssertionError: expected [ Array(1) ] to deeply equal []
- []
+ [ "security-delegatable-admin-position" ]

FAIL  ... > still locates the D12 containment check at runtime
AssertionError: expected '/**\n   * [ADR-0091 D3] Delegation of…' to match /runtime/i

Exactly as predicted, including which two.

Mechanism assumptions — one confirmed, one falsified

Falsified: no regeneration was needed. The dispatch expected an authorable key's JSDoc to reach the generated reference page, making some regen plausible. Measured, it does not. content/docs/references/identity/position.mdx:67 carries the .describe() string only — which this PR does not touch — and the JSDoc block reaches neither the .d.ts nor the runtime .js, only the sourcemaps. After a full pnpm --filter @objectstack/spec build, check:generated reports all ten artifacts up to date with nothing to regenerate. (Same "cuts both ways" shape as part 1 of #6630 / PR #6701.)

Confirmed: the changeset is right, though for a different reason than hover. packages/spec's published files includes "src/**/*.zod.ts", so position.zod.ts itself ships to npm — the corrected prose reaches consumers and AI authors reading the installed schema source. That is a published author surface, so @objectstack/spec: patch. No skip-changeset route is needed here.

Verification

All run in this worktree off origin/main @ 61282f906:

Check Result
pnpm --filter @objectstack/spec test 346 files / 8848 tests passed
pnpm --filter @objectstack/spec typecheck pass (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; the new pin compiles clean, no debt-ledger entry)
pnpm --filter @objectstack/spec check:generated all 10 artifacts up to date, no regeneration
pnpm lint (the required ESLint job) exit 0
pnpm check:nul-bytes OK — 6258 tracked files, no raw control bytes
check:adr-anchors / check:role-word / check:doc-authoring / check:published-files / check:spec-parsed-alias / check:quick-reference-counts pass

Byte discipline: the diff was self-scanned beyond the gate with grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' on both changed files — clean.

content/docs/releases/ untouched.


🤖 Generated with Claude Code

https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk


Generated by Claude Code

…#6628)

The JSDoc on the authorable `delegatable` key claimed the "never distribute an
`adminScope`-carrying set" invariant was "enforced by the
`security-delegatable-admin-position` lint rule and the D12 gate". That lint
rule was never written — the string occurred exactly once in the repository,
in that sentence. `packages/lint/src/validate-security-posture.ts`'s rule table
and its twelve exported rule-id constants are the authority, and ADR-0091's
other author-time rules (`security-grant-expired-at-authoring`,
`security-delegation-missing-reason`) DID land, so the absence is specific to
this one rule.

The invariant itself is real: plugin-security's delegated-admin gate implements
the D12 containment check as step 6 of the self-service delegation path. What
was false is WHEN it holds. The sentence promised an author-time gate, so an
author pairing `delegatable: true` with an `adminScope`-carrying set believed
`os lint` would stop them; it does not, and the mistake surfaces later as a
runtime deny in a different package.

The JSDoc now names only the D12 gate, locates it at delegation time, and says
the failure an author sees is a deny rather than a lint error — while pointing
at the one author-time rule ADR-0091 D3 does have, so "no lint rule for this"
cannot be misread as "unenforced".

A pin (`position-delegatable-enforcer.pin.test.ts`) holds the relation: every
`security-*` rule the JSDoc names must be one `packages/lint` actually exports,
read off its `src/` the way `rule-id-barrel-exports.test.ts` (#5648) does, plus
a self-test proving the predicate has teeth. Reverse-verified: restoring the
original sentence turns exactly 2 of its 4 cases red.

Text only — `PositionSchema` accepts exactly what it accepted before, and all
ten spec generated-artifact gates are up to date with no regeneration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ffcE95NaMJcL9XJ9VDYgk
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 8, 2026 2:26pm

Request Review

@github-actions github-actions Bot added the size/m label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

112 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

2 participants