Skip to content

Commit 80f12ca

Browse files
os-zhuangclaude
andauthored
feat(spec)!: book audience gates by permission set — retire the { profile } arm (ADR-0090) (#2732)
ADR-0090 D2 removed the Profile concept, but BookAudienceSchema still modelled its gated arm as { profile: string } (with pre-D3 vocabulary in the comments). Books ship in packages, and packages own permission sets but never positions (D9 — the same environment-ownership argument that removed profiles), so the gate is a capability reference: - `{ profile: string }` → `{ permissionSet: string }` — one-step rename, no alias; the zod union now rejects `{ profile }` at parse time. - `'public'` comment now points at the built-in `guest` position (D9) instead of the removed "guest profile". - Regenerated content/docs/references (book.mdx), which also catches up security/* with the AdminScope (D12) and explain (D6) shapes that landed without a docs regen. Claude-Session: https://claude.ai/code/session_01Ph4jEAfWDh6taMbZweWhom Co-authored-by: Claude <noreply@anthropic.com>
1 parent 332b711 commit 80f12ca

4 files changed

Lines changed: 30 additions & 7 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': major
3+
---
4+
5+
`BookAudience` gated arm renamed: `{ profile: string }``{ permissionSet: string }`.
6+
7+
ADR-0090 D2 removed the Profile concept, but `book.audience` (ADR-0046 §6.7)
8+
still modelled its gated arm as a profile reference. Books ship in packages,
9+
and packages own permission sets but never positions (ADR-0090 D9), so the
10+
gate is a capability reference — a permission-set name the reader must hold,
11+
e.g. `{ permissionSet: 'crm_admin' }`. Pre-launch one-step rename, no alias:
12+
the zod union now rejects `{ profile }` at parse time. `'org'` and `'public'`
13+
literals are unchanged (`'public'` ≡ the built-in `guest` position, D9).

content/docs/references/system/book.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ Type: `string`
9494

9595
| Property | Type | Required | Description |
9696
| :--- | :--- | :--- | :--- |
97-
| **profile** | `string` || |
97+
| **permissionSet** | `string` || |
9898

9999
---
100100

packages/spec/src/system/book.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ describe('BookSchema (ADR-0046 §6)', () => {
2525
expect(() => BookSchema.parse({ name: 'crm_guide', groups: [{ key: 'Start', label: 'x' }] })).toThrow();
2626
});
2727
it('accepts audience variants', () => {
28-
for (const audience of ['org', 'public', { profile: 'admin' }] as const) {
28+
for (const audience of ['org', 'public', { permissionSet: 'crm_admin' }] as const) {
2929
expect(() => BookSchema.parse({ name: 'b', audience, groups: [] })).not.toThrow();
3030
}
3131
});
32+
it('rejects the removed { profile } audience shape (ADR-0090 D2)', () => {
33+
expect(() => BookSchema.parse({ name: 'b', audience: { profile: 'admin' }, groups: [] })).toThrow();
34+
});
3235
});
3336

3437
describe('resolveBookTree — derived membership (the AI-safety core)', () => {
@@ -151,7 +154,7 @@ describe('deriveImplicitPackageBook + audience', () => {
151154
it('isPublicAudience only true for public', () => {
152155
expect(isPublicAudience('public')).toBe(true);
153156
expect(isPublicAudience('org')).toBe(false);
154-
expect(isPublicAudience({ profile: 'admin' })).toBe(false);
157+
expect(isPublicAudience({ permissionSet: 'crm_admin' })).toBe(false);
155158
expect(isPublicAudience(undefined)).toBe(false);
156159
});
157160
});

packages/spec/src/system/book.zod.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,22 @@ export type BookGroup = {
8282
pages?: BookNode[];
8383
};
8484

85-
/** Access audience for a book — a reference into the permission model (ADR-0046 §6.7). */
85+
/**
86+
* Access audience for a book — a reference into the permission model
87+
* (ADR-0046 §6.7, vocabulary per ADR-0090). The gate is a capability
88+
* reference (a permission-set name), never a distribution one: books ship in
89+
* packages, and packages own permission sets but never positions (ADR-0090
90+
* D9) — a package gating its Admin Guide to its own `crm_admin` set keeps
91+
* provenance and uninstall semantics intact (ADR-0086).
92+
*/
8693
export const BookAudienceSchema = lazySchema(() =>
8794
z.union([
8895
z.literal('org'), // default — inherits the package grant (§3.6)
89-
z.literal('public'), // ≡ the data-layer `guest` profile (anonymous, indexable)
90-
z.object({ profile: z.string() }), // role-gated, e.g. { profile: 'admin' }
96+
z.literal('public'), // ≡ the built-in `guest` position (ADR-0090 D9): anonymous, indexable
97+
z.object({ permissionSet: z.string() }), // capability-gated, e.g. { permissionSet: 'crm_admin' }
9198
]),
9299
);
93-
export type BookAudience = 'org' | 'public' | { profile: string };
100+
export type BookAudience = 'org' | 'public' | { permissionSet: string };
94101

95102
export const BookSchema = lazySchema(() =>
96103
z.object({

0 commit comments

Comments
 (0)