Skip to content

fix(security): pass the stack's declared capabilities at every audience-anchor predicate consumer - #18602

Merged
os-justin merged 4 commits into
mainfrom
claude/issue-18535-declared-capabilities-consumers
Sep 17, 2026
Merged

os-justin merged 4 commits into
mainfrom
claude/issue-18535-declared-capabilities-consumers

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

Fixes #18535

ADR-0090 D5 rules the everyone-anchor offending list as 「平台系统权限;带 package provenance 的应用声明 capability 令牌不计」. PR #17811 landed the predicate that implements it — describeHighPrivilegeBits(def, context?) / describeAnchorForbiddenBits(def, anchor, context?), where AnchorBindingContext.declaredCapabilities excuses a systemPermissions name, the platform floor stays absolute and an omitted context refuses — and its own changeset named this follow-up: 「the plugin-security boot refusal and the lint security-anchor-high-privilege rule pass the declared list in a follow-up」. This is that follow-up. packages/spec/** is untouched.

What changed, per site

Premise re-verified on the branch before editing: four consumer sites, none passing a context; declaredCapabilities / AnchorBindingContext in packages/plugins/plugin-security/src + packages/lint/src → 0 hits (control: 3 in high-privilege.ts).

site before after
plugin-security/src/security-plugin.ts (boot bind, bindBaselineToEveryone) const offending = boot ? describeHighPrivilegeBits(boot) : null; :3595 const offending = boot ? describeHighPrivilegeBits(boot, anchorContext) : null; — context read once per pass at :3592
plugin-security/src/security-plugin.ts (engine write gate) const offending = describeAnchorForbiddenBits(boot ?? setDef, positionName as 'everyone' | 'guest'); :5503–:5508 the same call with await declaredCapabilityContext() as the third argument, memoised at :5469
plugin-security/src/suggested-audience-bindings.ts (confirm path) const offending = describeAnchorForbiddenBits(setRow, row.anchor as 'everyone' | 'guest'); :968–:972 the same call with await readDeclaredCapabilityContext(ql, deps.metadata)
lint/src/validate-security-posture.ts (security-anchor-high-privilege) const offending = describeAnchorForbiddenBits(ps, 'everyone'); :795 describeAnchorForbiddenBits(ps, 'everyone', anchorContext), built at :440–:443 from recordsOf(stack.capabilities)

New module: packages/plugins/plugin-security/src/declared-capability-context.ts — readDeclaredCapabilityContext(ql, metadataService), the registry-first / metadata-service-fallback read the sys_capability seeder itself uses, returning undefined when the stack declares nothing.

Where the declared list is read, and why that moment is safe

Boot (the three runtime doors) reads the DECLARATIONS, not the sys_capability rows. The predicate's docblock names the rows at boot; the ordering forbids it, so the card's ruled fallback applies and this is the "say so" half of it.

Ordering evidence, all in security-plugin.ts's runBootstrap:

  • :3878 for (const organizationId of catalogPasses) await bindBaselineToEveryone(organizationId);
  • :3917 const capOutcome = await bootstrapDeclaredCapabilities(ql, this.metadata, …);
  • :3926 await bootstrapSystemCapabilities(ql, …)

The binding runs 39 lines and one awaited pass BEFORE the seeder that writes managed_by:'package' rows, so on a first boot that table is empty at bind time; reading it there would refuse every declared token one layer in. The position is pinned by two other constraints stated in the code at :3866–:3868: the bind MUST follow bootstrapBuiltinRoles (which seeds the everyone anchor) and MUST precede reconcileAudienceBindingSuggestions. Nothing in the boot sequence was reordered.

The same reader serves the engine write gate and confirmAudienceBindingSuggestion on purpose: the confirm check is the friendly early rendition of the gate that re-enforces the predicate on the insert it performs, so a second source there could answer "confirmed" and then have its own write refused under it.

Lint reads the stack's own capabilities: collection through recordsOf(stack.capabilities) — the authoring-time source the predicate's docblock names, indexed by the same helper every other collection in the rule uses. No second declaration source was invented.

Pins (each beside the consumer it guards, three cases per door)

file:line case
packages/plugins/plugin-security/src/security-plugin.test.ts:4372 boot: a declared token BINDS (row asserted, not just a flag)
packages/plugins/plugin-security/src/security-plugin.test.ts:4381 boot: an UNDECLARED token still refuses (declarations present, naming a different capability)
packages/plugins/plugin-security/src/security-plugin.test.ts:4392 boot: a PLATFORM capability still refuses although the stack declares that name
packages/plugins/plugin-security/src/security-plugin.test.ts:4410 write gate: admits the declared token
packages/plugins/plugin-security/src/security-plugin.test.ts:4415 write gate: refuses the undeclared one — code: PERMISSION_DENIED, statusCode: 403 (ADR-0112 envelope), message names the class
packages/plugins/plugin-security/src/security-plugin.test.ts:4426 write gate: refuses the platform capability, same envelope
packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:347 confirm: binds, and the bound row really carries the token
packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:365 confirm: undeclared still refused, suggestion stays pending
packages/plugins/plugin-security/src/suggested-audience-bindings.test.ts:378 confirm: platform capability still refused
packages/lint/src/validate-security-posture.test.ts:457 lint: a declared token lints CLEAN
packages/lint/src/validate-security-posture.test.ts:473 lint: an undeclared token still errors
packages/lint/src/validate-security-posture.test.ts:492 lint: a platform capability still errors

The platform-floor cases reuse high-privilege.ts's own vocabulary (manage_users from PLATFORM_CAPABILITY_NAMES), so the two layers cannot drift. The boot pins drive the METADATA-SERVICE door of the reader and the confirm pins drive the REGISTRY door, so both halves of the fallback are exercised. Three cases per door and not one: "the declared token binds" alone is equally satisfied by a door that stopped judging systemPermissions altogether.

