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:

CheckResult
pnpm --filter @objectstack/spec test346 files / 8848 tests passed
pnpm --filter @objectstack/spec typecheckpass (tsc --noEmit + check:scripts-typecheck + check:test-typecheck; the new pin compiles clean, no debt-ledger entry)
pnpm --filter @objectstack/spec check:generatedall 10 artifacts up to date, no regeneration
pnpm lint (the required ESLint job)exit 0
pnpm check:nul-bytesOK — 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-countspass

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

vercelBot commented Aug 8, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectstackIgnoredIgnoredAug 8, 2026 2:26pm

Request Review

@github-actions

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-project-manager@claude