@odla-ai/brand 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -19,9 +19,38 @@ npm i @odla-ai/brand
19
19
 
20
20
  The package is a **schema + rules + an agent skill + a color engine + a token compiler + a route factory** — not a service you run.
21
21
 
22
- - **Proposals are the only door to the palette.** The agent explores colors (deterministic OKLCH math: harmonies, ramps, ΔEOK dedupe, WCAG contrast) and writes every candidate as an open `brand_proposal`. A human accepts or rejects; `resolve_proposal` is instructed to fire only on explicit user confirmation. Accepted proposals become `brand_palette` rows with full provenance.
23
- - **Vision happens in the model's own turn.** `view_asset` fetches uploaded bytes worker-side and returns them as image/PDF blocks *inside the tool result* — gated on the model's `toolResultBlocks` catalog capability (`supportsBrandVision`), with a pre-turn attachment fallback for models without it. No AI calls inside any tool handler; every handler is deterministic and testable.
24
- - **Uploads are worker-mediated.** The odla-db browser client cannot write files, so `createBrandRoutes` proxies multipart uploads onto the app's `$files` storage (content-type allowlist, size cap, unguessable paths) and keeps `brand_asset` rows deny-all so they always match real storage.
22
+ - **Proposals are the human checkpoint.** Brand direction, image/logo choices,
23
+ typography, voice, and colors stay proposals in the conversation. Palette
24
+ candidates are the executable path today: the agent explores them with
25
+ deterministic OKLCH/WCAG math and writes an open `brand_proposal`. A human
26
+ approves, rejects, or requests a revision before any palette effect. The
27
+ agent skill has no resolution tool: both `propose_palette` and
28
+ `update_section` park open proposals. A current book member who is also a
29
+ direct Clerk app owner/co-owner posts the exact reviewed snapshot to the
30
+ guarded route. It consumes one central `human_exact` `brand.approve` use and
31
+ atomically writes the effect plus an immutable `brand_approval_receipt`.
32
+ The central receipt names the exact Registry app incarnation, so an approval
33
+ from a purged project cannot resolve content in a same-id replacement.
34
+ Review clients must retain one mutation id for each exact proposal digest +
35
+ accepted/rejected decision across pending requests and retries. Proposal
36
+ author names must come from a Registry-owned principal projection; mutable
37
+ chat participants and raw ids are not identity presentation.
38
+ - **Vision happens through a semantic bridge.** `view_asset` asks the trusted
39
+ host for bytes by asset id and returns image/PDF blocks inside the tool
40
+ result when the model supports them. The rules-scoped agent credential
41
+ cannot read raw files, mint signed URLs, or write `brand_*` rows. There is
42
+ no raw or signed-URL fallback. Message attachment ids are revalidated against
43
+ the exact live Brand book before they enter the prompt; every facet proposal
44
+ may preserve the source asset's immutable byte snapshot.
45
+ - **Brand child writes are worker-mediated.** CEL permits members to read their
46
+ sections, palettes, proposals, and approval receipts, but all writes are
47
+ closed. `createBrandRoutes` owns guarded human decisions and proxies
48
+ private multipart uploads, short-lived signed downloads, and human effects.
49
+ Agent tools call only the host-provided `BrandAgentBridge` operations in
50
+ `BRAND_AGENT_PROFILE`.
51
+ - **The upload surface is intentionally narrow.** Raster PNG/JPEG/GIF/WebP and
52
+ PDF inputs are supported. SVG active content and binary font files are not;
53
+ typography remains an exact, reviewable family/style proposal.
25
54
  - **Tokens compile to the @odla-ai/ui contract.** `compileBrandTokens` maps swatch roles onto `--ui-*` names — always emitting every required token plus the accent-composing derived set, so a scoped preview island never keeps stale root composites — and `renderTokensCss` emits theme-structured CSS (light, dark, invert). Dark is derived algorithmically and contrast re-tuned per token.
26
55
  - **Zero runtime dependencies.** The db client is injected structurally (a real `@odla-ai/db` `AdminDb` satisfies `BrandDb`); `@odla-ai/ai` is a types-only optional peer.
27
56
 
@@ -38,7 +67,24 @@ const db = init({ appId, adminToken, endpoint });
38
67
 