The lint meta-pins (#5017) were visited deliberately rather than silenced: stack.capabilities joined the stack read surface and a cap receiver entry was added against ObjectStackSchema.capabilities[], so the new read is held to the same "reads only keys the spec declares" rule as every other.

Changesets

  • .changeset/18535-anchor-declared-capabilities-consumers.md — @objectstack/plugin-security: minor
  • .changeset/18535-lint-anchor-declared-capabilities.md — @objectstack/lint: minor

minor, not patch: the PR declares Clause-②: yes (widening) and check:changeset-no-major requires at least one moved package at minor or above under that declaration. Both bodies carry the arm and the consumer-facing FROM → TO sentence.

Measurements

Red-then-green, with the control lit. Reverse verification ran from the COMMITTED fix, mutating the four call sites back to their pre-fix argument lists, proving the mutation reached the disk (anchored occurrence counts 1 → 0 for each fixed spelling, plus git diff --stat), and restoring under a trap … EXIT INT TERM with absolute paths. The subjects resolve through src (same-package relative imports), so no dist leg applies.

  • ablated plugin-security (both files): Tests 3 failed | 293 passed — exactly the three accepting pins (binds an isDefault set …, binds the isDefault set …, write gate: admits …)
  • ablated lint: Tests 1 failed | 125 passed — exactly the accepting pin
  • the six refusal controls (undeclared + platform, at each door) stayed GREEN under the ablation, which is what makes the four reds mean the context and not the predicate
  • restore leg proven by blob identity, not by an exit code: git hash-object of each of the three files equals its HEAD blob (3a8fd520…, 30c2ad7c…, f16fb00e…), git status clean, git diff HEAD empty

Suites (merged tree, 1fcf14513):

  • pnpm --filter @objectstack/lint --filter @objectstack/plugin-security test → exit 0 — lint 103 files / 3868 tests, plugin-security 113 files / 2190 tests
  • pnpm --filter @objectstack/lint --filter @objectstack/plugin-security typecheck → exit 0, 0 error TS
  • pnpm lint (repo-wide eslint . --no-inline-config) → exit 0 — the whole population, no narrowing claimed
  • targeted eslint --format json over the 7 changed source files → 7 files, 0 errors, 0 warnings

Derived gates — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, re-derived after the merge: 71 families, all run, reconciled with --ran carrying each exit code → 71 derived, 68 run, 3 NOT-MEASURED, 0 UNRUN. 67 green. The four non-zero:

  • pnpm check:cross-package-test-inputs → exit 1. NOT caused by this diff, proven with a control: at the base commit e0d05538c in a separate worktree the gate exits 0 with no packages/spec/dist/ on disk, and exits 1 with the identical finding the moment one empty packages/spec/dist/security directory exists. The finding names packages/cli/test/init-created-files-summary.e2e.test.ts descending into packages/spec/dist/ — a file this PR does not touch, in a package it does not touch. Reported for filing, not fixed here.
  • pnpm check:dual-build-cjs-loads, pnpm check:i18n, pnpm check:type-check-debt → exit 3, PREREQUISITE NOT MET: each refuses to measure without a full workspace build (53 packages with no dist/). NOT MEASURED locally, not a pass and not a finding; CI builds first and runs them for real.

Three gates DID go red on this diff and were fixed, all in the new boot double: check:engine-double-contract (grown seam counts ratcheted with --write), check:objectql-double-limit (the find double now applies the caller's bound by presence, after the filter) and check:where-matcher (the matcher now REFUSES a $-prefixed combinator instead of comparing it as a field name — the refusal had to live INSIDE the matcher callback, since that gate probes the extracted matcher behaviourally).

Merge: origin/main moved from e0d05538c to b79fae8fb during the work and PR #18503 landed in validate-security-posture.ts. The one conflict was the @objectstack/spec import line; BOTH sides were kept (referenceCarrierOf from /data and describeAnchorForbiddenBits, type AnchorBindingContext from /security), neither dropped, and every measurement above was re-taken on the merged tree.

Note for the contract-tier reviewer (Clause-② yes)

Exactly two accept sets widen, both by the same ruled rule and both only for the everyone anchor:

  1. the runtime anchor-binding accept set (boot bind, engine write gate, suggestion confirm) — a systemPermissions token THIS stack declares under capabilities: no longer counts as a platform system permission;
  2. the lint rule security-anchor-high-privilege's accept set for isDefault: true sets — the same names, at authoring time.

What did NOT move: the platform floor (PLATFORM_CAPABILITY_NAMES is applied inside the predicate, so declaring manage_users launders nothing); undeclared names (still refused everywhere); the guest tier (the predicate drops the context for guest by contract, and no call site overrides that); the VAMA / delete / transfer / bulk-export / wildcard arms of the predicate; the boot sequence's order; and the failure direction when the declarations cannot be read — an unreadable registry, an unreadable metadata service, or an empty list all yield undefined, which is the pre-#17811 verdict verbatim.


Generated by Claude Code

… predicate consumer

WIP: the four consumer sites of describeHighPrivilegeBits /
describeAnchorForbiddenBits now hand over AnchorBindingContext.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
…-predicate consumer

ADR-0090 D5's `everyone`-anchor excusal for app-declared capability tokens
landed as a spec predicate in PR #17811 and no consumer passed it a context, so
a declared token still made an `isDefault` set unbindable at boot, at the engine
write gate, at the suggestion confirm path and in the lint rule.

All four now hand over `AnchorBindingContext.declaredCapabilities`, read from
the stack's `capabilities:` declarations; the platform floor and the
undeclared-name refusal are unchanged, and the `guest` tier is untouched.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
The `find` double added for the #18535 anchor pins refuses a WHERE combinator
it does not implement instead of comparing it as a field name, applies the
caller's bound by presence after the filter, and its grown seam counts are
ratcheted into the engine-double ledger.

Claude-Session: https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu
Co-authored-by: Claude <noreply@anthropic.com>
…clared-capabilities-consumers

# Conflicts:
#	packages/lint/src/validate-security-posture.ts
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/lint, @objectstack/plugin-security, touching 6 documentable anchor(s).

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via security.suggestedBindings.confirm (sdk, the route ledger binds it to POST /api/v1/security/suggested-bindings/:id/confirm, selected by route anchor /security/suggested-bindings/:id/confirm; the route ledger binds it to POST /security/suggested-bindings/:id/confirm))
  • content/docs/permissions/authorization.mdx (via validateSecurityPosture (symbol, a top-level function))
  • content/docs/permissions/permission-sets.mdx (via /security/suggested-bindings/:id/confirm (route, bridged from symbol confirmAudienceBindingSuggestion — its route source's handler names it))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v13.mdx (via validateSecurityPosture (symbol, a top-level function))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ad067addec3731c4da61fe1de75d2212029fa8ef → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 46575539988c9e11bebd9c978eb568b335967589 — the merge of head 1fcf14513ca02114214c315e9c59e1f8693e6cfa into base ad067addec3731c4da61fe1de75d2212029fa8ef, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 46575539988c9e11bebd9c978eb568b335967589 && git checkout 46575539988c9e11bebd9c978eb568b335967589
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ad067addec3731c4da61fe1de75d2212029fa8ef 1fcf14513ca02114214c315e9c59e1f8693e6cfa && git checkout -B drift-repro ad067addec3731c4da61fe1de75d2212029fa8ef && git merge --no-ff 1fcf14513ca02114214c315e9c59e1f8693e6cfa

node scripts/docs-audit/affected-docs.mjs --json ad067addec3731c4da61fe1de75d2212029fa8ef

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ad067addec3731c4da61fe1de75d2212029fa8ef → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1fcf14513ca02114214c315e9c59e1f8693e6cfa

In-seat review by the dispatching domain:skills seat (the blocked seat that took #18535 as the ownerless blocker of #17359), 2026-09-17T08:24Z, read against the PR's diff, files and head on GitHub and the fetched branch — ⛔ not against the dev report (5711251082). Clause-②: yes on both carriers; needs:contract-review hung on both at 2026-09-17T08:22Z (the seat's omission at claim time, repaired before this record) and stripped by this PASS in the same act.

① Derived judgments

  • The accept set widens exactly to the ruled set, at four doors, from one source. ADR-0090 D5 (「平台系统权限;带 package provenance 的应用声明 capability 令牌不计」) is implemented by PR feat(spec): an app-declared capability token is not a platform system permission at the everyone anchor #17811's predicate with context?.declaredCapabilities; this PR hands that context to every consumer that lacked it — bindBaselineToEveryone (describeHighPrivilegeBits(boot, anchorContext), read once per pass), the engine write gate on sys_position_permission_set (describeAnchorForbiddenBits(boot ?? setDef, positionName, await declaredCapabilityContext()), memoised, read only once an anchor row is in play), confirmAudienceBindingSuggestion (the same call with readDeclaredCapabilityContext(ql, deps.metadata)) and the lint rule security-anchor-high-privilege (describeAnchorForbiddenBits(ps, 'everyone', anchorContext) from recordsOf(stack.capabilities)). Correct: the pre-fix reading (0 hits of declaredCapabilities / AnchorBindingContext in the two packages' src) reproduced on the base.
  • What does not move, verified at the predicate, not in the PR's prose. The platform floor is applied inside describeHighPrivilegeBits (PLATFORM_CAPABILITY_NAMES), so declaring a platform name excuses nothing; describeAnchorForbiddenBits drops the context for guest (high-privilege.ts :195 「anchor === 'guest' ? undefined : context」), so the strictest tier is untouched; an undeclared name and an empty / unreadable declaration list yield undefined ⇒ the pre-feat(spec): an app-declared capability token is not a platform system permission at the everyone anchor #17811 verdict (omission refuses). The new reader declared-capability-context.ts fails closed at every step and never derives the list from the set under test.
  • The boot source is the declarations, and the ordering evidence holds on the head: bootstrapBuiltinRoles :3572 → bindBaselineToEveryone :3639 / :3888 → reconcileAudienceBindingSuggestions :3742 / :3905 → bootstrapDeclaredCapabilities :3927 → bootstrapSystemCapabilities :3936. The sys_capability rows with managed_by:'package' do not exist at bind time on a first boot; the card's ruled fallback (take the metadata declarations and say so) is taken and said in the module docblock, the call-site comment and the PR body. The reader is the seeder's own two-step (readDeclared(ql, 'capability') — the same call bootstrap-declared-capabilities.ts :460 makes — then the metadata service). No boot reordering; packages/spec/**, ADRs and skills/** untouched.
  • Pins in place, where the fork is caught: 12 cases across the three existing test files — declared binds / lints clean, undeclared still refuses, platform still refuses, at boot, at the write gate (with the ADR-0112 envelope), at confirm and at lint; the platform-floor cases reuse high-privilege.ts's vocabulary. The dev's ablation (four call sites reverted from the committed fix) turned exactly the four accepting pins red and left the six refusal controls green — the reds measure the context, not the predicate.
  • Merge with PR Retire check-reference-carrier-shape; refuse an unreadable reference carrier at the reader #18503 on validate-security-posture.ts: one conflict on the @objectstack/spec import line, both sides kept (referenceCarrierOf from /data; describeAnchorForbiddenBits + type AnchorBindingContext from /security) — read on the head, correct.
  • scripts/engine-double-contract.pinned.json +2 / −2: the seam counts of the new boot double in security-plugin.test.ts (findOne 5 → 6, update 1 → 2) re-pinned by the gate's own --write, not by hand; the two other doubles-gates that went red (check:objectql-double-limit, check:where-matcher) were fixed in the new double itself. Accepted.

② Semver level

minor for both @objectstack/plugin-security and @objectstack/lint, changesets named 18535-*.md, each carrying Clause-②: yes (widening) and the consumer-facing FROM/TO sentence — consistent with check-changeset-no-major (a PR declaring clause ② moves at least one package at minor) and with the launch-window level policy.

③ Boundary flags

Implemented-by: claude/issue-18535-declared-capabilities-consumers
Reviewed-by: session_01Gqi43smmqjJ5sUrhfoPeKu

VERDICT: PASS


Generated by Claude Code

@os-justin
os-justin marked this pull request as ready for review September 17, 2026 08:41
@os-justin
os-justin enabled auto-merge September 17, 2026 08:42
@os-justin
os-justin added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 21b7c12 Sep 17, 2026
43 checks passed
@os-justin
os-justin deleted the claude/issue-18535-declared-capabilities-consumers branch September 17, 2026 09:00
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…ility rule in objectstack-data security.md (objectstack-ai#18531)

Fixes objectstack-ai#17359

**Status 2026-09-17 — the paragraph below is historical and
discharged**: the follow-up it names landed as objectstack-ai#18535 (PR objectstack-ai#18602,
`origin/main` `21b7c12b4`), the ceiling was ruled, and this branch
merged `origin/main`; see "Patch round" at the end.

⛔ **Blocked-by: the objectstack-ai#17189 step-② follow-up (the `plugin-security` boot
refusal and the `packages/lint` `security-anchor-high-privilege` rule
passing the declared-capability list) — not landed on `origin/main`
`2496415`, and no open card names it.** Do not land this PR before that
follow-up: today the bullet's second sentence describes the protocol
(`packages/spec/src/security/high-privilege.ts` + the ADR-0090 D5
revision, landed by PR objectstack-ai#17814 and PR objectstack-ai#17811) and not yet the running
lint/boot — see "Premise check" below.

## What

One 3-line bullet in `skills/objectstack-data/rules/security.md`,
inserted after the `Source:` line of "## Object-level permissions
(RBAC)" — the construct accepted on the card (assessment 5616225081,
ACCEPT 5616301504) with the ACCEPT's boot-wording correction applied
(「fails lint and boot」 overstated the boot side; boot refuses the
binding with a warning, it does not fail). Every other line of the file
is byte-identical (`git diff --numstat` = `3 0`);
`skills/objectstack-platform/SKILL.md` unchanged; no other file.

Lines 35–37, widths 81 / 82 / 91 characters (the file's widest line is
93):

```markdown
- **`isDefault: true` = the `everyone` baseline (ADR-0090 D5).** It may carry app
  capabilities declared under `capabilities:` (`defineCapability`) and granted via
  `systemPermissions`; lint and boot refuse a platform capability or undeclared name there.
```

Wording deviation, declared: the dispatch's suggested passive form 「is
refused by lint and at boot」 puts line 3 at 101 characters, over the 93
cap; the active form above keeps every noun of the accepted text and the
ACCEPT's own verb ("refuse") at 91. No other word moved.

## Premise check on `origin/main` `2496415`

- **A1 holds**: `security.md` is 211 lines, widest line 93, lines 31–34
verbatim as quoted in the assessment. Its last touch is `7d350a4`
(objectstack-ai#17476, the Multi-tenancy section), not `6a3bcd8` as the dispatch read;
the neighbours are unaffected.
- **A2 holds in substance**: `git grep -F` over `skills/**` at `2496415`
— `defineCapability` 0, `systemPermissions` 0, `isDefault` 1 (a
list-view example), `capabilities:` 5 — and all five are the data-hook
VM tokens (`capabilities: ['api.read', …]` in
`objectstack-data/references/data-hooks.md` ×4 and
`objectstack-ui/rules/actions.md` ×1), a third registry, not the
ADR-0066 D1 stack key. Positive control `definePermissionSet` = 5 hits.
The bullet duplicates nothing.
- **A3 holds, and the boot side is narrower than the dispatch assumed**:
`bindBaselineToEveryone`
(`packages/plugins/plugin-security/src/security-plugin.ts:3581`) logs
`ctx.logger.warn('[security] refusing to bind fallback set to everyone —
high-privilege bits', { set, offending })` and `continue`s — a warning
and no binding, never a failure. ⚠️ Its call is
`describeHighPrivilegeBits(boot)` (:3585) with NO
`AnchorBindingContext`, and so are the other three consumers:
`security-plugin.ts:5475`, `suggested-audience-bindings.ts:961`,
`packages/lint/src/validate-security-posture.ts:771`. The predicate's
own contract (`high-privilege.ts:134`): 「Omission refuses」. PR objectstack-ai#17811's
changeset says it in so many words: 「No shipped behaviour moves in this
release. Every current caller invokes the predicates with the old arity
… The `@objectstack/plugin-security` boot refusal and the
`@objectstack/lint` `security-anchor-high-privilege` rule pass the
declared list in a follow-up」. ⇒ Today a DECLARED app token on the
`isDefault: true` set is still refused by lint (error) and at boot
(warn, no binding) — the shape hotclm hit. The bullet is correct for the
protocol and premature for the runtime; per 「文档应该以实际实现为准」 this PR waits
for the follow-up. The 「or undeclared name」 clause stays: the implicit
placeholder derivation in `bootstrap-declared-capabilities.ts` is alive,
and `describeHighPrivilegeBits` excuses only names on
`context.declaredCapabilities`, platform floor absolute.
- **Keys and anchor, all present**: `capabilities` on
`ObjectStackDefinitionSchema` (`packages/spec/src/stack.zod.ts:467`,
`.describe('[ADR-0066 D1] …')`); `defineCapability`
(`packages/spec/src/security/capabilities.ts:214`); `systemPermissions`
on `PermissionSetSchema`
(`packages/spec/src/security/permission.zod.ts:691`, schema at :587);
`isDefault` at :682 with `.describe('[ADR-0090 D5] App baseline for the
everyone position …')`; ADR-0090 D5 at
`docs/adr/0090-permission-model-v2-concept-convergence.md:46`, its
objectstack-ai#17189 revision block at :249. Control: a nonexistent key greps 0 in
`permission.zod.ts`.

## Line readings (the `skills/**` rule)

| reading | before (`2496415`) | after (`4ea43892e`) | net |
|:--|--:|--:|--:|
| `skills/objectstack-data/rules/security.md` | 211 | 214 | +3 |
| package: every `SKILL.md` under `skills/` (10) +
`skills/objectstack-data/{rules,references}/*` (10) — 20 files | 8959 |
8962 | +3 |

Token reading (the sister gate `scripts/check-skills-token-ratchet.mjs`,
convention ceil(utf8 bytes / 4)): `security.md` is **2543** tokens
against a ceiling of **2480** (`CEILINGS` row at
`scripts/check-skills-token-ratchet.mjs:424`, pinned at the landed count
with zero headroom) — **over by 63, the gate is RED at this head**.
Verdict line, verbatim: 「✗ check-skills-token-ratchet:
skills/objectstack-data/rules/security.md is 2543 tokens; the ratchet
ceiling is 2480 (over by 63). … The other direction lands only in a PR
whose body quotes a maintainer ruling authorizing it. ⛔
MAINTAINER-ONLY」. Not raised here and not paid by deletion (the accepted
construct pins every other line of the file). Landing needs that one row
moved to 2543 under the maintainer's word, or an equivalent deletion in
the same file directed by the PM.

## Changeset

`skip-changeset`, by measurement: a walk over every tracked
`package.json` finds **0** manifests whose `files[]` names a `skills`
path (positive control: 70 manifests name `dist`); no
`skills/*/package.json` exists; the catalog ships from the GitHub tree
by `npx skills add objectstack-ai/objectstack/skills`
(`skills/README.md`;
`packages/create-objectstack/src/skills-install.ts:62` `SKILLS_CATALOG =
'objectstack-ai/objectstack/skills'`), never inside an npm tarball.
`.github/workflows/pr-automation.yml:758` lists `skills/` among the
releases-nothing paths.

## Gates (local, at `4ea43892e`)

`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 22 commands; every one was run with
redirect-then-`$?` capture; `--ran` reconcile: 「22 derived, 22 run, 0
NOT-MEASURED, 0 UNRUN」, exit 0.

- 21 × exit 0 — among them `check:skill-frame-sync`,
`check:skill-identifier-liveness`, `check:skill-compatibility`,
`check:nul-bytes`, `check:doc-authoring`, `check:role-word`,
`check:corpus-claim-drift`, `check:cross-package-test-inputs`,
`check:pm-governed-merges`, `check-skills-token-ratchet --self-test`.
- `node scripts/check-skills-token-ratchet.mjs` → **exit 1** (the +63
above).
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions` →
first run exit 3 `PREREQUISITE NOT MET` (`@objectstack/formula` and
`@objectstack/lint` unbuilt — not a measurement); built both under
`scripts/pm/os-verify-lock.sh` (`VERDICT command-exit 0`, held 142s),
re-run → exit 0.
- Not applicable: the 14 pending-changeset families (no changeset, by
the measurement above); the 2 workflow-valued families and the 1
path-scheduled CI job are CI's own.
- No ① dependency-closure build and no ② package test: the diff touches
no package.
- Control-character self-scan of the file: none.

## Acceptance notes

- noted, not filed — 承接者: the PM, relayed in the dev report as a class
(b) contract finding with dedupe words: ADR-0090 D5 [ruled] and
`describeHighPrivilegeBits` say a declared app token is not an offending
bit, while the four consumers listed under A3 still refuse it. PR
objectstack-ai#17811's changeset promises the follow-up; no open card in
`domain:services` (48), `domain:devx` (103), `domain:spec` (144),
`security` (19) or `finding` (78) names it (lists read 2026-09-16,
keyword set `17189 / bindBaselineToEveryone / declaredCapabilities /
AnchorBindingContext / security-anchor-high-privilege`).
- noted, not filed — 承接者: 无: with this bullet the word "capabilities"
names three registries across the published bundle (data-hook VM tokens,
`requires:` platform service tokens, ADR-0066 D1 stack declarations).
Not a defect; the assessment placed the stack key beside its GRANT key
on purpose.

## 维护者速读(草稿)

**改了什么**:在 `skills/objectstack-data/rules/security.md` 的「Object-level
permissions (RBAC)」键列表里加一条三行要点:`isDefault: true` 的权限集就是 `everyone`
基线(ADR-0090 D5);它可以携带本应用在 `capabilities:` 下用 `defineCapability` 声明、再经
`systemPermissions` 授予的应用能力;平台能力或未声明的名字放在那里会被 lint
与启动拒绝。文件其余各行一字未动,`skills/objectstack-platform/SKILL.md` 不动。

2026-09-17 补丁轮:合入 `origin/main`(合并提交 `68e1b07e1`;objectstack-ai#18535 已落地,这三行描述的 lint
与启动行为已成真,三行本身一字未改),并按维护者裁定把 `scripts/check-skills-token-ratchet.mjs` 里
`security.md` 的 token 上限行从 2480 抬到
2543,上限行旁按该文件自己的抬限格式逐字引用裁定「security.md 允许增加到 2543」;合并后实测恰为
2543(`ceil(utf8 bytes / 4)`),余量 0,其它上限行不动。

**为什么改**:objectstack-ai#17189 裁定 (i) 之后,「默认权限集携带本应用自己的门牌令牌」这一组合从被拒变为合法(前提是先声明),而已发布的
skills 里没有任何一处写到这三个键;AI 作者照 schema 直接写 `systemPermissions` 就会写出 hotclm
踩过的那种形状。已接受的评估(5616225081)裁定只加这一条、不加反例、`+3` 行。

**风险与代价(含回滚)**:① 时序——objectstack-ai#17811 只落了协议这一半(spec 谓词 + ADR 修订),启动侧与 lint
侧尚未把声明清单传给谓词,今天照这条要点写出的默认集仍会被 lint 报错、启动只警告不绑定;所以本 PR ⛔ 不应先于那一半落地(正文顶部已标
Blocked-by)。② 已发布 skills 的 token 棘轮:`security.md` 上限 2480、余量 0,本次 +63 使
`check-skills-token-ratchet`
变红;上限行(`scripts/check-skills-token-ratchet.mjs:424`)按门禁自述只有维护者裁定可抬,本 PR
未抬。回滚 = revert 这一个提交(单文件 +3 行,无发布物)。

2026-09-17 更新:① 已解除——objectstack-ai#18535 落地,启动侧与 lint 侧都已把声明清单传给谓词;② 已解除——维护者裁定抬到
2543,本 PR 抬行,`check-skills-token-ratchet` 在新 head 上为绿。回滚 = revert
两个非合并提交(`4ea43892` 三行 + `dfe355143` 上限行),仍无发布物。

**席位意见**:

**你要做的**:① 确认 objectstack-ai#17189 第 ② 步(`plugin-security` 启动拒绝 + `packages/lint`
规则传入声明清单)是否已有卡;没有则立卡,并让本 PR 排在它之后合并。② 决定 token 上限:把 `security.md` 那一行抬到
2543(在本 PR 正文引用你的裁定),或指示在同一文件删等量内容(这会动已接受评估钉死的其他行)。③ 之后由 skills
席四件套复核,你点合并。

2026-09-17 更新:① ② 已完成(objectstack-ai#18535 落地;上限已按你的裁定抬到 2543 并在正文与上限行旁引用),只剩 ③。

## Patch round — merge main + ceiling raise (2026-09-17)

Both park conditions discharged (park note 5704585750, update
5710537792, unpark 5711767613). The `Blocked-by:` paragraph at the top
of this body is historical: objectstack-ai#18535 landed on `origin/main` as
`21b7c12b4` (PR objectstack-ai#18602). Same branch, commits added on top — no rebase,
no force-push, no new PR.

**Merge**: `68e1b07e1` = `git merge --no-ff origin/main` (`21b7c12b4`)
into the branch. Clean (`git merge-tree --write-tree` exit 0; 37 commits
behind at merge time); no `os-regen-pending` recorded (the branch
touches no generated artifact). Three-dot delta vs `origin/main` after
the round: `skills/objectstack-data/rules/security.md` +3/−0
(byte-identical to `4ea43892`), `scripts/check-skills-token-ratchet.mjs`
+10/−1.

**Re-count** (the ratchet's convention, `ceil(utf8 bytes / 4)`):
`security.md` on `origin/main` `21b7c12b4` = 9913 bytes → 2479 tokens
(main did not touch the file since the branch forked); branch head
before the round `4ea43892` = 10170 bytes → 2543; after the merge
(`68e1b07e1`, and the head `dfe355143`) = 10170 bytes → **2543**. Not
more than 2543, so the raise is exactly the ruling. Line readings
unchanged: file 211 → 214 (+3), widest line 93, package (20 files) 8959
→ 8962.

**Ratchet row** (`CEILINGS` in
`scripts/check-skills-token-ratchet.mjs`):
- before: `['skills/objectstack-data/rules/security.md', 2480],` (line
424 on `21b7c12b4`)
- after: `['skills/objectstack-data/rules/security.md', 2543],` (line
433 on `dfe355143`), with the raise recorded beside the row in the
file's own raise-ritual form (before → after, the surface it authorizes,
the arithmetic +63 / headroom 0 / ceiling +63, the ruling verbatim with
its record id). No other row moves; the ceiling equals the measurement.

**Maintainer ruling**, verbatim and untranslated (maintainer,
2026-09-17, recorded on objectstack-ai#17359 as comment 5710537499):

> 「security.md 允许增加到 2543」

That is the authorization the ratchet's own rule requires: 「the other
direction lands only in a PR whose body quotes a maintainer ruling
authorizing it」.

**Re-read of the three lines against the landed code** (`origin/main`
`21b7c12b4`):

| clause | verdict | evidence |
|:--|:--|:--|
| `isDefault: true` = the `everyone` baseline (ADR-0090 D5) | still true
| `packages/spec/src/security/permission.zod.ts:682`
`.describe('[ADR-0090 D5] App baseline for the everyone position …')`;
ADR-0090 D5 at
`docs/adr/0090-permission-model-v2-concept-convergence.md:46` |
| it may carry app capabilities declared under `capabilities:`
(`defineCapability`) and granted via `systemPermissions` | now true at
boot and in lint (protocol-only before objectstack-ai#18535) |
`packages/plugins/plugin-security/src/declared-capability-context.ts`
(`readDeclaredCapabilityContext` reads the stack's `capabilities:`
declarations, registry first, metadata service as fallback); boot bind
`security-plugin.ts:3592–3595` `describeHighPrivilegeBits(boot,
anchorContext)`; engine write gate `:5503–5508`; confirm path
`suggested-audience-bindings.ts:968–972`; lint
`packages/lint/src/validate-security-posture.ts:439–443` builds
`anchorContext` from `stack.capabilities`, `:795` passes it to
`describeAnchorForbiddenBits` |
| lint and boot refuse a platform capability or undeclared name there |
still true | `packages/spec/src/security/high-privilege.ts:70–74` the
platform floor (`PLATFORM_CAPABILITY_NAMES` is never excused), `:134`
「Omission refuses」 — a token absent from `declaredCapabilities` stays
offending; lint emits `security-anchor-high-privilege` at severity
`error` (`validate-security-posture.ts:795–808`); boot logs `[security]
refusing to bind fallback set to everyone — high-privilege bits` and
skips the binding (`security-plugin.ts:3595–3600`) |

No clause corrected; the three lines are unchanged.

**Ablation of the row** (from the committed head `dfe355143`;
trap-restored, absolute paths; no `dist/` involved — the gate reads its
own source): with the row reverted to 2480 on disk (grep counts:
2480-row 1, 2543-row 0) the gate exits 1 — 「✗
check-skills-token-ratchet: skills/objectstack-data/rules/security.md is
2543 tokens; the ratchet ceiling is 2480 (over by 63). … ⛔
MAINTAINER-ONLY」; restored with `git checkout HEAD -- …`: `git
hash-object` = HEAD blob `5b984866`, `git diff HEAD` empty, `git status
--porcelain` empty, gate exits 0 again with 「is 2543 tokens (ceiling
2543; headroom 0)」.

**Gates** (worktree at `dfe355143`; `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`, change set derived from
the merge base `21b7c12b4`, 2 paths): 40 commands derived, all 40 run
with redirect-then-`$?` capture; `--ran` reconcile: 「40 derived, 40 run,
0 NOT-MEASURED, 0 UNRUN」, exit 0. `pnpm --filter @objectstack/lint run
check:doc-formula-expressions` measured after building
`@objectstack/lint...` under `scripts/pm/os-verify-lock.sh` (「VERDICT
command-exit 0 · held the lock 136s · waited 0s」). `pnpm
check:pm-dispatch-gates` took 626 s under contention. The list,
byte-for-byte as derived, with exit codes:

```text
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-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/pm/bare-root-worklist.mjs --self-test :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: 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:entry-guard :: 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:pnpm-filter-targets :: 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:watch-hint-literal :: exit 0
```

Named verdict lines: `check-skills-token-ratchet` 「✓ … security.md is
2543 tokens (ceiling 2543; headroom 0)」 and 「34 authored bundle file(s)
within their ceilings; 10 generator-owned file(s) measured, not
ratcheted」; its `--self-test` 「65 cases pass」;
`check-ratchet-remedy-authority` 「255 scripts swept … 15 mark the
expanding remedy ⛔ MAINTAINER-ONLY」 (unchanged);
`check-skill-frame-sync` self-test 14 cases + the frame coherent. Not
owed locally: the 51 artifact-roster, 11 wide-population and 14
pending-changeset families, the path-scheduled CI job and the
always-runs tail (CI's own); no ① dependency-closure build or ② package
test is owed — the diff touches no package.

**Governed**: `node scripts/pm/check-governed-merges.mjs --test
skills/objectstack-data/rules/security.md
scripts/check-skills-token-ratchet.mjs` → exit 3, 「GOVERNED — a human
merge is the review record for this PR」 (1 of 2 paths hit the register;
`skills/**` is the rules layer). Still draft; not for any seat to land.

**Changeset**: still `skip-changeset` —
`scripts/check-skills-token-ratchet.mjs` is a repo-root gate script
(private root package; no `files[]` ships it), and the `skills/**`
measurement above stands. Label set read back after the round:
`documentation`, `size/s`, `skip-changeset` (no label written this
round).

**Control characters**: `grep -naP` over both touched files → no match
(exit 1).

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Gqi43smmqjJ5sUrhfoPeKu)_

---
_Generated by [Claude Code](https://claude.ai/code)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…lity DECLARATIONS, not the not-yet-seeded sys_capability rows (objectstack-ai#18767)

Clause-②: no

Fixes objectstack-ai#18603

Comment text only, in one file: the `AnchorBindingContext` docblock in
`packages/spec/src/security/high-privilege.ts`. No predicate, type,
export or accept set moves.

## What the sentence said, and why a literal follower is refused

The docblock named two sources for `declaredCapabilities`: at boot 「the
`sys_capability` rows carrying `managed_by: 'package'` provenance」, at
authoring time the stack's own `capabilities` array. The boot half
carried an ordering precondition the sentence never stated. The ADR-0090
D5 anchor binding runs BEFORE the seeder that writes those rows, so on a
first boot the table is empty at exactly the moment the docblock said to
read it — and this docblock's own 「omission refuses」 property then turns
that emptiness into a silent refusal of every declared token: the app's
own `isDefault` set unbindable at the `everyone` anchor, which is the
defect objectstack-ai#17811 introduced the input to remove, reproduced one layer in.

The boot half now names the DECLARATIONS, read through the seeder's own
two-step — the ObjectQL registry first, the metadata service as the
fallback — which is exactly what `readDeclaredCapabilityContext`
(`@objectstack/plugin-security`, PR objectstack-ai#18602) already implements, so the
contract text and its one runtime consumer corroborate each other
instead of contradicting. The `sys_capability` rows stay a valid source,
qualified: only once the seeder has written them.

## LIT — the ordering was READ, by symbol, on this branch's base

The card's line numbers were taken on PR objectstack-ai#18602's head and were carried
forward unverified. They were re-derived here by SYMBOL on `origin/main`
`95b21b33be` (this branch's merge base),
`packages/plugins/plugin-security/src/security-plugin.ts`:

| symbol | line | inside |
| :-- | :-- | :-- |
| `const runBootstrap` | `:3655` | the boot sweep itself |
| `await seedCatalogBuiltins(...)` | `:3866` | `runBootstrap` — reaches
`bootstrapBuiltinRoles` at `:3572` (defined in `seedCatalogBuiltins`,
`:3570`), which seeds the `everyone` anchor |
| `await bindBaselineToEveryone(...)` | `:3888` | `runBootstrap` — the
ADR-0090 D5 bind; defined at `:3583`, consults
`describeHighPrivilegeBits` at `:3595` |
| `await reconcileAudienceBindingSuggestions(...)` | `:3905` |
`runBootstrap` |
| `await bootstrapDeclaredCapabilities(...)` | `:3927` | `runBootstrap`
— the seeder that WRITES the `managed_by: 'package'` rows |

`:3888` and `:3927` sit in one straight-line `try` body of one function
with no branch between them, so the bind precedes the seeder. **The
card's conclusion holds.** Three line attributions in the card's table
are worth correcting for the next reader, and none of them moves the
conclusion:

- `:3572` is `bootstrapBuiltinRoles`'s call site inside the helper
`seedCatalogBuiltins` (`:3570`), not a line of `runBootstrap`;
`runBootstrap` reaches it at `:3866`.
- `:3639` is a SECOND `bindBaselineToEveryone` call, inside
`seedCatalogForOrganization` (`:3635`) — the organization-creation hook,
not the boot sweep. Only `:3888` is `runBootstrap`'s.
- `:3742` is `reconcileAudienceBindingSuggestions` inside the
publish-materializer callback `runBootstrap` registers — a runtime
publish path, not a boot step. The boot step is `:3905`.

## DARK — a reading that must be ZERO, with a control proving it fires

Predicate: take `git diff -U0` over
`packages/spec/src/security/high-privilege.ts`, keep the `+`/`-` lines
that are not the `+++`/`---` headers, and drop every one that is blank
or begins with `*`, `//` or `/*`. What remains is CODE.

| leg | input | reading |
| :-- | :-- | :-- |
| this change | `git diff -U0 95b21b3 HEAD --
packages/spec/src/security/high-privilege.ts` |
`NON_COMMENT_CHANGED_LINES=0` |
| control | the same file's own `d5c91dd681` (objectstack-ai#17811), same predicate,
same input shape | `NON_COMMENT_CHANGED_LINES=33` — it names the added
`import`, the `export interface AnchorBindingContext`, its member and
the whole of `appDeclaredCapabilityNames` |

The zero is a measurement, not an absence of input: the same instrument
reads 33 on a real code change to the same file. `git diff --stat` for
this change is 17 insertions / 2 deletions, all of them comment.

## Changeset — measured, not assumed

`skip-changeset` would be wrong: published content moves.

- `packages/spec/src/security/high-privilege.ts` is NOT shipped as
source. `@objectstack/spec`'s `files[]` takes `src/**/*.zod.ts` and this
file is not one — `npm pack --dry-run --json` lists 2021 shipped paths
and does not include it, with the sibling
`src/security/permission.zod.ts` present in the same listing as the lit
control.
- Its published reach is the EMITTED declarations, and they move. After
`pnpm --filter @objectstack/spec build`, the new clause is present in
`dist/security/index.d.ts` and `dist/security/index.d.mts` — both in
that same shipped listing — the superseded spelling is absent from every
built declaration file (0 files), and the docblock's unchanged
neighbouring sentence (「Never synthesize this from the set under test」)
is present in the same two files as the lit control.

Hence `.changeset/18603-anchor-binding-declared-capabilities.md`,
`@objectstack/spec: patch`.

## Verification, at `2387ad9a5c`

- `pnpm --filter @objectstack/spec build` — green.
- `pnpm --filter @objectstack/spec test` — 486 test files, 14017 tests,
all passed.
- `pnpm --filter @objectstack/spec typecheck` — green.
- `pnpm --filter @objectstack/spec check:generated` — all 15 generated
artifacts up to date; nothing needed regenerating.
- `pnpm build` — 73/73 tasks successful.
- `pnpm lint` (`eslint . --no-inline-config`, the repo-wide population)
— green, exit 0.
- The gate families derived by `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack`: all 75 run, all exit 0,
reconciled with `--ran` (75 derived / 75 run / 0 NOT-MEASURED, derived
from recorded exit codes). Three of them
(`check:doc-formula-expressions`, `check:dual-build-cjs-loads`,
`check:lean-entry-closure`) first answered `exit 3` PREREQUISITE NOT MET
on an unbuilt tree, which is not a finding; they were re-run green after
`pnpm build`.

## Acceptance notes

Nothing filable was found alongside this change. The three
line-attribution corrections above are reported here rather than filed:
they are a nuance in a card's evidence table, not a defect in the code,
and the ordering they describe is correct.

Landing is the owning seat's — left as a draft, auto-merge not armed.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/l tests tooling

Projects

None yet

2 participants