@odla-ai/brand 0.2.1 → 0.4.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,26 @@ 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
+ // Optional test/edge fetch; it receives only a Brand-minted private URL.
75
+ fetchPrivateAsset: fetch,
76
+ authorizeCapability: verifyCurrentHumanAppOwner,
77
+ consumeHumanExact: async (input) =>
78
+ (await appsForBearer(input.req).consumeHumanExactAuthority(
79
+ input.appId,
80
+ {
81
+ capability: input.capability,
82
+ projectCapability: input.projectCapability,
83
+ effect: input.effect,
84
+ resource: input.resource,
85
+ actionDigest: input.actionDigest,
86
+ consumptionIdempotencyKey: input.consumptionIdempotencyKey,
87
+ },
88
+ )).receipt,
89
+ verifySourceAssetSnapshot: verifyPrivateObjectExactly,
42
90
  });
43
91
 
44
92
  export default {
@@ -48,6 +96,53 @@ export default {
48
96
  };
49
97
  ```
50
98
 
99
+ `GET /api/brand/discussion-references?q=…` searches only books on the
100
+ asserted principal's current roster and their live assets, palettes, proposals,
101
+ and receipts. Supplying `kind` + `id` performs exact lookup and returns no item
102
+ unless that row still exists in the named book. Returned links use stable
103
+ `odla-ref=brand:kind/book-id[/resource-id]` identities; the destination calls
104
+ `parseBrandDiscussionReference` to select the book and review surface. Each
105
+ projection includes a bounded product-authored `summary`, `status`, and
106
+ `destination` for people and read-only agent tools. Registry assertions bind
107
+ the exact `brand` product, `brand.read`, current canonical endpoint audience,
108
+ app incarnation, environment, and principal; an agent assertion also binds its
109
+ manager and live grant revision. The host must set its current
110
+ `ODLA_APP_INCARNATION` and fail closed on a mismatch. This assertion cannot
111
+ authorize proposal resolution or any other Brand mutation.
112
+
113
+ Palette projections additionally carry at most 24 validated semantic roles,
114
+ normalized `#rrggbb` colors, and bounded optional names so Discussion can use
115
+ the shared `@odla-ai/ui` `PaletteStrip` without reconstructing brand state.
116
+ An assertion-authorized exact lookup may request
117
+ `inspect=asset-content&kind=brand:asset&id=…`. Brand resolves the linked row,
118
+ mints its own short-lived private-object URL, and returns only bounded PNG,
119
+ JPEG, GIF, WebP, or PDF bytes—never the URL or a separate object path, storage
120
+ id, app id, or book id. Only the already-discovered canonical reference id
121
+ crosses the boundary. It validates type and magic, size, and SHA-256, then
122
+ reloads both
123
+ membership and the exact linked asset row after I/O. SVG, malformed, spoofed,
124
+ changed, unsupported, and over-budget assets fail closed. Returned bytes carry
125
+ `untrusted_project_material` taint for the hosted inference boundary; the
126
+ native link remains navigation only and human proposal approval is unchanged.
127
+
128
+ Resolve a proposal from a human review UI with one stable mutation id. Send
129
+ back exactly the proposal snapshot that UI rendered; a stale or concurrently
130
+ resolved snapshot answers `409`, while an identical retry returns the original
131
+ receipt with `duplicate: true`.
132
+
133
+ ```ts
134
+ await fetch(`/api/brand/books/${book.id}/proposals/${proposal.id}/resolve`, {
135
+ method: "POST",
136
+ headers: { "content-type": "application/json" },
137
+ body: JSON.stringify({
138
+ mutationId: crypto.randomUUID(),
139
+ resolution: "accepted", // or "rejected"
140
+ reviewedProposal: proposal,
141
+ note: "Approved in palette review",
142
+ }),
143
+ });
144
+ ```
145
+
51
146
  Wire the agent by attaching `brandSkill` to a persona (or let the chat-agent worker route a `brandBotTrigger` dispatch into `dispatchBrandTurn`):
52
147
 
53
148
  ```ts
@@ -57,8 +152,14 @@ const spec = inference.catalog[model];
57
152
  const persona = createBrandPersona({
58
153
  model,
59
154
  brand: {
60
- db, bookId, fileBaseUrl,
155
+ db: rulesScopedAgentDb,
156
+ bookId,
61
157
  self: { selfId: botId, kind: "bot" },
158
+ agentDbBinding: { principalId: botId, credentialRef },
159
+ agentJobId: jobId,
160
+ agentBridge,
161
+ authorizeCapability: verifyLiveAgentGrant,
162
+ resolvePrincipals: resolveSafeDirectoryProjection,
62
163
  visionInToolResults: supportsBrandVision(spec),
63
164
  },
64
165
  });
@@ -75,3 +176,20 @@ import { compileBrandTokens, renderTokensCss } from "@odla-ai/brand/tokens";
75
176
  const { light, dark, warnings } = compileBrandTokens({ swatches });
76
177
  const css = renderTokensCss(light, { dark }); // :root / [data-theme="dark"] / media guard
77
178
  ```
179
+
180
+ ## Authority and asset contract
181
+
182
+ - Install `BRAND_AGENT_PROFILE`: agents receive only `brand.read` and
183
+ `brand.edit`, and may create proposals, record analysis, and read bounded
184
+ asset content only through semantic host operations. Deny raw Brand writes,
185
+ raw file reads, signing, and admin credentials.
186
+ - `createBrandRoutes` requires all security seams. `authorize` derives the
187
+ actor from a verified bearer; `authorizeCapability` revalidates current app
188
+ ownership; `consumeHumanExact` consumes the exact reviewed action; and
189
+ `verifySourceAssetSnapshot` revalidates private path, ETag, size, media type,
190
+ and SHA-256 immediately before approval. The returned authority receipt must
191
+ carry the current 32-character app incarnation and is stored losslessly with
192
+ the Brand decision.
193
+ - `409` means the review or authority conflicted, `410` means its authority or
194
+ source is gone, and registry/storage/network outages remain opaque `500`s.
195
+ 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 };