From 720b8362692d3bb6a3e000f570f664a11c739fdc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 19:29:22 +0000 Subject: [PATCH 1/2] docs(spec): AnchorBindingContext's boot half names the stack's capability DECLARATIONS, not the not-yet-seeded sys_capability rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 has an ordering precondition the sentence never stated. The ADR-0090 D5 anchor binding (`bindBaselineToEveryone`) runs BEFORE `bootstrapDeclaredCapabilities`, 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 defect #18535 removed, 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) — the same source the authoring-time half already named, and the read `readDeclaredCapabilityContext` in `@objectstack/plugin-security` already implements. The `sys_capability` rows stay a valid source, qualified: only once the seeder has written them. Comment text only. No predicate, type, export or behaviour changes. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- packages/spec/src/security/high-privilege.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/security/high-privilege.ts b/packages/spec/src/security/high-privilege.ts index cf047ce9a1..07b0383d7a 100644 --- a/packages/spec/src/security/high-privilege.ts +++ b/packages/spec/src/security/high-privilege.ts @@ -31,8 +31,23 @@ function coerceRecord(v: unknown): Record | undefined { * The predicates are pure and synchronous — they read one permission-set * definition and nothing else — so the one fact they cannot discover for * themselves is which capability names this stack DECLARED. The caller holds - * it: at boot from the `sys_capability` rows carrying `managed_by: 'package'` - * provenance, at authoring time from the stack's own `capabilities` array. + * it: at boot from the stack's own `capabilities:` DECLARATIONS — and from + * the `sys_capability` rows carrying `managed_by: 'package'` provenance only + * ONCE THE SEEDER HAS WRITTEN THEM — at authoring time from the stack's own + * `capabilities` array. + * + * ⚠️ [#18603] Why the boot half names the DECLARATIONS and not the rows: the + * ADR-0090 D5 anchor binding (`bindBaselineToEveryone` in + * `@objectstack/plugin-security`) runs BEFORE `bootstrapDeclaredCapabilities`, + * the seeder that WRITES those `managed_by: 'package'` rows, and that order is + * fixed by two other constraints — the binding must follow the seeding of the + * `everyone` anchor it binds to, and precede the audience-binding suggestion + * reconciliation. On a first boot the table is therefore EMPTY at the bind + * moment, and "omission refuses" below turns that emptiness into a silent + * refusal of every declared token. A boot caller reads the declarations + * through the seeder's own two-step — the ObjectQL registry first, the + * metadata service as the fallback — as `readDeclaredCapabilityContext` + * (`@objectstack/plugin-security`) does. * * ⛔ Never synthesize this from the set under test. The point of the input is * that a set cannot vouch for its own tokens; a "declared" list derived from From 2387ad9a5cebb841bbeb29f50d44a124894234e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 19:47:00 +0000 Subject: [PATCH 2/2] chore(changeset): patch @objectstack/spec for the AnchorBindingContext boot-source clause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docblock's published reach is the emitted declarations, not the source: `src/security/high-privilege.ts` is not a `.zod.ts`, so `files[]` does not ship it, but `dist/security/index.d.ts` and `dist/security/index.d.mts` are shipped and carry the TSDoc — measured with `npm pack --dry-run`. Published content moves, so this is a `patch`, not `skip-changeset`. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- ...03-anchor-binding-declared-capabilities.md | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .changeset/18603-anchor-binding-declared-capabilities.md diff --git a/.changeset/18603-anchor-binding-declared-capabilities.md b/.changeset/18603-anchor-binding-declared-capabilities.md new file mode 100644 index 0000000000..5c8424209e --- /dev/null +++ b/.changeset/18603-anchor-binding-declared-capabilities.md @@ -0,0 +1,47 @@ +--- +'@objectstack/spec': patch +--- + +`AnchorBindingContext`'s boot half names the stack's capability DECLARATIONS, not the `sys_capability` rows the seeder has not written yet + +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, and a caller following it literally +lands on the defect the input exists to remove. + +`runBootstrap` (`@objectstack/plugin-security`) awaits `bindBaselineToEveryone` +— the ADR-0090 D5 anchor binding, the boot call site that consults +`describeHighPrivilegeBits` — BEFORE it calls `bootstrapDeclaredCapabilities`, +the seeder that WRITES those `managed_by: 'package'` rows. The order is fixed by +two other constraints stated at that call site: the binding must follow the +seeding of the `everyone` anchor it binds to, and precede the audience-binding +suggestion reconciliation. 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 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 #17811 introduced the input to remove. + +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 +what `readDeclaredCapabilityContext` (`@objectstack/plugin-security`, #18535) +already implements, so the contract text and its one runtime consumer now +corroborate each other instead of contradicting. The `sys_capability` rows stay +a valid source, qualified: only once the seeder has written them, which is where +an admin-surface or post-boot caller reads them. + +⛔ No behaviour changes. The diff is comment text: `git diff` against the branch +point over `src/security/high-privilege.ts` changes **0** non-comment lines (the +same predicate reads 33 on that file's own #17811 commit, which is the control +proving it fires). No predicate, no type, no export, no accept set moves. + +**This is shipped, which is why it carries a changeset rather than +`skip-changeset`.** `src/security/high-privilege.ts` is NOT shipped as source — +`@objectstack/spec`'s published `files[]` takes `src/**/*.zod.ts`, and this file +is not one (`npm pack --dry-run` lists 2021 files and excludes it, with the +sibling `src/security/permission.zod.ts` present as the lit control). Its +published reach is the emitted declarations, and they move: the new clause is +present in `dist/security/index.d.ts` and `dist/security/index.d.mts`, both in +that same shipped list, with the superseded spelling absent from every built +declaration file and the docblock's unchanged neighbouring sentence present in +the same two as the lit control.