@odla-ai/brand 0.1.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.
@@ -0,0 +1,1342 @@
1
+ import { ToolHandler, Skill, ToolDef, Persona, Inference, AgentRunInput, ImageBlock, OracleContentBlock } from '@odla-ai/ai';
2
+
3
+ /** The odla-db namespaces @odla-ai/brand installs and writes. */
4
+ declare const BRAND_NS: {
5
+ readonly book: "brand_book";
6
+ readonly section: "brand_section";
7
+ readonly palette: "brand_palette";
8
+ readonly proposal: "brand_proposal";
9
+ readonly asset: "brand_asset";
10
+ };
11
+ /** Brand-book lifecycle: authored → adopted → retired (delete is owner-only). */
12
+ declare const BOOK_STATUSES: readonly ["draft", "active", "archived"];
13
+ /** Where a brand book stands in its lifecycle. */
14
+ type BookStatus = (typeof BOOK_STATUSES)[number];
15
+ /** The section kinds a book carries — one row per kind, upserted by natural key. */
16
+ declare const SECTION_KINDS: readonly ["palette", "typography", "voice", "logo", "imagery"];
17
+ /** Which facet of the brand a section documents. */
18
+ type SectionKind = (typeof SECTION_KINDS)[number];
19
+ /** Section review state: agent drafts, a human approves. */
20
+ declare const SECTION_STATUSES: readonly ["draft", "approved"];
21
+ /** Whether a section's content has human sign-off. */
22
+ type SectionStatus = (typeof SECTION_STATUSES)[number];
23
+ /** Palette lifecycle — palettes are never row-deleted, only archived. */
24
+ declare const PALETTE_STATUSES: readonly ["active", "archived"];
25
+ /** Whether a palette is the live one or a retired predecessor. */
26
+ type PaletteStatus = (typeof PALETTE_STATUSES)[number];
27
+ /** How a palette came to be: pulled from an asset, derived from a seed color,
28
+ * or entered by hand. */
29
+ declare const PALETTE_SOURCES: readonly ["extracted", "derived", "manual"];
30
+ /** Provenance of a palette's swatches. */
31
+ type PaletteSource = (typeof PALETTE_SOURCES)[number];
32
+ /** What a proposal proposes (palettes are the first-class case). */
33
+ declare const PROPOSAL_KINDS: readonly ["palette", "typography", "voice", "logo"];
34
+ /** Which brand facet a proposal targets. */
35
+ type ProposalKind = (typeof PROPOSAL_KINDS)[number];
36
+ /** Proposal lifecycle: open until a HUMAN accepts/rejects it (or a newer
37
+ * proposal supersedes it) — agents never resolve their own proposals. */
38
+ declare const PROPOSAL_STATUSES: readonly ["open", "accepted", "rejected", "superseded"];
39
+ /** Where a proposal stands in the human-gated review flow. */
40
+ type ProposalStatus = (typeof PROPOSAL_STATUSES)[number];
41
+ /** What kind of upload an asset row records. */
42
+ declare const ASSET_KINDS: readonly ["logo", "wordmark", "inspiration", "document", "font", "other"];
43
+ /** The declared purpose of an uploaded asset. */
44
+ type AssetKind = (typeof ASSET_KINDS)[number];
45
+ /** Every swatch role a palette may assign. `chart` may repeat (a series);
46
+ * `custom` is the escape hatch for roles the token compiler doesn't map. */
47
+ declare const SWATCH_ROLES: readonly ["primary", "secondary", "highlight", "bg", "surface", "text", "neutral", "good", "warn", "danger", "chart", "custom"];
48
+
49
+ /** Scalar values odla-db attrs and where-clauses accept. */
50
+ type BrandScalar = string | number | boolean | null;
51
+ /** Natural-key lookup ref for idempotent upserts (odla-db `Lookup` shape). */
52
+ interface BrandLookup {
53
+ ns: string;
54
+ attr: string;
55
+ value: BrandScalar;
56
+ }
57
+ /** How an op names its row: a raw entity id, or a natural-key lookup. */
58
+ type BrandEntityRef = string | BrandLookup;
59
+ /** Attr payload of one op. Typed `any` on purpose: the real odla-db
60
+ * `transact` constrains attrs to its own `Value` type, and `any` is the one
61
+ * shape assignable in both directions with no cast. */
62
+ type BrandAttrs = Record<string, any>;
63
+ /** One transact operation (update = attr-merge upsert; merge = deep-merge
64
+ * into a json attribute; retract = clear the named attributes, since a typed
65
+ * scalar can't store null). */
66
+ type BrandOp = {
67
+ t: "update";
68
+ ns: string;
69
+ id: BrandEntityRef;
70
+ attrs: BrandAttrs;
71
+ } | {
72
+ t: "merge";
73
+ ns: string;
74
+ id: BrandEntityRef;
75
+ attrs: BrandAttrs;
76
+ } | {
77
+ t: "retract";
78
+ ns: string;
79
+ id: BrandEntityRef;
80
+ attrs: string[];
81
+ } | {
82
+ t: "delete";
83
+ ns: string;
84
+ id: BrandEntityRef;
85
+ };
86
+ /** A row as odla-db returns it: hydrated id plus attrs. */
87
+ type BrandRow = {
88
+ id: string;
89
+ } & Record<string, unknown>;
90
+ /** Query result: namespace → rows. */
91
+ type BrandResult = Record<string, BrandRow[]>;
92
+ /** The role a swatch plays in the palette (what the token compiler maps). */
93
+ type SwatchRole = (typeof SWATCH_ROLES)[number];
94
+ /** One palette color: a role, a `#rrggbb` hex, and optional naming/why. */
95
+ interface Swatch {
96
+ role: SwatchRole;
97
+ hex: string;
98
+ name?: string;
99
+ rationale?: string;
100
+ }
101
+ /** `palette`-kind section content: the accepted palette, denormalized. */
102
+ interface PaletteSection {
103
+ paletteId: string;
104
+ name: string;
105
+ swatches: Swatch[];
106
+ }
107
+ /** `typography`-kind section content. `scale` is a modular type-scale ratio. */
108
+ interface TypographySection {
109
+ fontDisplay?: string;
110
+ fontBody?: string;
111
+ fontMono?: string;
112
+ scale?: number;
113
+ notes?: string;
114
+ }
115
+ /** `voice`-kind section content: tone plus writing principles/examples. */
116
+ interface VoiceSection {
117
+ tone: string;
118
+ principles: string[];
119
+ examples?: string[];
120
+ }
121
+ /** `logo`-kind section content: usage rules and don'ts. */
122
+ interface LogoSection {
123
+ clearspace?: string;
124
+ minSize?: string;
125
+ usage: string[];
126
+ donts: string[];
127
+ }
128
+ /** `imagery`-kind section content: photographic/illustration direction. */
129
+ interface ImagerySection {
130
+ style: string;
131
+ guidance: string[];
132
+ }
133
+ /** Agent-produced description of an uploaded asset (validated + capped). */
134
+ interface AssetAnalysis {
135
+ description: string;
136
+ dominantColors: string[];
137
+ tags: string[];
138
+ }
139
+ /** The compiled-token cache stored on `brand_book.tokens`. `warnings` carries
140
+ * the compiler's `TokenWarning[]` (typed loosely here so the row types don't
141
+ * depend on the token compiler's module). */
142
+ interface BrandTokensSnapshot {
143
+ light: Record<string, string>;
144
+ dark: Record<string, string>;
145
+ warnings: unknown[];
146
+ compiledAt: number;
147
+ }
148
+ /** A brand book row: the auth roster (`memberIds`, including the bot agent
149
+ * id) plus the active palette and compiled-token cache. */
150
+ interface BrandBook {
151
+ id: string;
152
+ slug: string;
153
+ name: string;
154
+ status: BookStatus;
155
+ ownerId: string;
156
+ memberIds: string[];
157
+ channelId?: string;
158
+ activePaletteId?: string;
159
+ tokens?: BrandTokensSnapshot;
160
+ summary?: string;
161
+ createdAt: number;
162
+ updatedAt: number;
163
+ }
164
+ /** A section row — one per (book, kind), upserted by its natural `key`. */
165
+ interface BrandSection {
166
+ id: string;
167
+ key: string;
168
+ bookId: string;
169
+ kind: SectionKind;
170
+ status: SectionStatus;
171
+ content: Record<string, unknown>;
172
+ audience: string[];
173
+ updatedBy: string;
174
+ updatedAt: number;
175
+ }
176
+ /** A palette row. Swatches are json ON the palette, not a namespace: the
177
+ * palette is the atomic proposal/approval unit and swatches are never
178
+ * queried across palettes. */
179
+ interface BrandPalette {
180
+ id: string;
181
+ bookId: string;
182
+ name: string;
183
+ status: PaletteStatus;
184
+ swatches: Swatch[];
185
+ seedHex?: string;
186
+ source: PaletteSource;
187
+ rationale?: string;
188
+ proposalId?: string;
189
+ audience: string[];
190
+ createdAt: number;
191
+ updatedAt: number;
192
+ }
193
+ /** A proposal row: agent output parked for explicit human resolution. */
194
+ interface BrandProposal {
195
+ id: string;
196
+ bookId: string;
197
+ kind: ProposalKind;
198
+ status: ProposalStatus;
199
+ payload: Record<string, unknown>;
200
+ rationale: string;
201
+ sourceAssetId?: string;
202
+ messageId?: string;
203
+ audience: string[];
204
+ createdBy: string;
205
+ createdAt: number;
206
+ resolvedBy?: string;
207
+ resolvedAt?: number;
208
+ resolutionNote?: string;
209
+ }
210
+ /** An asset row mirroring one real uploaded file (worker-mediated; rules
211
+ * close the namespace). Delete is a tombstone (`deletedAt`), never a row
212
+ * delete, so analyses/proposals referencing it stay coherent. */
213
+ interface BrandAsset {
214
+ id: string;
215
+ bookId: string;
216
+ kind: AssetKind;
217
+ path: string;
218
+ url: string;
219
+ contentType: string;
220
+ size: number;
221
+ title?: string;
222
+ analysis?: AssetAnalysis;
223
+ analyzedAt?: number;
224
+ audience: string[];
225
+ uploadedBy: string;
226
+ createdAt: number;
227
+ deletedAt?: number;
228
+ }
229
+ /** What `db.storage.upload` resolves to (odla-db's file record shape). */
230
+ interface BrandFileRecord {
231
+ id: string;
232
+ path: string;
233
+ url: string;
234
+ size: number;
235
+ contentType: string;
236
+ }
237
+ /** Bodies `storage.upload` accepts — a practical subset of the platform's
238
+ * `BodyInit` (which has no ambient type under a DOM-less lib). The real
239
+ * @odla-ai/db client accepts a superset, so it still structurally satisfies
240
+ * {@link BrandStorage}. */
241
+ type BrandUploadBody = string | Blob | ArrayBuffer | Uint8Array | ReadableStream<Uint8Array>;
242
+ /** The injected file store — @odla-ai/db's `storage` API satisfies this. */
243
+ interface BrandStorage {
244
+ upload(path: string, data: BrandUploadBody, contentType?: string): Promise<BrandFileRecord>;
245
+ delete(path: string): Promise<void>;
246
+ }
247
+ /** The injected odla client (structural — a real @odla-ai/db `AdminDb`
248
+ * satisfies it). `transact` accepts flat op arrays and `mutationId` gives
249
+ * exactly-once application (`duplicate: true` on replay). */
250
+ interface BrandDb {
251
+ query(q: Record<string, unknown>): Promise<Record<string, unknown[]>>;
252
+ transact(ops: unknown[], opts?: {
253
+ mutationId?: string;
254
+ }): Promise<{
255
+ txId: number;
256
+ duplicate?: boolean;
257
+ }>;
258
+ storage: BrandStorage;
259
+ }
260
+ /** Who is acting: a human member or the book's bot agent. */
261
+ interface BrandActor {
262
+ id: string;
263
+ kind: "human" | "agent";
264
+ email?: string;
265
+ }
266
+
267
+ /** Caller-fault error (bad hex, oversized payload, blocked transition…) — the
268
+ * route layer maps it to a 400. `fields` carries per-field messages when the
269
+ * failure came from input validation. */
270
+ declare class BrandInputError extends Error {
271
+ readonly fields?: Record<string, string>;
272
+ constructor(message: string, fields?: Record<string, string>);
273
+ }
274
+ /** Missing entity — the route layer maps it to a 404. */
275
+ declare class BrandNotFoundError extends Error {
276
+ constructor(what: string);
277
+ }
278
+
279
+ /** What `fetchBytes` resolves to: raw bytes plus the served content type. */
280
+ interface BrandFetchedBytes {
281
+ bytes: Uint8Array;
282
+ contentType: string;
283
+ }
284
+ /** What every brand operation needs: the injected db, plus test overrides. */
285
+ interface BrandDeps {
286
+ db: BrandDb;
287
+ /** Clock override (default `Date.now`). */
288
+ now?: () => number;
289
+ /** Id factory override (default `crypto.randomUUID`). */
290
+ newId?: () => string;
291
+ /** Asset-byte fetcher override (default: global `fetch`). */
292
+ fetchBytes?: (url: string) => Promise<BrandFetchedBytes>;
293
+ }
294
+ /** {@link BrandDeps} with the injectable defaults filled in. */
295
+ interface ResolvedBrandDeps {
296
+ db: BrandDb;
297
+ now: () => number;
298
+ newId: () => string;
299
+ fetchBytes: (url: string) => Promise<BrandFetchedBytes>;
300
+ }
301
+ /** Fill the injectable defaults. */
302
+ declare function resolveDeps(deps: BrandDeps): ResolvedBrandDeps;
303
+
304
+ /** Wire-shape attribute value kinds odla-db stores. */
305
+ type AttrType = "string" | "number" | "boolean" | "date" | "json";
306
+ /** One serialized attribute: its type and index/uniqueness flags. */
307
+ interface SerializedAttr {
308
+ type: AttrType;
309
+ unique: boolean;
310
+ indexed: boolean;
311
+ optional: boolean;
312
+ }
313
+ /** One serialized namespace: its attribute map. */
314
+ interface SerializedEntity {
315
+ attrs: Record<string, SerializedAttr>;
316
+ }
317
+ /** One end of a serialized schema link. */
318
+ interface SerializedLinkEnd {
319
+ on: string;
320
+ has: "one" | "many";
321
+ label: string;
322
+ }
323
+ /** A serialized schema link (unused by brand — audiences are denormalized). */
324
+ interface SerializedLink {
325
+ forward: SerializedLinkEnd;
326
+ reverse: SerializedLinkEnd;
327
+ }
328
+ /** The wire-shape schema POSTed to `/app/:id/schema`. */
329
+ interface SerializedSchema {
330
+ entities: Record<string, SerializedEntity>;
331
+ links: Record<string, SerializedLink>;
332
+ }
333
+ /**
334
+ * The pre-serialized (wire-shape) odla-db schema for the five brand
335
+ * namespaces; POST it to `/app/:id/schema` as `{ schema: BRAND_SCHEMA }`.
336
+ * Natural keys (`book.slug`, `section.key`) make provisioning and section
337
+ * upserts idempotent; mirrored `id` attrs make rows addressable by
338
+ * `where: { id }`.
339
+ */
340
+ declare const BRAND_SCHEMA: SerializedSchema;
341
+
342
+ /** Per-namespace CEL rule strings (missing action = deny). */
343
+ interface BrandRule {
344
+ view?: string;
345
+ create?: string;
346
+ update?: string;
347
+ delete?: string;
348
+ }
349
+ /** Namespace → rule set, as installed at `/app/:id/admin/rules`. */
350
+ type BrandRules = Record<string, BrandRule>;
351
+ /**
352
+ * Default-deny CEL rules for the five brand namespaces; install at
353
+ * `/app/:id/admin/rules`. Books gate on the `memberIds` roster (owner-only
354
+ * writes); sections/palettes/proposals gate on their denormalized `audience`
355
+ * and are never client-deletable (archive via `status` instead); assets are
356
+ * fully worker-mediated. Use {@link brandRules} for a per-install copy.
357
+ */
358
+ declare const BRAND_RULES: BrandRules;
359
+ /**
360
+ * Rules factory, mirroring `chatRules()`/`crmRules()`. Today it returns a
361
+ * fresh copy of {@link BRAND_RULES}; it exists so a future option (e.g.
362
+ * locking views to an org email domain) can widen or tighten namespaces
363
+ * without apps changing their provisioning call shape.
364
+ */
365
+ declare function brandRules(): BrandRules;
366
+
367
+ /** Most swatches a single palette may carry. */
368
+ declare const MAX_SWATCHES = 24;
369
+ /** Upload content types the asset pipeline accepts. */
370
+ declare const ASSET_CONTENT_TYPES: ReadonlySet<string>;
371
+ /**
372
+ * Assert `value` is a `#rgb`/`#rrggbb` hex color and normalize it to
373
+ * lowercase `#rrggbb`. Alpha channels (`#rgba`/`#rrggbbaa`) and every other
374
+ * color syntax are rejected — tokens and contrast math are defined on opaque
375
+ * sRGB hex.
376
+ */
377
+ declare function assertHex(value: unknown, label?: string): string;
378
+ /** Assert a trimmed non-empty string of at most `max` characters. */
379
+ declare function capString(value: unknown, label: string, max: number): string;
380
+ /** Assert an array of capped strings (each trimmed and non-empty). */
381
+ declare function capStringArray(value: unknown, label: string, opts: {
382
+ maxItems: number;
383
+ maxLen: number;
384
+ minItems?: number;
385
+ }): string[];
386
+ /**
387
+ * Assert a swatch list: 1–{@link MAX_SWATCHES} entries, each with a known
388
+ * role, a valid hex (normalized), and capped optional name/rationale.
389
+ * Returns the normalized copy — write THAT, never the raw input.
390
+ */
391
+ declare function assertSwatches(value: unknown): Swatch[];
392
+ /**
393
+ * Validate + normalize a section's `content` payload for its kind. Returns
394
+ * the normalized content to write; throws {@link BrandInputError} on an
395
+ * unknown kind or an invalid payload.
396
+ */
397
+ declare function assertSectionContent(kind: string, content: unknown): Record<string, unknown>;
398
+ /**
399
+ * Sanitize an upload's file name for use in an R2 key: strips path
400
+ * separators, control characters, and leading dots, then caps at 120
401
+ * characters. Throws when nothing survives sanitizing.
402
+ */
403
+ declare function safeFileName(name: unknown): string;
404
+ /**
405
+ * Assert an upload content type is on the {@link ASSET_CONTENT_TYPES}
406
+ * allowlist. Normalizes case and strips parameters (`; charset=…`) before
407
+ * checking; returns the normalized bare type.
408
+ */
409
+ declare function assertAssetContentType(value: unknown): string;
410
+ /**
411
+ * Validate + normalize an agent's asset analysis: description ≤ 2000 chars,
412
+ * ≤ 12 dominant colors (each normalized hex), ≤ 24 tags of ≤ 60 chars.
413
+ */
414
+ declare function assertAnalysis(value: unknown): AssetAnalysis;
415
+
416
+ /** Input to {@link createBookOps}. `memberIds` is the full auth roster —
417
+ * include the bot agent's id so it can pass the audience rules. */
418
+ interface CreateBookInput {
419
+ id: string;
420
+ slug: string;
421
+ name: string;
422
+ ownerId: string;
423
+ memberIds: string[];
424
+ channelId?: string;
425
+ now: number;
426
+ }
427
+ /**
428
+ * Ops to create a brand book: one `brand_book` row in `draft` status with the
429
+ * id mirrored as an attr (odla-db ids aren't attrs), the owner always folded
430
+ * into the deduplicated `memberIds` roster (the create rule requires it), and
431
+ * both timestamps set to `now`.
432
+ */
433
+ declare function createBookOps(input: CreateBookInput): BrandOp[];
434
+ /**
435
+ * Ops to patch a brand book. `null` means CLEAR (a `retract` op — odla-db has
436
+ * no storable scalar null) and is only allowed on optional columns;
437
+ * `undefined` keys are dropped. Any write stamps `updatedAt`; an empty
438
+ * effective patch emits no ops at all.
439
+ */
440
+ declare function updateBookOps(bookId: BrandEntityRef, patch: Record<string, unknown>, now: number): BrandOp[];
441
+ /** The child rows whose `audience` snapshots a roster change must rewrite.
442
+ * Pass the rows you queried — the builder stays pure. */
443
+ interface AudienceChildren {
444
+ sections?: Array<{
445
+ id: string;
446
+ }>;
447
+ palettes?: Array<{
448
+ id: string;
449
+ }>;
450
+ proposals?: Array<{
451
+ id: string;
452
+ }>;
453
+ assets?: Array<{
454
+ id: string;
455
+ }>;
456
+ }
457
+ /**
458
+ * Ops to change a book's roster: rewrite `memberIds` (owner always kept) and
459
+ * fan the new list out to every child row's denormalized `audience` snapshot.
460
+ * Query the children first and pass them in — this builder does no I/O, so
461
+ * tests can assert the exact fan-out.
462
+ */
463
+ declare function audienceFanoutOps(book: Pick<BrandBook, "id" | "ownerId">, newMemberIds: string[], children: AudienceChildren, now: number): BrandOp[];
464
+
465
+ /** The natural key a section row is upserted by: `${bookId}:${kind}`. */
466
+ declare function sectionKey(bookId: string, kind: SectionKind): string;
467
+ /** Input to {@link upsertSectionOps}. `audience` is the book's roster
468
+ * snapshot; `status` defaults to `draft` (a human approves later). */
469
+ interface UpsertSectionInput {
470
+ bookId: string;
471
+ kind: SectionKind;
472
+ content: Record<string, unknown>;
473
+ status?: SectionStatus;
474
+ audience: string[];
475
+ updatedBy: string;
476
+ now: number;
477
+ }
478
+ /**
479
+ * Ops to create-or-replace a book's section of one kind: a single `update`
480
+ * addressed by the `{ ns, attr: "key", value }` Lookup ref, carrying the
481
+ * validated + normalized content (see `assertSectionContent`), the audience
482
+ * snapshot, and the author/timestamp.
483
+ */
484
+ declare function upsertSectionOps(input: UpsertSectionInput): BrandOp[];
485
+
486
+ /** Input to {@link proposePaletteOps}. `audience` is the book's roster
487
+ * snapshot; `contrastReport` is stored verbatim in the payload for the
488
+ * human reviewer. */
489
+ interface ProposePaletteInput {
490
+ id: string;
491
+ bookId: string;
492
+ name: string;
493
+ rationale: string;
494
+ swatches: unknown;
495
+ seedHex?: string;
496
+ contrastReport?: unknown;
497
+ createdBy: string;
498
+ audience: string[];
499
+ sourceAssetId?: string;
500
+ messageId?: string;
501
+ now: number;
502
+ }
503
+ /**
504
+ * Ops to park a palette proposal for human review: one `brand_proposal` row,
505
+ * `kind: "palette"`, `status: "open"`, with the validated swatches (and
506
+ * optional seed/contrast report) in `payload`.
507
+ */
508
+ declare function proposePaletteOps(input: ProposePaletteInput): BrandOp[];
509
+ /** Input to {@link acceptProposalOps}: the proposal row as read back, the id
510
+ * to mint the palette under, and who resolved it. */
511
+ interface AcceptProposalInput {
512
+ proposal: BrandProposal;
513
+ paletteId: string;
514
+ resolvedBy: string;
515
+ now: number;
516
+ resolutionNote?: string;
517
+ }
518
+ /**
519
+ * Ops to accept an OPEN palette proposal, all in one transact:
520
+ * 1. the `brand_palette` row (source: `extracted` when the proposal came from
521
+ * an asset, `derived` when seeded, else `manual`),
522
+ * 2. the approved `palette` section upsert,
523
+ * 3. the book's `activePaletteId` pointer,
524
+ * 4. the proposal flipped to `accepted` with resolver + timestamp.
525
+ * Throws on a non-palette or already-resolved proposal, or a payload that no
526
+ * longer validates.
527
+ */
528
+ declare function acceptProposalOps(input: AcceptProposalInput): BrandOp[];
529
+ /** Input to {@link rejectProposalOps}. */
530
+ interface RejectProposalInput {
531
+ proposal: BrandProposal;
532
+ resolvedBy: string;
533
+ now: number;
534
+ resolutionNote?: string;
535
+ }
536
+ /**
537
+ * Ops to reject an OPEN proposal (any kind): flip it to `rejected` with
538
+ * resolver, timestamp, and the optional note. Nothing else is touched — the
539
+ * proposal row itself is the audit trail.
540
+ */
541
+ declare function rejectProposalOps(input: RejectProposalInput): BrandOp[];
542
+
543
+ /** Input to {@link createAssetOps} — the storage upload's receipt fields
544
+ * (`path`/`url`/`size`) plus the declared kind and the book's audience. */
545
+ interface CreateAssetInput {
546
+ id: string;
547
+ bookId: string;
548
+ kind: AssetKind;
549
+ path: string;
550
+ url: string;
551
+ contentType: string;
552
+ size: number;
553
+ uploadedBy: string;
554
+ audience: string[];
555
+ title?: string;
556
+ now: number;
557
+ }
558
+ /**
559
+ * Ops to record one uploaded asset: a single `brand_asset` row mirroring the
560
+ * storage object, with the content type re-checked against the allowlist and
561
+ * the size required to be a positive byte count.
562
+ */
563
+ declare function createAssetOps(input: CreateAssetInput): BrandOp[];
564
+ /**
565
+ * Ops to tombstone an asset after its storage object was deleted: stamp
566
+ * `deletedAt` in place. List reads exclude tombstoned rows; the row itself
567
+ * stays for provenance.
568
+ */
569
+ declare function tombstoneAssetOps(assetId: BrandEntityRef, now: number): BrandOp[];
570
+ /**
571
+ * Ops to record an agent's analysis of an asset: the validated + capped
572
+ * analysis json plus `analyzedAt`. Replaces any prior analysis whole.
573
+ */
574
+ declare function recordAnalysisOps(assetId: BrandEntityRef, analysis: unknown, now: number): BrandOp[];
575
+
576
+ /** An sRGB color with channels as fractions in [0, 1]. */
577
+ interface Rgb {
578
+ /** Red channel, 0..1. */
579
+ r: number;
580
+ /** Green channel, 0..1. */
581
+ g: number;
582
+ /** Blue channel, 0..1. */
583
+ b: number;
584
+ }
585
+ /** Clamps a number into [0, 1]; NaN clamps to 0. */
586
+ declare function clamp01(x: number): number;
587
+ /**
588
+ * Parses `#rgb` or `#rrggbb` (case-insensitive) into channel fractions.
589
+ *
590
+ * @throws RangeError for anything else — named colors, missing `#`, wrong
591
+ * digit counts; alpha forms (`#rgba`/`#rrggbbaa`) get a dedicated message.
592
+ */
593
+ declare function parseHex(hex: string): Rgb;
594
+ /** Formats channel fractions as lowercase `#rrggbb`, clamping each into [0, 1]. */
595
+ declare function toHex(rgb: Rgb): string;
596
+ /** Normalizes any accepted hex form to lowercase `#rrggbb` (throws like parseHex). */
597
+ declare function normalizeHex(hex: string): string;
598
+
599
+ /** A color in HSL: hue in degrees [0, 360), saturation and lightness 0..1. */
600
+ interface Hsl {
601
+ /** Hue angle in degrees, [0, 360); 0 when achromatic. */
602
+ h: number;
603
+ /** Saturation, 0..1. */
604
+ s: number;
605
+ /** Lightness, 0..1. */
606
+ l: number;
607
+ }
608
+ /** A color in OKLab: L 0..1, a/b roughly within ±0.4 for sRGB colors. */
609
+ interface Oklab {
610
+ /** Perceived lightness, 0..1. */
611
+ L: number;
612
+ /** Green–red axis. */
613
+ a: number;
614
+ /** Blue–yellow axis. */
615
+ b: number;
616
+ }
617
+ /** A color in OKLCH — OKLab in polar form. */
618
+ interface Oklch {
619
+ /** Perceived lightness, 0..1. */
620
+ L: number;
621
+ /** Chroma (radius in the a/b plane), ≥ 0. */
622
+ C: number;
623
+ /** Hue angle in degrees, [0, 360); 0 when achromatic. */
624
+ h: number;
625
+ }
626
+ /** IEC 61966-2-1 sRGB decoding: gamma-encoded channel → linear-light, both 0..1. */
627
+ declare function srgbToLinear(c: number): number;
628
+ /** IEC 61966-2-1 sRGB encoding: linear-light channel → gamma-encoded, both 0..1. */
629
+ declare function linearToSrgb(c: number): number;
630
+ /** Converts sRGB channel fractions to HSL (CSS Color 4 § 7). */
631
+ declare function rgbToHsl({ r, g, b }: Rgb): Hsl;
632
+ /** Converts HSL to sRGB channel fractions (CSS Color 4 § 7; any hue angle accepted). */
633
+ declare function hslToRgb({ h, s, l }: Hsl): Rgb;
634
+ /** Converts gamma-encoded sRGB to OKLab (Ottosson M1 → cbrt → M2). */
635
+ declare function rgbToOklab({ r, g, b }: Rgb): Oklab;
636
+ /**
637
+ * Converts OKLab to gamma-encoded sRGB (Ottosson's inverse matrices).
638
+ * Out-of-gamut inputs yield channels outside [0, 1] — feed the OKLCH form
639
+ * through {@link clampToGamut} first when hue fidelity matters (toHex would
640
+ * clip per channel, which distorts hue).
641
+ */
642
+ declare function oklabToRgb({ L, a, b }: Oklab): Rgb;
643
+ /** OKLab → OKLCH (polar form). Hue is 0 for achromatic colors (C ≈ 0). */
644
+ declare function oklabToOklch({ L, a, b }: Oklab): Oklch;
645
+ /** OKLCH → OKLab (rectangular form). */
646
+ declare function oklchToOklab({ L, C, h }: Oklch): Oklab;
647
+ /** Convenience: hex → OKLCH (throws like parseHex on bad input). */
648
+ declare function hexToOklch(hex: string): Oklch;
649
+ /**
650
+ * Convenience: OKLCH → lowercase `#rrggbb`. Out-of-gamut input is clipped
651
+ * per channel by toHex; call {@link clampToGamut} first to reduce chroma
652
+ * instead (preserving hue) when the input may be out of gamut.
653
+ */
654
+ declare function oklchToHex(lch: Oklch): string;
655
+ /** True when every sRGB channel is within [0, 1] (tiny epsilon for float noise). */
656
+ declare function inSrgbGamut({ r, g, b }: Rgb): boolean;
657
+ /**
658
+ * Brings an OKLCH color into the sRGB gamut by reducing chroma only —
659
+ * lightness and hue are preserved exactly (binary search, 24 iterations,
660
+ * chroma precision well under one 8-bit quantum). L is clamped into [0, 1]
661
+ * first; C = 0 (a pure gray) is always representable, so the search always
662
+ * lands in gamut.
663
+ */
664
+ declare function clampToGamut(lch: Oklch): Oklch;
665
+
666
+ /** Contrast contexts with distinct WCAG thresholds. */
667
+ type ContrastKind = "text" | "large-text" | "ui";
668
+ /** WCAG 2.1 relative luminance of a hex color: 0 = black, 1 = white. */
669
+ declare function relativeLuminance(hex: string): number;
670
+ /** WCAG 2.1 contrast ratio between two hex colors, in [1, 21]; order-independent. */
671
+ declare function contrastRatio(hexA: string, hexB: string): number;
672
+ /**
673
+ * WCAG 2.1 level-AA check for a contrast ratio: 4.5:1 for body text
674
+ * (SC 1.4.3), 3:1 for large text, 3:1 for UI components and graphical
675
+ * objects (SC 1.4.11).
676
+ */
677
+ declare function meetsAA(ratio: number, kind?: ContrastKind): boolean;
678
+ /**
679
+ * WCAG 2.1 level-AAA check for a contrast ratio (SC 1.4.6): 7:1 for body
680
+ * text, 4.5:1 for large text. WCAG defines no AAA bar for non-text UI, so
681
+ * "ui" is not an accepted kind here.
682
+ */
683
+ declare function meetsAAA(ratio: number, kind?: Exclude<ContrastKind, "ui">): boolean;
684
+ /**
685
+ * Default {@link pickTextOn} candidates: white and a near-black chosen so
686
+ * that the better of the two clears 4.5:1 against ANY background. The worst
687
+ * case is a mid-gray backdrop (luminance ≈ 0.18), where this pair still
688
+ * yields ≈ 4.51:1; a softer near-black (e.g. #111111) would drop that floor
689
+ * below 4.5.
690
+ */
691
+ declare const PICK_TEXT_DEFAULT_CANDIDATES: readonly string[];
692
+ /**
693
+ * Picks the candidate with the highest contrast ratio against `bgHex`
694
+ * (earliest candidate wins ties). With the default candidates the winner is
695
+ * always ≥ 4.5:1 — see {@link PICK_TEXT_DEFAULT_CANDIDATES}.
696
+ *
697
+ * @throws RangeError when `candidates` is empty.
698
+ */
699
+ declare function pickTextOn(bgHex: string, candidates?: readonly string[]): string;
700
+
701
+ /**
702
+ * Rotates a color's OKLCH hue by `degrees` (any sign/magnitude), keeping
703
+ * lightness and chroma constant, then chroma-clamps into sRGB. Rotating an
704
+ * achromatic color is a no-op up to 8-bit rounding (its chroma is ~0).
705
+ */
706
+ declare function rotateHue(hex: string, degrees: number): string;
707
+ /** The 180° opposite on the OKLCH hue wheel. */
708
+ declare function complementary(hex: string): string;
709
+ /** The two neighbors at ±`angle` (default 30°), as [minus, plus]. */
710
+ declare function analogous(hex: string, angle?: number): [string, string];
711
+ /** The two colors completing an equilateral triad (±120°), as [minus, plus]. */
712
+ declare function triadic(hex: string): [string, string];
713
+ /** The two colors flanking the complement (±150°), as [minus, plus]. */
714
+ declare function splitComplementary(hex: string): [string, string];
715
+ /** The three colors completing a square (+90°, +180°, +270°). */
716
+ declare function tetradic(hex: string): [string, string, string];
717
+ /**
718
+ * A monochrome chroma scale: `steps` colors at the seed's lightness and hue,
719
+ * fading chroma linearly from the seed's own chroma down to 0 (a pure gray).
720
+ * The first entry is the seed itself (normalized).
721
+ *
722
+ * @throws RangeError when `steps` is not an integer ≥ 2.
723
+ */
724
+ declare function monochrome(hex: string, steps?: number): string[];
725
+
726
+ /** Lightest OKLab L emitted by {@link tintShadeRamp} (first step). */
727
+ declare const RAMP_L_MAX = 0.96;
728
+ /** Darkest OKLab L emitted by {@link tintShadeRamp} (last step). */
729
+ declare const RAMP_L_MIN = 0.27;
730
+ /**
731
+ * A light-to-dark ramp of `steps` colors (default 9) built from the seed's
732
+ * hue and chroma: OKLab lightness runs linearly from {@link RAMP_L_MAX} down
733
+ * to {@link RAMP_L_MIN}, and chroma tapers from 100% of the seed's chroma at
734
+ * mid-ramp to 25% at both ends (then gamut-clamps, preserving hue).
735
+ *
736
+ * @throws RangeError when `steps` is not an integer ≥ 2.
737
+ */
738
+ declare function tintShadeRamp(hex: string, steps?: number): string[];
739
+ /** Direction {@link adjustLightnessUntil} may move lightness. */
740
+ type LightnessDirection = "lighten" | "darken";
741
+ /**
742
+ * Moves a color's OKLCH lightness toward white ("lighten") or black
743
+ * ("darken") until `predicate(hex)` holds, returning the passing color
744
+ * nearest the input (chroma and hue constant, gamut-clamped per step).
745
+ *
746
+ * Returns the input itself (normalized) when it already passes, and null
747
+ * when no lightness in that direction satisfies the predicate. Work is
748
+ * bounded: a 16-step coarse scan plus 22 bisection rounds. The predicate is
749
+ * always evaluated on exact 8-bit hex strings, including the returned one.
750
+ */
751
+ declare function adjustLightnessUntil(hex: string, predicate: (hex: string) => boolean, direction: LightnessDirection): string | null;
752
+
753
+ /** Euclidean distance between two OKLab coordinates (CSS Color 4 ΔEOK). */
754
+ declare function deltaEOKLab(x: Oklab, y: Oklab): number;
755
+ /**
756
+ * ΔEOK between two hex colors. 0 = identical; ~0.02 ≈ one just-noticeable
757
+ * difference; white ↔ black = 1. Symmetric in its arguments.
758
+ *
759
+ * @throws RangeError on malformed hex (see parseHex).
760
+ */
761
+ declare function deltaEOK(hexA: string, hexB: string): number;
762
+
763
+ /** One CSS named color: the keyword and its spec hex value. */
764
+ interface NamedColor {
765
+ /** The CSS keyword, e.g. "rebeccapurple". */
766
+ name: string;
767
+ /** The spec's `#rrggbb` value, lowercase. */
768
+ hex: string;
769
+ }
770
+ /**
771
+ * All 148 CSS named colors in alphabetical order. Aliases (aqua/cyan,
772
+ * fuchsia/magenta, the gray/grey pairs) are separate entries sharing a hex.
773
+ */
774
+ declare const CSS_NAMED_COLORS: readonly NamedColor[];
775
+ /** A nearest-name match: the keyword, its spec hex, and the ΔEOK distance. */
776
+ interface NearestNamedColor extends NamedColor {
777
+ /** ΔEOK from the query color to this named color (0 = exact hit). */
778
+ deltaEOK: number;
779
+ }
780
+ /**
781
+ * The CSS named color perceptually closest to `hex`, by ΔEOK in OKLab.
782
+ * Exact hits return distance 0. Ties — including shared-hex aliases like
783
+ * aqua/cyan — go to the alphabetically first keyword. As a rough guide,
784
+ * distances ≲ 0.02 are visually indistinguishable and ≳ 0.1 is only a loose
785
+ * "same family" match.
786
+ *
787
+ * @throws RangeError on malformed hex (see parseHex).
788
+ */
789
+ declare function nearestNamedColor(hex: string): NearestNamedColor;
790
+
791
+ /**
792
+ * Documented floor for pairwise ΔEOK between derived chart colors. The
793
+ * default chart constants guarantee it by construction (see file header);
794
+ * deriveChartColors also enforces it as a filter.
795
+ */
796
+ declare const CHART_DELTA_MIN = 0.1;
797
+ /** Options for {@link deriveChartColors}. */
798
+ interface DeriveChartOptions {
799
+ /** How many colors to return, 1..12 (default 6). */
800
+ count?: number;
801
+ /** Pairwise ΔEOK floor (default {@link CHART_DELTA_MIN}). Best-effort above the default. */
802
+ deltaMin?: number;
803
+ }
804
+ /**
805
+ * Derives categorical chart colors from a seed: hue rotations (60° grid,
806
+ * then 30° offsets) around the seed's hue at a constant, gamut-safe
807
+ * lightness/chroma. Candidates are kept only if they stay at least
808
+ * `deltaMin` ΔEOK from every accepted color; if the requested floor is
809
+ * unattainable for `count` colors, remaining distinct candidates fill the
810
+ * set in hue order so callers always get `count` colors back.
811
+ *
812
+ * @throws RangeError when `count` is not an integer in 1..12, or on
813
+ * malformed hex.
814
+ */
815
+ declare function deriveChartColors(seedHex: string, opts?: DeriveChartOptions): string[];
816
+ /** Options for {@link derivePalette}. */
817
+ interface DerivePaletteOptions {
818
+ /** Attach nearest-CSS-keyword `name`s to every swatch (default true). */
819
+ names?: boolean;
820
+ }
821
+ /**
822
+ * Derives a complete brand palette from one seed color: one swatch each for
823
+ * primary, secondary, highlight, good, warn, danger, bg, surface, text and
824
+ * neutral, plus six chart swatches (16 total) — see the file header for the
825
+ * derivation strategy and its WCAG guarantees.
826
+ *
827
+ * @throws RangeError on malformed seed hex (see parseHex).
828
+ */
829
+ declare function derivePalette(seedHex: string, opts?: DerivePaletteOptions): Swatch[];
830
+
831
+ /**
832
+ * The @odla-ai/ui required tier (js/tokens.js REQUIRED_TOKENS), in the ui
833
+ * contract's order. The compiler always emits a concrete value for every
834
+ * one of these.
835
+ */
836
+ declare const BRAND_REQUIRED_TOKENS: readonly ["--ui-bg", "--ui-surface", "--ui-surface-2", "--ui-text", "--ui-text-muted", "--ui-text-faint", "--ui-border", "--ui-border-strong", "--ui-accent", "--ui-accent-strong", "--ui-accent-soft", "--ui-on-accent", "--ui-good", "--ui-good-soft", "--ui-warn", "--ui-warn-soft", "--ui-danger", "--ui-danger-soft", "--ui-code-bg", "--ui-code-text", "--ui-shadow", "--ui-font-sans", "--ui-font-serif", "--ui-font-mono", "--ui-font-display"];
837
+ /**
838
+ * Accent-composing derived roles from the ui defaulted tier. All five
839
+ * accent-family members of the :where([data-ui-accent]) re-declaration set
840
+ * (--ui-accent-glow, --ui-accent-2, --ui-accent-2-soft, --ui-highlight,
841
+ * --ui-focus) plus --ui-shadow-strong, which composes var(--ui-shadow) —
842
+ * a token this compiler re-declares, so the island rule applies to it too.
843
+ */
844
+ declare const BRAND_DERIVED_TOKENS: readonly ["--ui-accent-glow", "--ui-accent-2", "--ui-accent-2-soft", "--ui-highlight", "--ui-focus", "--ui-shadow-strong"];
845
+ /**
846
+ * Chart roles the compiler emits: the six series slots (real palette
847
+ * swatches when the palette carries them, ui var() compositions otherwise)
848
+ * and the four accent-composing chart roles the :where([data-ui-accent])
849
+ * block re-declares (band, band-strong, flow, glow).
850
+ */
851
+ declare const BRAND_CHART_TOKENS: readonly ["--ui-chart-1", "--ui-chart-2", "--ui-chart-3", "--ui-chart-4", "--ui-chart-5", "--ui-chart-6", "--ui-chart-band", "--ui-chart-band-strong", "--ui-chart-flow", "--ui-chart-glow"];
852
+ /**
853
+ * Chat surface roles (js/tokens.js CHAT_TOKENS, complete). Two are in the
854
+ * accent-swap set (user-bg, tool-accent); the other four compose surface /
855
+ * text tokens the compiler also re-declares, so an island stays coherent.
856
+ */
857
+ declare const BRAND_CHAT_TOKENS: readonly ["--ui-chat-user-bg", "--ui-chat-user-text", "--ui-chat-assistant-bg", "--ui-chat-thinking-bg", "--ui-chat-thinking-text", "--ui-chat-tool-accent"];
858
+ /**
859
+ * Every token the compiler emits, in emission order — also the deterministic
860
+ * declaration order renderTokensCss uses, so golden CSS fixtures are stable.
861
+ */
862
+ declare const BRAND_EMITTED_TOKENS: readonly string[];
863
+
864
+ /**
865
+ * A compiled token map: `--ui-*` custom-property name → CSS value. Values
866
+ * are either concrete (hex, font stack, shadow) or composition strings
867
+ * (`var(--ui-accent)`, `color-mix(in srgb, var(--ui-accent) 10%, transparent)`)
868
+ * that recompute wherever the map is declared — which is what lets one
869
+ * object serve as both a :root theme and a scoped override-island payload.
870
+ */
871
+ type BrandTokens = Record<string, string>;
872
+ /**
873
+ * A non-fatal compiler note: a missing swatch role that fell back to a
874
+ * derived value, a dropped invalid input, or a color the compiler had to
875
+ * move to clear a WCAG contrast bar (then `adjustedFrom` carries the
876
+ * original hex).
877
+ */
878
+ interface TokenWarning {
879
+ /** The `--ui-*` token the note is about. */
880
+ token: string;
881
+ /** Human-readable explanation, actionable for the brand author. */
882
+ message: string;
883
+ /** The original hex, when the compiler adjusted a color for contrast. */
884
+ adjustedFrom?: string;
885
+ }
886
+ /** Input to {@link import("./compile").compileBrandTokens}. */
887
+ interface CompileInput {
888
+ /** The palette to compile — typically a brand_palette row's swatches. */
889
+ swatches: Swatch[];
890
+ /** Optional typography section; fonts get system-stack fallbacks appended. */
891
+ typography?: TypographySection;
892
+ /** Author escape hatch: applied verbatim onto the light map, last. */
893
+ overrides?: BrandTokens;
894
+ }
895
+ /** The compiler's result: light + derived dark token maps, plus notes. */
896
+ interface CompiledBrandTokens {
897
+ /** Light-mode tokens (every name in BRAND_EMITTED_TOKENS, plus overrides). */
898
+ light: BrandTokens;
899
+ /** Dark-mode tokens derived from `light` — same key set. */
900
+ dark: BrandTokens;
901
+ /** Fallbacks taken and contrast adjustments made; empty on a clean palette. */
902
+ warnings: TokenWarning[];
903
+ }
904
+
905
+ /**
906
+ * Seed used when the palette has no usable chromatic swatch at all — the
907
+ * @odla-ai/ui neutral default accent (css/tokens.css restrained blue).
908
+ */
909
+ declare const DEFAULT_ACCENT_SEED = "#3b5e8c";
910
+ /** What {@link mapPaletteToTokens} returns. */
911
+ interface MapResult {
912
+ /** The light-mode token map — every name in BRAND_EMITTED_TOKENS. */
913
+ tokens: BrandTokens;
914
+ /** Fallbacks taken and contrast adjustments made. */
915
+ warnings: TokenWarning[];
916
+ }
917
+ /**
918
+ * Maps a palette (+ optional typography) onto the full light-mode
919
+ * @odla-ai/ui token set. Never throws on bad palettes: invalid swatches are
920
+ * dropped, missing roles derive from the accent via derivePalette, and every
921
+ * degradation is recorded as a {@link TokenWarning}.
922
+ */
923
+ declare function mapPaletteToTokens(input: CompileInput): MapResult;
924
+
925
+ /** Darkest OKLab L a flipped neutral may take — the dark bg floor (never black). */
926
+ declare const DARK_FLIP_L_MIN = 0.2;
927
+ /** Lightest OKLab L a flipped neutral may take — keeps flipped text off pure white. */
928
+ declare const DARK_FLIP_L_MAX = 0.95;
929
+ /**
930
+ * Derives the dark-mode token map from a light one (see the file header for
931
+ * the flip / re-lighten / passthrough rules). Pure and total: tokens the
932
+ * rules don't recognize — including non-hex values in recognized slots —
933
+ * pass through unchanged, and the result always has exactly the input's
934
+ * key set. Deterministic, never throws.
935
+ */
936
+ declare function deriveDarkTokens(light: BrandTokens): BrandTokens;
937
+
938
+ /** Options for {@link renderTokensCss}. */
939
+ interface RenderTokensCssOptions {
940
+ /** Dark-mode tokens (deriveDarkTokens output). Omit for a light-only sheet. */
941
+ dark?: BrandTokens;
942
+ /**
943
+ * Selector the light block declares on (default ":root"). A scoped
944
+ * selector (e.g. `[data-brand="acme"]`) gets its dark blocks nested under
945
+ * the document-level theme guards instead of replacing them.
946
+ */
947
+ selector?: string;
948
+ /**
949
+ * Also emit a `.ui-invert` block carrying the full dark payload, so the
950
+ * subtree renders dark regardless of the global mode. Requires `dark`.
951
+ */
952
+ includeInvert?: boolean;
953
+ }
954
+ /**
955
+ * Renders a compiled token map (plus optional dark map) as CSS text shaped
956
+ * like an @odla-ai/ui theme tokens file — see the file header for the block
957
+ * structure and ordering guarantees. Deterministic for a given input.
958
+ */
959
+ declare function renderTokensCss(light: BrandTokens, opts?: RenderTokensCssOptions): string;
960
+
961
+ /**
962
+ * Compiles a brand palette into @odla-ai/ui design tokens: a light map
963
+ * covering every name in BRAND_EMITTED_TOKENS (plus overrides), a dark map
964
+ * derived from it (same key set), and the warnings accumulated along the
965
+ * way — missing-role fallbacks and contrast adjustments (`adjustedFrom`).
966
+ *
967
+ * Both maps double as runtime override-island payloads: every derived
968
+ * default that composes the accent family is re-declared, so applying the
969
+ * map on any element recomputes charts, softs, focus and chat roles against
970
+ * the brand palette (see roles.ts). Deterministic; never throws on bad
971
+ * palettes.
972
+ */
973
+ declare function compileBrandTokens(input: CompileInput): CompiledBrandTokens;
974
+
975
+ /** The bot identity the skill writes as (`selfId` = the agent id carried in
976
+ * the book's `memberIds` roster and every audience snapshot). */
977
+ interface BrandSkillSelf {
978
+ selfId: string;
979
+ kind: "bot";
980
+ displayName?: string;
981
+ }
982
+ /** Options for {@link brandSkill}. */
983
+ interface BrandSkillOpts {
984
+ /** The injected odla client (a real @odla-ai/db AdminDb satisfies it). */
985
+ db: BrandDb;
986
+ /** The one brand book this skill instance is scoped to. */
987
+ bookId: string;
988
+ /** The bot identity acting through these tools. */
989
+ self: BrandSkillSelf;
990
+ /** Origin prefixed onto relative asset urls (`url` starting with `/`). */
991
+ fileBaseUrl: string;
992
+ /** Asset-byte fetcher override (default: global fetch via resolveDeps). */
993
+ fetchBytes?: (url: string) => Promise<BrandFetchedBytes>;
994
+ /** False when the model cannot take image/document blocks inside a
995
+ * tool_result — view_asset then steers to the pre-turn attachment path. */
996
+ visionInToolResults?: boolean;
997
+ /** Clock override (tests). */
998
+ now?: () => number;
999
+ /** Id factory override (tests). */
1000
+ newId?: () => string;
1001
+ }
1002
+ /** The resolved context every brand tool module builds its tools from. */
1003
+ interface BrandToolCtx {
1004
+ db: BrandDb;
1005
+ bookId: string;
1006
+ self: BrandSkillSelf;
1007
+ fileBaseUrl: string;
1008
+ fetchBytes: (url: string) => Promise<BrandFetchedBytes>;
1009
+ /** Whether image/document blocks may be returned inside tool results. */
1010
+ visionInToolResults: boolean;
1011
+ now: () => number;
1012
+ newId: () => string;
1013
+ /** Load the scoped book row; throws BrandNotFoundError when missing. */
1014
+ loadBook(): Promise<BrandBook>;
1015
+ /** Wrap a handler so Brand validation errors come back as actionable
1016
+ * error text instead of the loop's sanitized generic failure. */
1017
+ guard(handler: ToolHandler): ToolHandler;
1018
+ }
1019
+ /** Workflow guidance appended to the persona's system prompt as the skill's
1020
+ * instruction section. */
1021
+ declare const BRAND_INSTRUCTIONS: string;
1022
+ /**
1023
+ * Build the brand Skill: read/asset/palette/book tools scoped to one book,
1024
+ * acting as one bot identity, plus {@link BRAND_INSTRUCTIONS}. Attach it to
1025
+ * a Persona (or use `createBrandPersona`).
1026
+ */
1027
+ declare function brandSkill(opts: BrandSkillOpts): Skill;
1028
+
1029
+ /** Byte cap for in-conversation asset viewing (4.5 MiB — providers reject
1030
+ * larger inline payloads well before context does). */
1031
+ declare const MAX_VIEW_BYTES = 4718592;
1032
+ /**
1033
+ * Base64-encode bytes without assuming a platform: `btoa` over a chunked
1034
+ * binary string where available (workerd, browsers, Node ≥ 16), `Buffer`
1035
+ * otherwise. Chunking keeps `String.fromCharCode` off the argument-count
1036
+ * cliff for multi-megabyte assets.
1037
+ */
1038
+ declare function base64FromBytes(bytes: Uint8Array): string;
1039
+ /**
1040
+ * The asset tools, scoped to the context's book: `view_asset` (fetch the
1041
+ * stored bytes and return them as an image/PDF block, with size and type
1042
+ * gates) and `record_asset_analysis` (persist the model's description,
1043
+ * dominant colors, and tags onto the asset row).
1044
+ */
1045
+ declare function assetTools(ctx: BrandToolCtx): ToolDef[];
1046
+
1047
+ /**
1048
+ * The book tools: `update_section` (validated per-kind content, upserted by
1049
+ * natural key with the book's audience snapshot) and `compile_tokens`
1050
+ * (palette + typography → light/dark token cache on the book row).
1051
+ */
1052
+ declare function bookTools(ctx: BrandToolCtx): ToolDef[];
1053
+
1054
+ /**
1055
+ * The palette tools: `analyze_color`, `evaluate_contrast` (pure math),
1056
+ * `propose_palette` (writes an OPEN proposal with an automatic contrast
1057
+ * report), and `resolve_proposal` (applies the human's explicit verdict).
1058
+ */
1059
+ declare function paletteTools(ctx: BrandToolCtx): ToolDef[];
1060
+
1061
+ /** The default brand-director system prompt (override via `system`). */
1062
+ declare const DEFAULT_BRAND_SYSTEM: string;
1063
+ /** Options for {@link createBrandPersona}. */
1064
+ interface CreateBrandPersonaOpts {
1065
+ /** Canonical model id the persona runs on. */
1066
+ model: string;
1067
+ /** System prompt override (default {@link DEFAULT_BRAND_SYSTEM}). */
1068
+ system?: string;
1069
+ /** Provider web-search server tool (default true — brand research). */
1070
+ webSearch?: boolean;
1071
+ /** Max model turns per run (default 10 — the workflow is multi-step). */
1072
+ maxSteps?: number;
1073
+ /** Extra skills appended after the brand skill (e.g. a chat skill). */
1074
+ skills?: Skill[];
1075
+ /** Options for the attached {@link brandSkill}. */
1076
+ brand: BrandSkillOpts;
1077
+ }
1078
+ /** Build the brand-director Persona: {@link brandSkill} first, extra skills
1079
+ * after, defaults per the option JSDoc above. */
1080
+ declare function createBrandPersona(opts: CreateBrandPersonaOpts): Persona;
1081
+ /** The capability flags {@link supportsBrandVision} inspects. */
1082
+ interface BrandVisionCapabilities {
1083
+ toolResultBlocks?: boolean;
1084
+ imageIn?: boolean;
1085
+ documentIn?: boolean;
1086
+ }
1087
+ /** The (structural) slice of an @odla-ai/ai ModelSpec the probe reads. */
1088
+ interface BrandVisionSpec {
1089
+ capabilities?: BrandVisionCapabilities;
1090
+ }
1091
+ /**
1092
+ * Whether a model can view assets THROUGH view_asset — i.e. take image and
1093
+ * document blocks inside tool results. Pure capabilities-metadata check
1094
+ * (`toolResultBlocks && imageIn && documentIn`), never provider names; when
1095
+ * false, hosts attach asset bytes as pre-turn input blocks instead and pass
1096
+ * `visionInToolResults: false` to the skill.
1097
+ */
1098
+ declare function supportsBrandVision(spec: BrandVisionSpec | null | undefined): boolean;
1099
+
1100
+ /**
1101
+ * The two read-only tools, scoped to the context's book:
1102
+ * `read_brand_book` (book + sections + palettes + open proposals as compact
1103
+ * text) and `list_assets` (live, non-tombstoned asset rows, optionally
1104
+ * filtered by kind).
1105
+ */
1106
+ declare function readTools(ctx: BrandToolCtx): ToolDef[];
1107
+
1108
+ /** Everything {@link import("./index").createBrandRoutes} needs from the host worker. */
1109
+ interface BrandRouteOpts {
1110
+ /** The injected odla client (a real @odla-ai/db AdminDb satisfies it). */
1111
+ db: BrandDb;
1112
+ /** Host-owned auth: resolve the request to an actor or null (→ 401). The
1113
+ * tokens routes skip this only when `publicTokens` is true. */
1114
+ authorize: (req: Request) => Promise<BrandActor | null> | BrandActor | null;
1115
+ /** Mount point. Default "/api/brand". */
1116
+ basePath?: string;
1117
+ /** Upload size cap in bytes (checked against `File.size`). Default 8 MiB. */
1118
+ maxUploadBytes?: number;
1119
+ /** Serve `GET /books/:id/tokens.css` and `tokens.json` without authorize —
1120
+ * compiled tokens only; every other route always authorizes. Default false. */
1121
+ publicTokens?: boolean;
1122
+ /** Injectable clock (tests). Default `Date.now`. */
1123
+ now?: () => number;
1124
+ /** Injectable id factory (tests). Default `crypto.randomUUID`. */
1125
+ newId?: () => string;
1126
+ }
1127
+ /** Per-request context handed to the authorized route handlers. */
1128
+ interface BrandRouteCtx {
1129
+ db: BrandDb;
1130
+ actor: BrandActor;
1131
+ now: () => number;
1132
+ newId: () => string;
1133
+ maxUploadBytes: number;
1134
+ }
1135
+
1136
+ /**
1137
+ * Build the brand fetch handler. Returns `null` for requests outside
1138
+ * `basePath` (default `/api/brand`) so the host worker falls through to its
1139
+ * own routes; inside it, every route requires `authorize` to resolve an
1140
+ * actor (401 otherwise) EXCEPT the two tokens routes when `publicTokens` is
1141
+ * true — compiled tokens are the one intentionally publishable surface.
1142
+ * Domain errors map to 400/404 JSON; anything unexpected is an opaque 500.
1143
+ */
1144
+ declare function createBrandRoutes(options: BrandRouteOpts): (req: Request) => Promise<Response | null>;
1145
+
1146
+ /** The dispatch envelope odla-db's commit hook POSTs when a trigger fires —
1147
+ * the same wire shape chat-agent consumes (see the header provenance note). */
1148
+ interface BrandDispatchBody {
1149
+ v: number;
1150
+ appId: string;
1151
+ trigger: {
1152
+ id: string;
1153
+ skill: string;
1154
+ runAs: {
1155
+ agentId: string;
1156
+ persona: string;
1157
+ };
1158
+ maxDepth?: number;
1159
+ };
1160
+ event: {
1161
+ ns: string;
1162
+ id: string;
1163
+ row: Record<string, unknown>;
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Validate an untrusted dispatch body; null if malformed. SHAPE-only,
1168
+ * mirroring chat-agent's parseDispatch: it does NOT gate on `trigger.skill` —
1169
+ * routing on skill is the HOST's job (a worker serving several skills checks
1170
+ * `body.trigger.skill === "brand"` before calling {@link dispatchBrandTurn}),
1171
+ * so an absent skill parses as `""`, never silently defaulted to "brand".
1172
+ */
1173
+ declare function parseBrandDispatch(raw: unknown): BrandDispatchBody | null;
1174
+ /** The brand book bound to a chat channel (`brand_book.channelId`), or null
1175
+ * when the channel id is empty or no book claims it. */
1176
+ declare function bookForChannel(db: BrandDb, channelId: string): Promise<BrandBook | null>;
1177
+ /**
1178
+ * The agent-turn input for a dispatched message: a brand-flavored prompt
1179
+ * (mirrors chat-agent's `inputFor` tone, steering into the brand workflow),
1180
+ * and — when pre-turn `attachments` are supplied (the no-vision-in-tool-
1181
+ * results path) — an explicit block array with the images BEFORE the text.
1182
+ */
1183
+ declare function brandInputFor(body: BrandDispatchBody, attachments?: ImageBlock[]): string | OracleContentBlock[];
1184
+ /** Everything {@link dispatchBrandTurn} needs from the host worker. The
1185
+ * @odla-ai/ai pieces (`inference`, `runAgent`) are injected so this module
1186
+ * keeps the package's types-only relationship with the peer. */
1187
+ interface BrandDispatchDeps {
1188
+ /** The injected odla client (a real @odla-ai/db AdminDb satisfies it). */
1189
+ db: BrandDb;
1190
+ /** The @odla-ai/ai inference facade the turn runs on. */
1191
+ inference: Inference;
1192
+ /** Canonical model id for the persona. */
1193
+ model: string;
1194
+ /** The agent loop — pass @odla-ai/ai's `runAgent`. */
1195
+ runAgent: (inference: Inference, persona: Persona, input: AgentRunInput) => Promise<{
1196
+ finalText: string;
1197
+ }>;
1198
+ /** Origin prefixed onto relative asset urls (skill + pre-turn attachments). */
1199
+ fileBaseUrl: string;
1200
+ /** Optional chat-skill factory (e.g. wrapping @odla-ai/chat's chatSkill) so
1201
+ * the bot can reply in the triggering channel. */
1202
+ chatSkillFor?: (channelId: string, self: BrandSkillSelf) => Skill;
1203
+ /** Model catalog slice (model id → capability spec) for the vision probe;
1204
+ * an absent catalog or unknown model fails safe to the pre-turn path. */
1205
+ catalog?: Record<string, BrandVisionSpec>;
1206
+ }
1207
+ /**
1208
+ * Run one brand-agent turn for a dispatched chat message: resolve the book
1209
+ * via the row's `channelId` (throws {@link BrandNotFoundError} when no book
1210
+ * claims the channel), probe `supportsBrandVision(catalog[model])`, build the
1211
+ * persona (brand skill + optional chat skill, bot identity from
1212
+ * `trigger.runAs`), and run the loop. When the model can NOT take blocks in
1213
+ * tool results, assets are attached PRE-TURN as URL image blocks — v1 keeps
1214
+ * detection simple: only when the message row carries an `assetIds` array
1215
+ * (no body-text scraping), and only live image assets.
1216
+ */
1217
+ declare function dispatchBrandTurn(deps: BrandDispatchDeps, body: BrandDispatchBody): Promise<{
1218
+ finalText: string;
1219
+ }>;
1220
+
1221
+ /** The odla-db namespace chat messages live in.
1222
+ * PROVENANCE: @odla-ai/chat `CHAT_NS.message` (packages/chat/src/constants.ts)
1223
+ * — duplicated as a local const because this package is zero-dep and the wire
1224
+ * value is a stable contract, not an implementation detail. */
1225
+ declare const CHAT_MESSAGE_NS = "chat_message";
1226
+ /** Matches @odla/server's `Trigger` (the exact JSON POSTed to
1227
+ * `/app/:id/admin/triggers`) — the same wire shape as chat's `ChatTrigger`. */
1228
+ interface BrandTrigger {
1229
+ id: string;
1230
+ watch: {
1231
+ ns: string;
1232
+ on: "create" | "update";
1233
+ };
1234
+ when?: string;
1235
+ runAs: {
1236
+ agentId: string;
1237
+ persona: string;
1238
+ };
1239
+ skill: string;
1240
+ maxDepth?: number;
1241
+ channels?: string[];
1242
+ validate?: string;
1243
+ }
1244
+ /** Options for {@link brandBotTrigger}. */
1245
+ interface BrandBotTriggerOpts {
1246
+ /** Trigger id (unique per app). */
1247
+ id: string;
1248
+ /** The bot's agent id — must be in each target book's `memberIds` roster. */
1249
+ agentId: string;
1250
+ /** Persona name the dispatching worker runs. */
1251
+ persona: string;
1252
+ /** Fire only on messages mentioning this handle, e.g. "@brand". */
1253
+ mention?: string;
1254
+ /** Extra CEL over the new message row, ANDed with the built-in guards. */
1255
+ when?: string;
1256
+ /** Restrict to specific channel ids (the per-book mode). */
1257
+ channels?: string[];
1258
+ /** Watched write kind (default "create"). */
1259
+ on?: "create" | "update";
1260
+ }
1261
+ /**
1262
+ * Build a trigger that runs the brand agent on human chat messages
1263
+ * (optionally @-mentions, optionally channel-scoped): chat's `botTrigger`
1264
+ * shape with `skill: "brand"` and `maxDepth: 1` so a bot reply can never
1265
+ * re-trigger a bot.
1266
+ */
1267
+ declare function brandBotTrigger(opts: BrandBotTriggerOpts): BrandTrigger;
1268
+
1269
+ /** One owner-facing setting the integration exposes (crm's shape). */
1270
+ interface IntegrationSetting {
1271
+ key: string;
1272
+ description: string;
1273
+ public: boolean;
1274
+ pattern?: string;
1275
+ perEnv: boolean;
1276
+ source: string;
1277
+ }
1278
+ /** One vault secret the integration needs (brand core needs none). */
1279
+ interface IntegrationSecret {
1280
+ key: string;
1281
+ description: string;
1282
+ pattern?: string;
1283
+ vault: boolean;
1284
+ }
1285
+ /** Human / CLI / doctor provisioning steps, documentation-as-data. */
1286
+ interface IntegrationProvision {
1287
+ human: string[];
1288
+ cli: string[];
1289
+ doctor: string[];
1290
+ }
1291
+ /** Unauthenticated route check run by `odla-ai smoke`. */
1292
+ interface BrandIntegrationProbe {
1293
+ path: string;
1294
+ expectedStatus: number;
1295
+ }
1296
+ /** The brand descriptor shape: the shared base plus the schema + rules
1297
+ * provisioning installs and the (app-supplied) bot triggers. */
1298
+ interface BrandIntegrationDescriptor {
1299
+ id: string;
1300
+ title: string;
1301
+ npm: string;
1302
+ settings: IntegrationSetting[];
1303
+ secrets: IntegrationSecret[];
1304
+ /** The schema to POST at /app/:id/schema. */
1305
+ schema: SerializedSchema;
1306
+ /** The default-deny rules to install at /app/:id/admin/rules (merged with existing). */
1307
+ rules: BrandRules;
1308
+ /** Commit triggers to register at /app/:id/admin/triggers (per-bot; app-supplied). */
1309
+ triggers: BrandTrigger[];
1310
+ probes?: BrandIntegrationProbe[];
1311
+ provision: IntegrationProvision;
1312
+ }
1313
+ /**
1314
+ * The static documentation descriptor for @odla-ai/brand. Data only: it
1315
+ * describes what installing the brand capability means — push
1316
+ * {@link BRAND_SCHEMA}, install the default-deny `brandRules()`, register
1317
+ * `brandBotTrigger`s (app-supplied, in either the global mention-gated or the
1318
+ * per-book channel-scoped mode), mount `createBrandRoutes` in the app worker
1319
+ * — and performs none of it. Use {@link createBrandIntegration} in
1320
+ * `odla.config.mjs` to add the live route probe the CLI can execute.
1321
+ */
1322
+ declare const brandIntegration: BrandIntegrationDescriptor;
1323
+ /** Options for {@link createBrandIntegration}. */
1324
+ interface CreateBrandIntegrationOptions {
1325
+ /** Worker mount path; defaults to `/api/brand`. */
1326
+ basePath?: string;
1327
+ /** Document (and probe-plan for) unauthenticated tokens routes. */
1328
+ publicTokens?: boolean;
1329
+ /** The global bot's mention handle — folded into the CLI trigger step. */
1330
+ mention?: string;
1331
+ }
1332
+ /**
1333
+ * Build the project-specific, CLI-consumable brand integration descriptor:
1334
+ * {@link brandIntegration} plus a live smoke probe (an anonymous GET of
1335
+ * `<basePath>/books` must answer 401 — routes mounted, authorize enforced,
1336
+ * exactly crm's probe idiom) and, when `mention`/`publicTokens` are given,
1337
+ * concrete CLI steps carrying the app's actual values. Mounting
1338
+ * `createBrandRoutes` remains app-owned source work.
1339
+ */
1340
+ declare function createBrandIntegration(options?: CreateBrandIntegrationOptions): BrandIntegrationDescriptor;
1341
+
1342
+ export { ASSET_CONTENT_TYPES, ASSET_KINDS, type AcceptProposalInput, type AssetAnalysis, type AssetKind, type AttrType, type AudienceChildren, BOOK_STATUSES, BRAND_CHART_TOKENS, BRAND_CHAT_TOKENS, BRAND_DERIVED_TOKENS, BRAND_EMITTED_TOKENS, BRAND_INSTRUCTIONS, BRAND_NS, BRAND_REQUIRED_TOKENS, BRAND_RULES, BRAND_SCHEMA, type BookStatus, type BrandActor, type BrandAsset, type BrandAttrs, type BrandBook, type BrandBotTriggerOpts, type BrandDb, type BrandDeps, type BrandDispatchBody, type BrandDispatchDeps, type BrandEntityRef, type BrandFetchedBytes, type BrandFileRecord, BrandInputError, type BrandIntegrationDescriptor, type BrandIntegrationProbe, type BrandLookup, BrandNotFoundError, type BrandOp, type BrandPalette, type BrandProposal, type BrandResult, type BrandRouteCtx, type BrandRouteOpts, type BrandRow, type BrandRule, type BrandRules, type BrandScalar, type BrandSection, type BrandSkillOpts, type BrandSkillSelf, type BrandStorage, type BrandTokens, type BrandTokensSnapshot, type BrandToolCtx, type BrandTrigger, type BrandUploadBody, type BrandVisionCapabilities, type BrandVisionSpec, CHART_DELTA_MIN, CHAT_MESSAGE_NS, CSS_NAMED_COLORS, type CompileInput, type CompiledBrandTokens, type ContrastKind, type CreateAssetInput, type CreateBookInput, type CreateBrandIntegrationOptions, type CreateBrandPersonaOpts, DARK_FLIP_L_MAX, DARK_FLIP_L_MIN, DEFAULT_ACCENT_SEED, DEFAULT_BRAND_SYSTEM, type DeriveChartOptions, type DerivePaletteOptions, type Hsl, type ImagerySection, type IntegrationProvision, type IntegrationSecret, type IntegrationSetting, type LightnessDirection, type LogoSection, MAX_SWATCHES, MAX_VIEW_BYTES, type MapResult, type NamedColor, type NearestNamedColor, type Oklab, type Oklch, PALETTE_SOURCES, PALETTE_STATUSES, PICK_TEXT_DEFAULT_CANDIDATES, PROPOSAL_KINDS, PROPOSAL_STATUSES, type PaletteSection, type PaletteSource, type PaletteStatus, type ProposalKind, type ProposalStatus, type ProposePaletteInput, RAMP_L_MAX, RAMP_L_MIN, type RejectProposalInput, type RenderTokensCssOptions, type ResolvedBrandDeps, type Rgb, SECTION_KINDS, SECTION_STATUSES, SWATCH_ROLES, type SectionKind, type SectionStatus, type SerializedAttr, type SerializedEntity, type SerializedLink, type SerializedLinkEnd, type SerializedSchema, type Swatch, type SwatchRole, type TokenWarning, type TypographySection, type UpsertSectionInput, type VoiceSection, acceptProposalOps, adjustLightnessUntil, analogous, assertAnalysis, assertAssetContentType, assertHex, assertSectionContent, assertSwatches, assetTools, audienceFanoutOps, base64FromBytes, bookForChannel, bookTools, brandBotTrigger, brandInputFor, brandIntegration, brandRules, brandSkill, capString, capStringArray, clamp01, clampToGamut, compileBrandTokens, complementary, contrastRatio, createAssetOps, createBookOps, createBrandIntegration, createBrandPersona, createBrandRoutes, deltaEOK, deltaEOKLab, deriveChartColors, deriveDarkTokens, derivePalette, dispatchBrandTurn, hexToOklch, hslToRgb, inSrgbGamut, linearToSrgb, mapPaletteToTokens, meetsAA, meetsAAA, monochrome, nearestNamedColor, normalizeHex, oklabToOklch, oklabToRgb, oklchToHex, oklchToOklab, paletteTools, parseBrandDispatch, parseHex, pickTextOn, proposePaletteOps, readTools, recordAnalysisOps, rejectProposalOps, relativeLuminance, renderTokensCss, resolveDeps, rgbToHsl, rgbToOklab, rotateHue, safeFileName, sectionKey, splitComplementary, srgbToLinear, supportsBrandVision, tetradic, tintShadeRamp, toHex, tombstoneAssetOps, triadic, updateBookOps, upsertSectionOps };