@odla-ai/brand 0.7.1 → 0.8.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
@@ -51,7 +51,7 @@ The package is a **schema + rules + an agent skill + a color engine + a token co
51
51
  - **The upload surface is intentionally narrow.** Raster PNG/JPEG/GIF/WebP and
52
52
  PDF inputs are supported. SVG active content and binary font files are not;
53
53
  typography remains an exact, reviewable family/style proposal.
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.
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. A reviewed typography proposal may also carry bounded `tokenOverrides`; those `--ui-*` component and layout values are validated, stored with the approved section, and compiled into both published token maps.
55
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.
56
56
 
57
57
  - **Claude Designs are a first-class input.** A design exported from
@@ -66,13 +66,18 @@ interface PaletteSection {
66
66
  name: string;
67
67
  swatches: Swatch[];
68
68
  }
69
- /** `typography`-kind section content. `scale` is a modular type-scale ratio. */
69
+ /**
70
+ * `typography`-kind section content. `scale` is a modular type-scale ratio.
71
+ * `tokenOverrides` carries bounded `--ui-*` component/layout values through
72
+ * the same reviewed activation that publishes the font choices.
73
+ */
70
74
  interface TypographySection {
71
75
  fontDisplay?: string;
72
76
  fontBody?: string;
73
77
  fontMono?: string;
74
78
  scale?: number;
75
79
  notes?: string;
80
+ tokenOverrides?: Record<string, string>;
76
81
  }
77
82
  /** `voice`-kind section content: tone plus writing principles/examples. */