39
68
  const routes = createBrandRoutes({
40
69
  db,
41
- authorize: async (req) => (await isMember(req)) ? { id: await userId(req), kind: "human" } : null,
70
+ appId,
71
+ authorize: verifyDirectCaller,
72
+ // Read-only: exact brand/brand.read/audience/app-incarnation assertion.
73
+ authorizeDiscussionReferences: verifyDiscussionReferenceAssertion,
74
+ authorizeCapability: verifyCurrentHumanAppOwner,
75
+ consumeHumanExact: async (input) =>
76
+ (await appsForBearer(input.req).consumeHumanExactAuthority(
77
+ input.appId,
78
+ {
79
+ capability: input.capability,
80
+ projectCapability: input.projectCapability,
81
+ effect: input.effect,
82
+ resource: input.resource,
83
+ actionDigest: input.actionDigest,
84
+ consumptionIdempotencyKey: input.consumptionIdempotencyKey,
85
+ },
86
+ )).receipt,
87
+ verifySourceAssetSnapshot: verifyPrivateObjectExactly,
42
88
  });
43
89
 
44
90
  export default {
@@ -48,6 +94,38 @@ export default {
48
94
  };
49
95
  ```
50
96
 
97
+ `GET /api/brand/discussion-references?q=…` searches only books on the
98
+ asserted principal's current roster and their live assets, palettes, proposals,
99
+ and receipts. Supplying `kind` + `id` performs exact lookup and returns no item
100
+ unless that row still exists in the named book. Returned links use stable
101
+ `odla-ref=brand:kind/book-id[/resource-id]` identities; the destination calls
102
+ `parseBrandDiscussionReference` to select the book and review surface. Each
103
+ projection includes a bounded product-authored `summary`, `status`, and
104
+ `destination` for people and read-only agent tools. Registry assertions bind
105
+ the exact `brand` product, `brand.read`, current canonical endpoint audience,
106
+ app incarnation, environment, and principal; an agent assertion also binds its
107
+ manager and live grant revision. The host must set its current
108
+ `ODLA_APP_INCARNATION` and fail closed on a mismatch. This assertion cannot
109
+ authorize proposal resolution or any other Brand mutation.
110
+
111
+ Resolve a proposal from a human review UI with one stable mutation id. Send
112
+ back exactly the proposal snapshot that UI rendered; a stale or concurrently
113
+ resolved snapshot answers `409`, while an identical retry returns the original
114
+ receipt with `duplicate: true`.
115
+
116
+ ```ts
117
+ await fetch(`/api/brand/books/${book.id}/proposals/${proposal.id}/resolve`, {
118
+ method: "POST",
119
+ headers: { "content-type": "application/json" },
120
+ body: JSON.stringify({
121
+ mutationId: crypto.randomUUID(),
122
+ resolution: "accepted", // or "rejected"
123
+ reviewedProposal: proposal,
124
+ note: "Approved in palette review",
125
+ }),
126
+ });
127
+ ```
128
+
51
129
  Wire the agent by attaching `brandSkill` to a persona (or let the chat-agent worker route a `brandBotTrigger` dispatch into `dispatchBrandTurn`):
52
130
 
53
131
  ```ts
@@ -57,8 +135,14 @@ const spec = inference.catalog[model];
57
135
  const persona = createBrandPersona({
58
136
  model,
59
137
  brand: {
60
- db, bookId, fileBaseUrl,
138
+ db: rulesScopedAgentDb,
139
+ bookId,
61
140
  self: { selfId: botId, kind: "bot" },
141
+ agentDbBinding: { principalId: botId, credentialRef },
142
+ agentJobId: jobId,
143
+ agentBridge,
144
+ authorizeCapability: verifyLiveAgentGrant,
145
+ resolvePrincipals: resolveSafeDirectoryProjection,
62
146
  visionInToolResults: supportsBrandVision(spec),
63
147
  },
64
148
  });
@@ -75,3 +159,20 @@ import { compileBrandTokens, renderTokensCss } from "@odla-ai/brand/tokens";
75
159
  const { light, dark, warnings } = compileBrandTokens({ swatches });
76
160
  const css = renderTokensCss(light, { dark }); // :root / [data-theme="dark"] / media guard
77
161
  ```
162
+
163
+ ## Authority and asset contract
164
+
165
+ - Install `BRAND_AGENT_PROFILE`: agents receive only `brand.read` and
166
+ `brand.edit`, and may create proposals, record analysis, and read bounded
167
+ asset content only through semantic host operations. Deny raw Brand writes,
168
+ raw file reads, signing, and admin credentials.
169
+ - `createBrandRoutes` requires all security seams. `authorize` derives the
170
+ actor from a verified bearer; `authorizeCapability` revalidates current app
171
+ ownership; `consumeHumanExact` consumes the exact reviewed action; and
172
+ `verifySourceAssetSnapshot` revalidates private path, ETag, size, media type,
173
+ and SHA-256 immediately before approval. The returned authority receipt must
174
+ carry the current 32-character app incarnation and is stored losslessly with
175
+ the Brand decision.
176
+ - `409` means the review or authority conflicted, `410` means its authority or
177
+ source is gone, and registry/storage/network outages remain opaque `500`s.
178
+ Definite identity or object mismatches fail closed.
@@ -4,6 +4,7 @@ declare const BRAND_NS: {
4
4
  readonly section: "brand_section";
5
5
  readonly palette: "brand_palette";
6
6
  readonly proposal: "brand_proposal";
7
+ readonly approvalReceipt: "brand_approval_receipt";
7
8
  readonly asset: "brand_asset";
8
9
  };
9
10
  /** Brand-book lifecycle: authored → adopted → retired (delete is owner-only). */
@@ -27,8 +28,8 @@ type PaletteStatus = (typeof PALETTE_STATUSES)[number];
27
28
  declare const PALETTE_SOURCES: readonly ["extracted", "derived", "manual"];
28
29
  /** Provenance of a palette's swatches. */
29
30
  type PaletteSource = (typeof PALETTE_SOURCES)[number];
30
- /** What a proposal proposes (palettes are the first-class case). */
31
- declare const PROPOSAL_KINDS: readonly ["palette", "typography", "voice", "logo"];
31
+ /** Every mutable brand facet uses the same proposal/approval contract. */
32
+ declare const PROPOSAL_KINDS: readonly ["palette", "typography", "voice", "logo", "imagery"];
32
33
  /** Which brand facet a proposal targets. */
33
34
  type ProposalKind = (typeof PROPOSAL_KINDS)[number];
34
35
  /** Proposal lifecycle: open until a HUMAN accepts/rejects it (or a newer
@@ -37,56 +38,13 @@ declare const PROPOSAL_STATUSES: readonly ["open", "accepted", "rejected", "supe
37
38
  /** Where a proposal stands in the human-gated review flow. */
38
39
  type ProposalStatus = (typeof PROPOSAL_STATUSES)[number];
39
40
  /** What kind of upload an asset row records. */
40
- declare const ASSET_KINDS: readonly ["logo", "wordmark", "inspiration", "document", "font", "other"];
41
+ declare const ASSET_KINDS: readonly ["logo", "wordmark", "inspiration", "document", "other"];
41
42
  /** The declared purpose of an uploaded asset. */
42
43
  type AssetKind = (typeof ASSET_KINDS)[number];
43
44
  /** Every swatch role a palette may assign. `chart` may repeat (a series);
44
45
  * `custom` is the escape hatch for roles the token compiler doesn't map. */
45
46
  declare const SWATCH_ROLES: readonly ["primary", "secondary", "highlight", "bg", "surface", "text", "neutral", "good", "warn", "danger", "chart", "custom"];
46
47
 
47
- /** Scalar values odla-db attrs and where-clauses accept. */
48
- type BrandScalar = string | number | boolean | null;
49
- /** Natural-key lookup ref for idempotent upserts (odla-db `Lookup` shape). */
50
- interface BrandLookup {
51
- ns: string;
52
- attr: string;
53
- value: BrandScalar;
54
- }
55
- /** How an op names its row: a raw entity id, or a natural-key lookup. */
56
- type BrandEntityRef = string | BrandLookup;
57
- /** Attr payload of one op. Typed `any` on purpose: the real odla-db
58
- * `transact` constrains attrs to its own `Value` type, and `any` is the one
59
- * shape assignable in both directions with no cast. */
60
- type BrandAttrs = Record<string, any>;
61
- /** One transact operation (update = attr-merge upsert; merge = deep-merge
62
- * into a json attribute; retract = clear the named attributes, since a typed
63
- * scalar can't store null). */
64
- type BrandOp = {
65
- t: "update";
66
- ns: string;
67
- id: BrandEntityRef;
68
- attrs: BrandAttrs;
69
- } | {
70
- t: "merge";
71
- ns: string;
72
- id: BrandEntityRef;
73
- attrs: BrandAttrs;
74
- } | {
75
- t: "retract";
76
- ns: string;
77
- id: BrandEntityRef;
78
- attrs: string[];
79
- } | {
80
- t: "delete";
81
- ns: string;
82
- id: BrandEntityRef;
83
- };
84
- /** A row as odla-db returns it: hydrated id plus attrs. */
85
- type BrandRow = {
86
- id: string;
87
- } & Record<string, unknown>;
88
- /** Query result: namespace → rows. */
89
- type BrandResult = Record<string, BrandRow[]>;
90
48
  /** The role a swatch plays in the palette (what the token compiler maps). */
91
49
  type SwatchRole = (typeof SWATCH_ROLES)[number];
92
50
  /** One palette color: a role, a `#rrggbb` hex, and optional naming/why. */
@@ -134,132 +92,18 @@ interface AssetAnalysis {
134
92
  dominantColors: string[];
135
93
  tags: string[];
136
94
  }
137
- /** The compiled-token cache stored on `brand_book.tokens`. `warnings` carries
138
- * the compiler's `TokenWarning[]` (typed loosely here so the row types don't
139
- * depend on the token compiler's module). */
95
+ /** The compiled-token cache stored on `brand_book.tokens`. */
140
96
  interface BrandTokensSnapshot {
141
97
  light: Record<string, string>;
142
98
  dark: Record<string, string>;
143
99
  warnings: unknown[];
144
100
  compiledAt: number;
145
- }
146
- /** A brand book row: the auth roster (`memberIds`, including the bot agent
147
- * id) plus the active palette and compiled-token cache. */
148
- interface BrandBook {
149
- id: string;
150
- slug: string;
151
- name: string;
152
- status: BookStatus;
153
- ownerId: string;
154
- memberIds: string[];
155
- channelId?: string;
156
- activePaletteId?: string;
157
- tokens?: BrandTokensSnapshot;
158
- summary?: string;
159
- createdAt: number;
160
- updatedAt: number;
161
- }
162
- /** A section row — one per (book, kind), upserted by its natural `key`. */
163
- interface BrandSection {
164
- id: string;
165
- key: string;
166
- bookId: string;
167
- kind: SectionKind;
168
- status: SectionStatus;
169
- content: Record<string, unknown>;
170
- audience: string[];
171
- updatedBy: string;
172
- updatedAt: number;
173
- }
174
- /** A palette row. Swatches are json ON the palette, not a namespace: the
175
- * palette is the atomic proposal/approval unit and swatches are never
176
- * queried across palettes. */
177
- interface BrandPalette {
178
- id: string;
179
- bookId: string;
180
- name: string;
181
- status: PaletteStatus;
182
- swatches: Swatch[];
183
- seedHex?: string;
184
- source: PaletteSource;
185
- rationale?: string;
186
- proposalId?: string;
187
- audience: string[];
188
- createdAt: number;
189
- updatedAt: number;
190
- }
191
- /** A proposal row: agent output parked for explicit human resolution. */
192
- interface BrandProposal {
193
- id: string;
194
- bookId: string;
195
- kind: ProposalKind;
196
- status: ProposalStatus;
197
- payload: Record<string, unknown>;
198
- rationale: string;
199
- sourceAssetId?: string;
200
- messageId?: string;
201
- audience: string[];
202
- createdBy: string;
203
- createdAt: number;
204
- resolvedBy?: string;
205
- resolvedAt?: number;
206
- resolutionNote?: string;
207
- }
208
- /** An asset row mirroring one real uploaded file (worker-mediated; rules
209
- * close the namespace). Delete is a tombstone (`deletedAt`), never a row
210
- * delete, so analyses/proposals referencing it stay coherent. */
211
- interface BrandAsset {
212
- id: string;
213
- bookId: string;
214
- kind: AssetKind;
215
- path: string;
216
- url: string;
217
- contentType: string;
218
- size: number;
219
- title?: string;
220
- analysis?: AssetAnalysis;
221
- analyzedAt?: number;
222
- audience: string[];
223
- uploadedBy: string;
224
- createdAt: number;
225
- deletedAt?: number;
226
- }
227
- /** What `db.storage.upload` resolves to (odla-db's file record shape). */
228
- interface BrandFileRecord {
229
- id: string;
230
- path: string;
231
- url: string;
232
- size: number;
233
- contentType: string;
234
- }
235
- /** Bodies `storage.upload` accepts — a practical subset of the platform's
236
- * `BodyInit` (which has no ambient type under a DOM-less lib). The real
237
- * @odla-ai/db client accepts a superset, so it still structurally satisfies
238
- * {@link BrandStorage}. */
239
- type BrandUploadBody = string | Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
240
- /** The injected file store — @odla-ai/db's `storage` API satisfies this. */
241
- interface BrandStorage {
242
- upload(path: string, data: BrandUploadBody, contentType?: string): Promise<BrandFileRecord>;
243
- delete(path: string): Promise<void>;
244
- }
245
- /** The injected odla client (structural — a real @odla-ai/db `AdminDb`
246
- * satisfies it). `transact` accepts flat op arrays and `mutationId` gives
247
- * exactly-once application (`duplicate: true` on replay). */
248
- interface BrandDb {
249
- query(q: Record<string, unknown>): Promise<Record<string, unknown[]>>;
250
- transact(ops: unknown[], opts?: {
251
- mutationId?: string;
252
- }): Promise<{
253
- txId: number;
254
- duplicate?: boolean;
255
- }>;
256
- storage: BrandStorage;
257
- }
258
- /** Who is acting: a human member or the book's bot agent. */
259
- interface BrandActor {
260
- id: string;
261
- kind: "human" | "agent";
262
- email?: string;
101
+ paletteId: string;
102
+ paletteReceiptId: string;
103
+ paletteActionDigest: string;
104
+ typographyReceiptId?: string;
105
+ typographyActionDigest?: string;
106
+ sourceDigest: string;
263
107
  }
264
108
 
265
109
  /**
@@ -406,4 +250,4 @@ declare function renderTokensCss(light: BrandTokens, opts?: RenderTokensCssOptio
406
250
  */
407
251
  declare function compileBrandTokens(input: CompileInput): CompiledBrandTokens;
408
252
 
409
- export { SWATCH_ROLES as $, type AssetAnalysis as A, type BrandDb as B, type BrandStorage as C, type BrandTokens as D, type BrandTokensSnapshot as E, type BrandUploadBody as F, type CompileInput as G, type CompiledBrandTokens as H, DARK_FLIP_L_MAX as I, DARK_FLIP_L_MIN as J, DEFAULT_ACCENT_SEED as K, type ImagerySection as L, type LogoSection as M, type MapResult as N, PALETTE_STATUSES as O, PALETTE_SOURCES as P, PROPOSAL_KINDS as Q, PROPOSAL_STATUSES as R, type Swatch as S, type PaletteSection as T, type PaletteSource as U, type PaletteStatus as V, type ProposalKind as W, type ProposalStatus as X, type RenderTokensCssOptions as Y, SECTION_KINDS as Z, SECTION_STATUSES as _, type BrandBook as a, type SwatchRole as a0, type TokenWarning as a1, type TypographySection as a2, type VoiceSection as a3, compileBrandTokens as a4, deriveDarkTokens as a5, mapPaletteToTokens as a6, renderTokensCss as a7, type BrandOp as b, type BrandEntityRef as c, type SectionKind as d, type SectionStatus as e, type BrandProposal as f, type AssetKind as g, type BrandActor as h, ASSET_KINDS as i, BOOK_STATUSES as j, BRAND_CHART_TOKENS as k, BRAND_CHAT_TOKENS as l, BRAND_DERIVED_TOKENS as m, BRAND_EMITTED_TOKENS as n, BRAND_NS as o, BRAND_REQUIRED_TOKENS as p, type BookStatus as q, type BrandAsset as r, type BrandAttrs as s, type BrandFileRecord as t, type BrandLookup as u, type BrandPalette as v, type BrandResult as w, type BrandRow as x, type BrandScalar as y, type BrandSection as z };
253
+ export { type AssetKind as A, type BookStatus as B, type CompileInput as C, DARK_FLIP_L_MAX as D, SWATCH_ROLES as E, type SwatchRole as F, type TypographySection as G, compileBrandTokens as H, type ImagerySection as I, deriveDarkTokens as J, mapPaletteToTokens as K, type LogoSection as L, type MapResult as M, renderTokensCss as N, type ProposalKind as P, type RenderTokensCssOptions as R, type Swatch as S, type TokenWarning as T, type VoiceSection as V, type ProposalStatus as a, type AssetAnalysis as b, type BrandTokensSnapshot as c, type PaletteStatus as d, type PaletteSource as e, type SectionKind as f, type SectionStatus as g, ASSET_KINDS as h, BOOK_STATUSES as i, BRAND_CHART_TOKENS as j, BRAND_CHAT_TOKENS as k, BRAND_DERIVED_TOKENS as l, BRAND_EMITTED_TOKENS as m, BRAND_NS as n, BRAND_REQUIRED_TOKENS as o, type BrandTokens as p, type CompiledBrandTokens as q, DARK_FLIP_L_MIN as r, DEFAULT_ACCENT_SEED as s, PALETTE_SOURCES as t, PALETTE_STATUSES as u, PROPOSAL_KINDS as v, PROPOSAL_STATUSES as w, type PaletteSection as x, SECTION_KINDS as y, SECTION_STATUSES as z };
@@ -4,6 +4,7 @@ declare const BRAND_NS: {
4
4
  readonly section: "brand_section";
5
5
  readonly palette: "brand_palette";
6
6
  readonly proposal: "brand_proposal";
7
+ readonly approvalReceipt: "brand_approval_receipt";
7
8
  readonly asset: "brand_asset";
8
9
  };
9
10
  /** Brand-book lifecycle: authored → adopted → retired (delete is owner-only). */
@@ -27,8 +28,8 @@ type PaletteStatus = (typeof PALETTE_STATUSES)[number];
27
28
  declare const PALETTE_SOURCES: readonly ["extracted", "derived", "manual"];
28
29
  /** Provenance of a palette's swatches. */
29
30
  type PaletteSource = (typeof PALETTE_SOURCES)[number];
30
- /** What a proposal proposes (palettes are the first-class case). */
31
- declare const PROPOSAL_KINDS: readonly ["palette", "typography", "voice", "logo"];
31
+ /** Every mutable brand facet uses the same proposal/approval contract. */
32
+ declare const PROPOSAL_KINDS: readonly ["palette", "typography", "voice", "logo", "imagery"];
32
33
  /** Which brand facet a proposal targets. */
33
34
  type ProposalKind = (typeof PROPOSAL_KINDS)[number];
34
35
  /** Proposal lifecycle: open until a HUMAN accepts/rejects it (or a newer
@@ -37,56 +38,13 @@ declare const PROPOSAL_STATUSES: readonly ["open", "accepted", "rejected", "supe
37
38
  /** Where a proposal stands in the human-gated review flow. */
38
39
  type ProposalStatus = (typeof PROPOSAL_STATUSES)[number];
39
40
  /** What kind of upload an asset row records. */
40
- declare const ASSET_KINDS: readonly ["logo", "wordmark", "inspiration", "document", "font", "other"];
41
+ declare const ASSET_KINDS: readonly ["logo", "wordmark", "inspiration", "document", "other"];
41
42
  /** The declared purpose of an uploaded asset. */
42
43
  type AssetKind = (typeof ASSET_KINDS)[number];
43
44
  /** Every swatch role a palette may assign. `chart` may repeat (a series);
44
45
  * `custom` is the escape hatch for roles the token compiler doesn't map. */
45
46
  declare const SWATCH_ROLES: readonly ["primary", "secondary", "highlight", "bg", "surface", "text", "neutral", "good", "warn", "danger", "chart", "custom"];
46
47
 
47
- /** Scalar values odla-db attrs and where-clauses accept. */
48
- type BrandScalar = string | number | boolean | null;
49
- /** Natural-key lookup ref for idempotent upserts (odla-db `Lookup` shape). */
50
- interface BrandLookup {
51
- ns: string;
52
- attr: string;
53
- value: BrandScalar;
54
- }
55
- /** How an op names its row: a raw entity id, or a natural-key lookup. */
56
- type BrandEntityRef = string | BrandLookup;
57
- /** Attr payload of one op. Typed `any` on purpose: the real odla-db
58
- * `transact` constrains attrs to its own `Value` type, and `any` is the one
59
- * shape assignable in both directions with no cast. */
60
- type BrandAttrs = Record<string, any>;
61
- /** One transact operation (update = attr-merge upsert; merge = deep-merge
62
- * into a json attribute; retract = clear the named attributes, since a typed
63
- * scalar can't store null). */
64
- type BrandOp = {
65
- t: "update";
66
- ns: string;
67
- id: BrandEntityRef;
68
- attrs: BrandAttrs;
69
- } | {
70
- t: "merge";
71
- ns: string;
72
- id: BrandEntityRef;
73
- attrs: BrandAttrs;
74
- } | {
75
- t: "retract";
76
- ns: string;
77
- id: BrandEntityRef;
78
- attrs: string[];
79
- } | {
80
- t: "delete";
81
- ns: string;
82
- id: BrandEntityRef;
83
- };
84
- /** A row as odla-db returns it: hydrated id plus attrs. */
85
- type BrandRow = {
86
- id: string;
87
- } & Record<string, unknown>;
88
- /** Query result: namespace → rows. */
89
- type BrandResult = Record<string, BrandRow[]>;
90
48
  /** The role a swatch plays in the palette (what the token compiler maps). */
91
49
  type SwatchRole = (typeof SWATCH_ROLES)[number];
92
50
  /** One palette color: a role, a `#rrggbb` hex, and optional naming/why. */
@@ -134,132 +92,18 @@ interface AssetAnalysis {
134
92
  dominantColors: string[];
135
93
  tags: string[];
136
94
  }
137
- /** The compiled-token cache stored on `brand_book.tokens`. `warnings` carries
138
- * the compiler's `TokenWarning[]` (typed loosely here so the row types don't
139
- * depend on the token compiler's module). */
95
+ /** The compiled-token cache stored on `brand_book.tokens`. */
140
96
  interface BrandTokensSnapshot {
141
97
  light: Record<string, string>;
142
98
  dark: Record<string, string>;
143
99
  warnings: unknown[];
144
100
  compiledAt: number;
145
- }
146
- /** A brand book row: the auth roster (`memberIds`, including the bot agent
147
- * id) plus the active palette and compiled-token cache. */
148
- interface BrandBook {
149
- id: string;
150
- slug: string;
151
- name: string;
152
- status: BookStatus;
153
- ownerId: string;
154
- memberIds: string[];
155
- channelId?: string;
156
- activePaletteId?: string;
157
- tokens?: BrandTokensSnapshot;
158
- summary?: string;
159
- createdAt: number;
160
- updatedAt: number;
161
- }
162
- /** A section row — one per (book, kind), upserted by its natural `key`. */
163
- interface BrandSection {
164
- id: string;
165
- key: string;
166
- bookId: string;
167
- kind: SectionKind;
168
- status: SectionStatus;
169
- content: Record<string, unknown>;
170
- audience: string[];
171
- updatedBy: string;
172
- updatedAt: number;
173
- }
174
- /** A palette row. Swatches are json ON the palette, not a namespace: the
175
- * palette is the atomic proposal/approval unit and swatches are never
176
- * queried across palettes. */
177
- interface BrandPalette {
178
- id: string;
179
- bookId: string;
180
- name: string;
181
- status: PaletteStatus;
182
- swatches: Swatch[];
183
- seedHex?: string;
184
- source: PaletteSource;
185
- rationale?: string;
186
- proposalId?: string;
187
- audience: string[];
188
- createdAt: number;
189
- updatedAt: number;
190
- }
191
- /** A proposal row: agent output parked for explicit human resolution. */
192
- interface BrandProposal {
193
- id: string;
194
- bookId: string;
195
- kind: ProposalKind;
196
- status: ProposalStatus;
197
- payload: Record<string, unknown>;
198
- rationale: string;
199
- sourceAssetId?: string;
200
- messageId?: string;
201
- audience: string[];
202
- createdBy: string;
203
- createdAt: number;
204
- resolvedBy?: string;
205
- resolvedAt?: number;
206
- resolutionNote?: string;
207
- }
208
- /** An asset row mirroring one real uploaded file (worker-mediated; rules
209
- * close the namespace). Delete is a tombstone (`deletedAt`), never a row
210
- * delete, so analyses/proposals referencing it stay coherent. */
211
- interface BrandAsset {
212
- id: string;
213
- bookId: string;
214
- kind: AssetKind;
215
- path: string;
216
- url: string;
217
- contentType: string;
218
- size: number;
219
- title?: string;
220
- analysis?: AssetAnalysis;
221
- analyzedAt?: number;
222
- audience: string[];
223
- uploadedBy: string;
224
- createdAt: number;
225
- deletedAt?: number;
226
- }
227
- /** What `db.storage.upload` resolves to (odla-db's file record shape). */
228
- interface BrandFileRecord {
229
- id: string;
230
- path: string;
231
- url: string;
232
- size: number;
233
- contentType: string;
234
- }
235
- /** Bodies `storage.upload` accepts — a practical subset of the platform's
236
- * `BodyInit` (which has no ambient type under a DOM-less lib). The real
237
- * @odla-ai/db client accepts a superset, so it still structurally satisfies
238
- * {@link BrandStorage}. */
239
- type BrandUploadBody = string | Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
240
- /** The injected file store — @odla-ai/db's `storage` API satisfies this. */
241
- interface BrandStorage {
242
- upload(path: string, data: BrandUploadBody, contentType?: string): Promise<BrandFileRecord>;
243
- delete(path: string): Promise<void>;
244
- }
245
- /** The injected odla client (structural — a real @odla-ai/db `AdminDb`
246
- * satisfies it). `transact` accepts flat op arrays and `mutationId` gives
247
- * exactly-once application (`duplicate: true` on replay). */
248
- interface BrandDb {
249
- query(q: Record<string, unknown>): Promise<Record<string, unknown[]>>;
250
- transact(ops: unknown[], opts?: {
251
- mutationId?: string;
252
- }): Promise<{
253
- txId: number;
254
- duplicate?: boolean;
255
- }>;
256
- storage: BrandStorage;
257
- }
258
- /** Who is acting: a human member or the book's bot agent. */
259
- interface BrandActor {
260
- id: string;
261
- kind: "human" | "agent";
262
- email?: string;
101
+ paletteId: string;
102
+ paletteReceiptId: string;
103
+ paletteActionDigest: string;
104
+ typographyReceiptId?: string;
105
+ typographyActionDigest?: string;
106
+ sourceDigest: string;
263
107
  }
264
108
 
265
109
  /**
@@ -406,4 +250,4 @@ declare function renderTokensCss(light: BrandTokens, opts?: RenderTokensCssOptio
406
250
  */
407
251
  declare function compileBrandTokens(input: CompileInput): CompiledBrandTokens;
408
252
 
409
- export { SWATCH_ROLES as $, type AssetAnalysis as A, type BrandDb as B, type BrandStorage as C, type BrandTokens as D, type BrandTokensSnapshot as E, type BrandUploadBody as F, type CompileInput as G, type CompiledBrandTokens as H, DARK_FLIP_L_MAX as I, DARK_FLIP_L_MIN as J, DEFAULT_ACCENT_SEED as K, type ImagerySection as L, type LogoSection as M, type MapResult as N, PALETTE_STATUSES as O, PALETTE_SOURCES as P, PROPOSAL_KINDS as Q, PROPOSAL_STATUSES as R, type Swatch as S, type PaletteSection as T, type PaletteSource as U, type PaletteStatus as V, type ProposalKind as W, type ProposalStatus as X, type RenderTokensCssOptions as Y, SECTION_KINDS as Z, SECTION_STATUSES as _, type BrandBook as a, type SwatchRole as a0, type TokenWarning as a1, type TypographySection as a2, type VoiceSection as a3, compileBrandTokens as a4, deriveDarkTokens as a5, mapPaletteToTokens as a6, renderTokensCss as a7, type BrandOp as b, type BrandEntityRef as c, type SectionKind as d, type SectionStatus as e, type BrandProposal as f, type AssetKind as g, type BrandActor as h, ASSET_KINDS as i, BOOK_STATUSES as j, BRAND_CHART_TOKENS as k, BRAND_CHAT_TOKENS as l, BRAND_DERIVED_TOKENS as m, BRAND_EMITTED_TOKENS as n, BRAND_NS as o, BRAND_REQUIRED_TOKENS as p, type BookStatus as q, type BrandAsset as r, type BrandAttrs as s, type BrandFileRecord as t, type BrandLookup as u, type BrandPalette as v, type BrandResult as w, type BrandRow as x, type BrandScalar as y, type BrandSection as z };
253
+ export { type AssetKind as A, type BookStatus as B, type CompileInput as C, DARK_FLIP_L_MAX as D, SWATCH_ROLES as E, type SwatchRole as F, type TypographySection as G, compileBrandTokens as H, type ImagerySection as I, deriveDarkTokens as J, mapPaletteToTokens as K, type LogoSection as L, type MapResult as M, renderTokensCss as N, type ProposalKind as P, type RenderTokensCssOptions as R, type Swatch as S, type TokenWarning as T, type VoiceSection as V, type ProposalStatus as a, type AssetAnalysis as b, type BrandTokensSnapshot as c, type PaletteStatus as d, type PaletteSource as e, type SectionKind as f, type SectionStatus as g, ASSET_KINDS as h, BOOK_STATUSES as i, BRAND_CHART_TOKENS as j, BRAND_CHAT_TOKENS as k, BRAND_DERIVED_TOKENS as l, BRAND_EMITTED_TOKENS as m, BRAND_NS as n, BRAND_REQUIRED_TOKENS as o, type BrandTokens as p, type CompiledBrandTokens as q, DARK_FLIP_L_MIN as r, DEFAULT_ACCENT_SEED as s, PALETTE_SOURCES as t, PALETTE_STATUSES as u, PROPOSAL_KINDS as v, PROPOSAL_STATUSES as w, type PaletteSection as x, SECTION_KINDS as y, SECTION_STATUSES as z };