diff --git a/.changeset/book-audience-permission-set.md b/.changeset/book-audience-permission-set.md
new file mode 100644
index 0000000000..d0c3a901bd
--- /dev/null
+++ b/.changeset/book-audience-permission-set.md
@@ -0,0 +1,13 @@
+---
+'@objectstack/spec': major
+---
+
+`BookAudience` gated arm renamed: `{ profile: string }` → `{ permissionSet: string }`.
+
+ADR-0090 D2 removed the Profile concept, but `book.audience` (ADR-0046 §6.7)
+still modelled its gated arm as a profile reference. Books ship in packages,
+and packages own permission sets but never positions (ADR-0090 D9), so the
+gate is a capability reference — a permission-set name the reader must hold,
+e.g. `{ permissionSet: 'crm_admin' }`. Pre-launch one-step rename, no alias:
+the zod union now rejects `{ profile }` at parse time. `'org'` and `'public'`
+literals are unchanged (`'public'` ≡ the built-in `guest` position, D9).
diff --git a/content/docs/references/security/explain.mdx b/content/docs/references/security/explain.mdx
new file mode 100644
index 0000000000..966c550f19
--- /dev/null
+++ b/content/docs/references/security/explain.mdx
@@ -0,0 +1,136 @@
+---
+title: Explain
+description: Explain protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+[ADR-0090 D6] Access-explanation contract — `explain(principal, object,
+
+operation)` as a first-class API.
+
+The explain engine (`@objectstack/plugin-security`) walks the SAME code
+
+paths as the enforcement middleware — the same permission-set resolution,
+
+the same evaluator, the same RLS compiler — and reports what each layer of
+
+the evaluation pipeline contributed to the final decision. "Explained by
+
+construction": the report can never drift from enforcement because it IS
+
+enforcement, minus the throw.
+
+Layer order mirrors the runtime pipeline:
+
+principal → required_permissions → object_crud → fls → owd_baseline →
+
+depth → sharing → vama_bypass → rls.
+
+
+**Source:** `packages/spec/src/security/explain.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { AccessMatrix, AccessMatrixEntry, ExplainDecision, ExplainLayer, ExplainOperation, ExplainRequest } from '@objectstack/spec/security';
+import type { AccessMatrix, AccessMatrixEntry, ExplainDecision, ExplainLayer, ExplainOperation, ExplainRequest } from '@objectstack/spec/security';
+
+// Validate data
+const result = AccessMatrix.parse(data);
+```
+
+---
+
+## AccessMatrix
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **version** | `number` | ✅ | |
+| **entries** | `Object[]` | ✅ | |
+
+
+---
+
+## AccessMatrixEntry
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **permissionSet** | `string` | ✅ | |
+| **object** | `string` | ✅ | |
+| **create** | `boolean` | ✅ | |
+| **read** | `boolean` | ✅ | |
+| **edit** | `boolean` | ✅ | |
+| **delete** | `boolean` | ✅ | |
+| **viewAllRecords** | `boolean` | ✅ | |
+| **modifyAllRecords** | `boolean` | ✅ | |
+| **readScope** | `string` | optional | |
+| **writeScope** | `string` | optional | |
+| **sharingModel** | `string` | optional | |
+
+
+---
+
+## ExplainDecision
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **allowed** | `boolean` | ✅ | |
+| **object** | `string` | ✅ | |
+| **operation** | `Enum<'read' \| 'create' \| 'update' \| 'delete' \| 'transfer' \| 'restore' \| 'purge'>` | ✅ | |
+| **principal** | `Object` | ✅ | |
+| **layers** | `Object[]` | ✅ | |
+| **readFilter** | `any` | optional | |
+
+
+---
+
+## ExplainLayer
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **layer** | `Enum<'principal' \| 'required_permissions' \| 'object_crud' \| 'fls' \| 'owd_baseline' \| 'depth' \| 'sharing' \| 'vama_bypass' \| 'rls'>` | ✅ | |
+| **verdict** | `Enum<'grants' \| 'denies' \| 'narrows' \| 'widens' \| 'neutral' \| 'not_applicable'>` | ✅ | |
+| **detail** | `string` | ✅ | |
+| **contributors** | `Object[]` | ✅ | |
+
+
+---
+
+## ExplainOperation
+
+### Allowed Values
+
+* `read`
+* `create`
+* `update`
+* `delete`
+* `transfer`
+* `restore`
+* `purge`
+
+
+---
+
+## ExplainRequest
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **object** | `string` | ✅ | |
+| **operation** | `Enum<'read' \| 'create' \| 'update' \| 'delete' \| 'transfer' \| 'restore' \| 'purge'>` | ✅ | |
+| **userId** | `string` | optional | |
+
+
+---
+
diff --git a/content/docs/references/security/index.mdx b/content/docs/references/security/index.mdx
index 4b94ae0a29..2885a067a9 100644
--- a/content/docs/references/security/index.mdx
+++ b/content/docs/references/security/index.mdx
@@ -6,6 +6,7 @@ description: Complete reference for all security protocol schemas
This section contains all protocol schemas for the security layer of ObjectStack.
+
diff --git a/content/docs/references/security/meta.json b/content/docs/references/security/meta.json
index 1e70554bfd..9d9dcff0cd 100644
--- a/content/docs/references/security/meta.json
+++ b/content/docs/references/security/meta.json
@@ -1,6 +1,7 @@
{
"title": "Security Protocol",
"pages": [
+ "explain",
"permission",
"rls",
"sharing",
diff --git a/content/docs/references/security/permission.mdx b/content/docs/references/security/permission.mdx
index 92e2af7597..4d1063964f 100644
--- a/content/docs/references/security/permission.mdx
+++ b/content/docs/references/security/permission.mdx
@@ -24,13 +24,29 @@ Refined with enterprise data lifecycle controls:
## TypeScript Usage
```typescript
-import { FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
-import type { FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
+import { AdminScope, FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
+import type { AdminScope, FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
// Validate data
-const result = FieldPermission.parse(data);
+const result = AdminScope.parse(data);
```
+---
+
+## AdminScope
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root |
+| **includeSubtree** | `boolean` | ✅ | Cover descendant business units too (default true) |
+| **manageAssignments** | `boolean` | ✅ | Manage user↔position assignments within the subtree |
+| **manageBindings** | `boolean` | ✅ | Manage position↔permission-set bindings within the subtree |
+| **authorEnvironmentSets** | `boolean` | ✅ | Author environment-owned permission sets |
+| **assignablePermissionSets** | `string[]` | ✅ | Allowlist of permission-set names the delegate may hand out |
+
+
---
## FieldPermission
@@ -96,6 +112,7 @@ const result = FieldPermission.parse(data);
| **tabPermissions** | `Record>` | optional | App/tab visibility: visible, hidden, default_on (shown by default), default_off (available but hidden initially) |
| **rowLevelSecurity** | `Object[]` | optional | Row-level security policies (see rls.zod.ts for full spec) |
| **contextVariables** | `Record` | optional | Context variables for RLS evaluation |
+| **adminScope** | `Object` | optional | [ADR-0090 D12] Scoped delegated-administration grant (BU subtree + assignable-set allowlist) |
---
diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx
index c4c9f321b8..d9169446f0 100644
--- a/content/docs/references/system/book.mdx
+++ b/content/docs/references/system/book.mdx
@@ -94,7 +94,7 @@ Type: `string`
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
-| **profile** | `string` | ✅ | |
+| **permissionSet** | `string` | ✅ | |
---
diff --git a/packages/spec/src/system/book.test.ts b/packages/spec/src/system/book.test.ts
index 3d96fdb032..60d47faced 100644
--- a/packages/spec/src/system/book.test.ts
+++ b/packages/spec/src/system/book.test.ts
@@ -25,10 +25,13 @@ describe('BookSchema (ADR-0046 §6)', () => {
expect(() => BookSchema.parse({ name: 'crm_guide', groups: [{ key: 'Start', label: 'x' }] })).toThrow();
});
it('accepts audience variants', () => {
- for (const audience of ['org', 'public', { profile: 'admin' }] as const) {
+ for (const audience of ['org', 'public', { permissionSet: 'crm_admin' }] as const) {
expect(() => BookSchema.parse({ name: 'b', audience, groups: [] })).not.toThrow();
}
});
+ it('rejects the removed { profile } audience shape (ADR-0090 D2)', () => {
+ expect(() => BookSchema.parse({ name: 'b', audience: { profile: 'admin' }, groups: [] })).toThrow();
+ });
});
describe('resolveBookTree — derived membership (the AI-safety core)', () => {
@@ -151,7 +154,7 @@ describe('deriveImplicitPackageBook + audience', () => {
it('isPublicAudience only true for public', () => {
expect(isPublicAudience('public')).toBe(true);
expect(isPublicAudience('org')).toBe(false);
- expect(isPublicAudience({ profile: 'admin' })).toBe(false);
+ expect(isPublicAudience({ permissionSet: 'crm_admin' })).toBe(false);
expect(isPublicAudience(undefined)).toBe(false);
});
});
diff --git a/packages/spec/src/system/book.zod.ts b/packages/spec/src/system/book.zod.ts
index 046ad71c52..8125a5fb85 100644
--- a/packages/spec/src/system/book.zod.ts
+++ b/packages/spec/src/system/book.zod.ts
@@ -82,15 +82,22 @@ export type BookGroup = {
pages?: BookNode[];
};
-/** Access audience for a book — a reference into the permission model (ADR-0046 §6.7). */
+/**
+ * Access audience for a book — a reference into the permission model
+ * (ADR-0046 §6.7, vocabulary per ADR-0090). The gate is a capability
+ * reference (a permission-set name), never a distribution one: books ship in
+ * packages, and packages own permission sets but never positions (ADR-0090
+ * D9) — a package gating its Admin Guide to its own `crm_admin` set keeps
+ * provenance and uninstall semantics intact (ADR-0086).
+ */
export const BookAudienceSchema = lazySchema(() =>
z.union([
z.literal('org'), // default — inherits the package grant (§3.6)
- z.literal('public'), // ≡ the data-layer `guest` profile (anonymous, indexable)
- z.object({ profile: z.string() }), // role-gated, e.g. { profile: 'admin' }
+ z.literal('public'), // ≡ the built-in `guest` position (ADR-0090 D9): anonymous, indexable
+ z.object({ permissionSet: z.string() }), // capability-gated, e.g. { permissionSet: 'crm_admin' }
]),
);
-export type BookAudience = 'org' | 'public' | { profile: string };
+export type BookAudience = 'org' | 'public' | { permissionSet: string };
export const BookSchema = lazySchema(() =>
z.object({