78
83
  interface VoiceSection {
@@ -66,13 +66,18 @@ interface PaletteSection {
66
66
  name: string;
67
67
  swatches: Swatch[];
68
68
  }
69
- /** `typography`-kind section content. `scale` is a modular type-scale ratio. */
69
+ /**
70
+ * `typography`-kind section content. `scale` is a modular type-scale ratio.
71
+ * `tokenOverrides` carries bounded `--ui-*` component/layout values through
72
+ * the same reviewed activation that publishes the font choices.
73
+ */
70
74
  interface TypographySection {
71
75
  fontDisplay?: string;
72
76
  fontBody?: string;
73
77
  fontMono?: string;
74
78
  scale?: number;
75
79
  notes?: string;
80
+ tokenOverrides?: Record<string, string>;
76
81
  }
77
82
  /** `voice`-kind section content: tone plus writing principles/examples. */
78
83
  interface VoiceSection {
package/dist/index.cjs CHANGED
@@ -31,6 +31,7 @@ __export(src_exports, {
31
31
  BRAND_EMITTED_TOKENS: () => BRAND_EMITTED_TOKENS,
32
32
  BRAND_INSTRUCTIONS: () => BRAND_INSTRUCTIONS,
33
33
  BRAND_NS: () => BRAND_NS,
34
+ BRAND_PROMOTION_ADAPTER: () => BRAND_PROMOTION_ADAPTER,
34
35
  BRAND_REQUIRED_TOKENS: () => BRAND_REQUIRED_TOKENS,
35
36
  BRAND_RULES: () => BRAND_RULES,
36
37
  BRAND_SCHEMA: () => BRAND_SCHEMA,
@@ -119,6 +120,7 @@ __export(src_exports, {
119
120
  digestDesignBundle: () => digestDesignBundle,
120
121
  digestDesignHtml: () => digestDesignHtml,
121
122
  dispatchBrandTurn: () => dispatchBrandTurn,
123
+ exportBrandPromotion: () => exportBrandPromotion,
122
124
  extractColors: () => extractColors,
123
125
  extractDesignTokens: () => extractDesignTokens,
124
126
  extractFonts: () => extractFonts,
@@ -598,6 +600,7 @@ var DESIGN_CONTENT_TYPES = /* @__PURE__ */ new Set(["text/html"]);
598
600
  var HEX_RGB = /^#[0-9a-f]{3}$/;
599
601
  var HEX_RRGGBB = /^#[0-9a-f]{6}$/;
600
602
  var HEX_ALPHA = /^#[0-9a-f]{4}$|^#[0-9a-f]{8}$/;
603
+ var UI_TOKEN_NAME = /^--ui-[a-z0-9-]+$/;
601
604
  var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
602
605
  function assertHex(value, label = "color") {
603
606
  if (typeof value !== "string")
@@ -664,6 +667,21 @@ function assertTypographySection(c) {
664
667
  out.scale = c.scale;
665
668
  }
666
669
  if (c.notes !== void 0) out.notes = capString(c.notes, "content.notes", 2e3);
670
+ if (c.tokenOverrides !== void 0) {
671
+ if (!isRecord(c.tokenOverrides))
672
+ throw new BrandInputError("content.tokenOverrides must be an object");
673
+ const entries = Object.entries(c.tokenOverrides);
674
+ if (entries.length > 64)
675
+ throw new BrandInputError("content.tokenOverrides must have at most 64 entries");
676
+ out.tokenOverrides = Object.fromEntries(entries.map(([name, raw]) => {
677
+ if (!UI_TOKEN_NAME.test(name))
678
+ throw new BrandInputError(`content.tokenOverrides.${name} must be a --ui-* token name`);
679
+ const value = capString(raw, `content.tokenOverrides.${name}`, 500);
680
+ if (/[;{}]/.test(value) || /url\s*\(/i.test(value))
681
+ throw new BrandInputError(`content.tokenOverrides.${name} contains an unsafe CSS value`);
682
+ return [name, value];
683
+ }));
684
+ }
667
685
  return out;
668
686
  }
669
687
  function assertVoiceSection(c) {
@@ -827,14 +845,14 @@ var SNAPSHOT_REQUIRED = [
827
845
  ];
828
846
  function validSnapshotShape(value) {
829
847
  if (!record3(value) || !exactKeys(value, SNAPSHOT_REQUIRED)) return false;
830
- const provenance = value.provenance;
831
- return boundedString(value.id) && boundedString(value.bookId) && typeof value.kind === "string" && PROPOSAL_KINDS.includes(value.kind) && value.status === "open" && record3(value.payload) && boundedString(value.rationale, 2e3) && record3(provenance) && exactKeys(provenance, [
848
+ const provenance2 = value.provenance;
849
+ return boundedString(value.id) && boundedString(value.bookId) && typeof value.kind === "string" && PROPOSAL_KINDS.includes(value.kind) && value.status === "open" && record3(value.payload) && boundedString(value.rationale, 2e3) && record3(provenance2) && exactKeys(provenance2, [
832
850
  "sourceAssetId",
833
851
  "sourceAsset",
834
852
  "messageId",
835
853
  "turnId",
836
854
  "taintLabels"
837
- ]) && (provenance.sourceAssetId === null || boundedString(provenance.sourceAssetId)) && (provenance.sourceAsset === null || record3(provenance.sourceAsset) && exactKeys(provenance.sourceAsset, [
855
+ ]) && (provenance2.sourceAssetId === null || boundedString(provenance2.sourceAssetId)) && (provenance2.sourceAsset === null || record3(provenance2.sourceAsset) && exactKeys(provenance2.sourceAsset, [
838
856
  "assetId",
839
857
  "contentDigest",
840
858
  "objectEtag",
@@ -843,7 +861,7 @@ function validSnapshotShape(value) {
843
861
  "contentType",
844
862
  "analysisRevision",
845
863
  "analysisDigest"
846
- ]) && boundedString(provenance.sourceAsset.assetId) && typeof provenance.sourceAsset.contentDigest === "string" && DIGEST.test(provenance.sourceAsset.contentDigest) && boundedString(provenance.sourceAsset.objectEtag) && Number.isSafeInteger(provenance.sourceAsset.objectSize) && provenance.sourceAsset.objectSize > 0 && typeof provenance.sourceAsset.pathDigest === "string" && DIGEST.test(provenance.sourceAsset.pathDigest) && (provenance.sourceAsset.contentType === null || boundedString(provenance.sourceAsset.contentType, 160)) && (provenance.sourceAsset.analysisRevision === null || Number.isSafeInteger(provenance.sourceAsset.analysisRevision) && provenance.sourceAsset.analysisRevision >= 0) && (provenance.sourceAsset.analysisDigest === null || typeof provenance.sourceAsset.analysisDigest === "string" && DIGEST.test(provenance.sourceAsset.analysisDigest))) && (provenance.sourceAssetId === null && provenance.sourceAsset === null || provenance.sourceAssetId !== null && provenance.sourceAsset !== null && provenance.sourceAsset.assetId === provenance.sourceAssetId) && (provenance.messageId === null || boundedString(provenance.messageId)) && (provenance.turnId === null || boundedString(provenance.turnId)) && Array.isArray(provenance.taintLabels) && provenance.taintLabels.length <= 16 && provenance.taintLabels.every((label) => boundedString(label, 120)) && new Set(provenance.taintLabels).size === provenance.taintLabels.length && typeof value.reviewDigest === "string" && DIGEST.test(value.reviewDigest) && Array.isArray(value.audience) && value.audience.length > 0 && value.audience.length <= 100 && value.audience.every((id) => boundedString(id)) && new Set(value.audience).size === value.audience.length && boundedString(value.createdBy) && boundedString(value.createdAuthorityRef) && safeTime(value.createdAt);
864
+ ]) && boundedString(provenance2.sourceAsset.assetId) && typeof provenance2.sourceAsset.contentDigest === "string" && DIGEST.test(provenance2.sourceAsset.contentDigest) && boundedString(provenance2.sourceAsset.objectEtag) && Number.isSafeInteger(provenance2.sourceAsset.objectSize) && provenance2.sourceAsset.objectSize > 0 && typeof provenance2.sourceAsset.pathDigest === "string" && DIGEST.test(provenance2.sourceAsset.pathDigest) && (provenance2.sourceAsset.contentType === null || boundedString(provenance2.sourceAsset.contentType, 160)) && (provenance2.sourceAsset.analysisRevision === null || Number.isSafeInteger(provenance2.sourceAsset.analysisRevision) && provenance2.sourceAsset.analysisRevision >= 0) && (provenance2.sourceAsset.analysisDigest === null || typeof provenance2.sourceAsset.analysisDigest === "string" && DIGEST.test(provenance2.sourceAsset.analysisDigest))) && (provenance2.sourceAssetId === null && provenance2.sourceAsset === null || provenance2.sourceAssetId !== null && provenance2.sourceAsset !== null && provenance2.sourceAsset.assetId === provenance2.sourceAssetId) && (provenance2.messageId === null || boundedString(provenance2.messageId)) && (provenance2.turnId === null || boundedString(provenance2.turnId)) && Array.isArray(provenance2.taintLabels) && provenance2.taintLabels.length <= 16 && provenance2.taintLabels.every((label) => boundedString(label, 120)) && new Set(provenance2.taintLabels).size === provenance2.taintLabels.length && typeof value.reviewDigest === "string" && DIGEST.test(value.reviewDigest) && Array.isArray(value.audience) && value.audience.length > 0 && value.audience.length <= 100 && value.audience.every((id) => boundedString(id)) && new Set(value.audience).size === value.audience.length && boundedString(value.createdBy) && boundedString(value.createdAuthorityRef) && safeTime(value.createdAt);
847
865
  }
848
866
  var CONSUMPTION_REQUIRED = [
849
867
  "id",
@@ -4143,7 +4161,10 @@ var proposalGuardEquals = (snapshot) => ({ ...snapshot });
4143
4161
 
4144
4162
  // src/routes/proposal-effects.ts
4145
4163
  async function tokenSnapshot(swatches, typography, input, paletteId, paletteReceiptId, paletteActionDigest, typographyReceiptId, typographyActionDigest) {
4146
- const compiled = compileBrandTokens({ swatches, ...typography ? { typography } : {} });
4164
+ const compiled = compileBrandTokens({
4165
+ swatches,
4166
+ ...typography ? { typography, overrides: typography.tokenOverrides } : {}
4167
+ });
4147
4168
  const sources = {
4148
4169
  paletteId,
4149
4170
  paletteReceiptId,
@@ -5436,4 +5457,111 @@ function createBrandIntegration(options = {}) {
5436
5457
  probes: [{ path: `${basePath}/books`, expectedStatus: 401 }]
5437
5458
  };
5438
5459
  }
5460
+
5461
+ // src/promotion.ts
5462
+ var import_promotion = require("@odla-ai/promotion");
5463
+ var BRAND_PROMOTION_ADAPTER = "@odla-ai/brand/v1";
5464
+ var provenance = (sourceIds, receiptDigests = []) => ({
5465
+ adapter: BRAND_PROMOTION_ADAPTER,
5466
+ sourceIds: [...sourceIds].sort(),
5467
+ receiptDigests: [...receiptDigests].sort()
5468
+ });
5469
+ var exclusions = (book) => [
5470
+ {
5471
+ kind: "brand.runtime",
5472
+ id: book.slug,
5473
+ reason: "environment_local",
5474
+ paths: [`${BRAND_NS.book}.ownerId`, `${BRAND_NS.book}.memberIds`, `${BRAND_NS.book}.channelId`, `${BRAND_NS.book}.*At`],
5475
+ detail: "Ownership, roster, channel, and timestamps remain local to the target environment."
5476
+ },
5477
+ {
5478
+ kind: "brand.drafts",
5479
+ id: book.slug,
5480
+ reason: "draft_state",
5481
+ paths: [`${BRAND_NS.proposal}.*`, `${BRAND_NS.section}[status=draft]`],
5482
+ detail: "Open proposals and unapproved sections are review state, not published brand content."
5483
+ },
5484
+ {
5485
+ kind: "brand.assets",
5486
+ id: book.slug,
5487
+ reason: "provider_identity",
5488
+ paths: [`${BRAND_NS.asset}.storageObjectId`, `${BRAND_NS.asset}.path`, `${BRAND_NS.asset}.*AuthorityRef`],
5489
+ detail: "Stored files and provider object identities require a separate asset-transfer contract."
5490
+ },
5491
+ {
5492
+ kind: "brand.authority",
5493
+ id: book.slug,
5494
+ reason: "authority_history",
5495
+ paths: [`${BRAND_NS.approvalReceipt}.*`, `${BRAND_NS.palette}.audience`],
5496
+ detail: "Authority rows stay immutable in their source environment; artifacts retain only receipt digests and ids."
5497
+ }
5498
+ ];
5499
+ function exportBrandPromotion(input) {
5500
+ const { book } = input;
5501
+ if (book.status !== "active") throw new TypeError(`brand book ${book.slug} is not active`);
5502
+ const revision = `brand:${book.slug}:${book.activationRevision}`;
5503
+ const artifacts = [];
5504
+ if (book.tokens) {
5505
+ const {
5506
+ compiledAt: _compiledAt,
5507
+ paletteReceiptId,
5508
+ paletteActionDigest,
5509
+ typographyReceiptId,
5510
+ typographyActionDigest,
5511
+ sourceDigest,
5512
+ ...stableTokens
5513
+ } = book.tokens;
5514
+ artifacts.push({
5515
+ kind: "brand.tokens",
5516
+ id: book.slug,
5517
+ sourceRevision: revision,
5518
+ provenance: provenance(
5519
+ [book.id, book.tokens.paletteId, paletteReceiptId, typographyReceiptId].filter((value) => Boolean(value)),
5520
+ [sourceDigest, paletteActionDigest, typographyActionDigest].filter((value) => Boolean(value))
5521
+ ),
5522
+ content: (0, import_promotion.canonicalPromotionValue)({ bookSlug: book.slug, tokens: stableTokens })
5523
+ });
5524
+ }
5525
+ for (const section of input.sections.filter((row) => row.bookId === book.id && row.status === "approved")) {
5526
+ if (!section.approvalReceiptId || !section.approvalActionDigest) {
5527
+ throw new TypeError(`approved brand section ${section.key} lacks approval provenance`);
5528
+ }
5529
+ artifacts.push({
5530
+ kind: "brand.section",
5531
+ id: `${book.slug}/${section.kind}`,
5532
+ sourceRevision: revision,
5533
+ provenance: provenance([book.id, section.id, section.approvalReceiptId], [section.approvalActionDigest]),
5534
+ content: (0, import_promotion.canonicalPromotionValue)({ bookSlug: book.slug, kind: section.kind, content: section.content })
5535
+ });
5536
+ }
5537
+ const activePalette2 = input.palettes.find((row) => row.bookId === book.id && row.id === book.activePaletteId && row.status === "active");
5538
+ if (book.activePaletteId && !activePalette2) throw new TypeError(`active palette ${book.activePaletteId} is unavailable`);
5539
+ if (activePalette2) {
5540
+ artifacts.push({
5541
+ kind: "brand.palette",
5542
+ id: `${book.slug}/${activePalette2.id}`,
5543
+ sourceRevision: revision,
5544
+ provenance: provenance([book.id, activePalette2.id, activePalette2.proposalId, activePalette2.approvalReceiptId], [activePalette2.approvalActionDigest]),
5545
+ content: (0, import_promotion.canonicalPromotionValue)({
5546
+ bookSlug: book.slug,
5547
+ name: activePalette2.name,
5548
+ swatches: activePalette2.swatches,
5549
+ source: activePalette2.source,
5550
+ ...activePalette2.seedHex ? { seedHex: activePalette2.seedHex } : {},
5551
+ ...activePalette2.rationale ? { rationale: activePalette2.rationale } : {}
5552
+ })
5553
+ });
5554
+ }
5555
+ return {
5556
+ adapter: BRAND_PROMOTION_ADAPTER,
5557
+ artifacts,
5558
+ exclusions: exclusions(book),
5559
+ preconditions: [{
5560
+ code: "brand.schema",
5561
+ path: "services.brand.schemaVersion",
5562
+ expected: 1,
5563
+ detail: "The target must expose the Brand v1 approved-content schema."
5564
+ }]
5565
+ };
5566
+ }
5439
5567
  //# sourceMappingURL=index.cjs.map