@ankhorage/devtools 1.10.12 → 1.10.14

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.
@@ -4,6 +4,117 @@ import { readFile } from 'node:fs/promises';
4
4
  import { dirname, join, parse, resolve } from 'node:path';
5
5
  import { pathToFileURL } from 'node:url';
6
6
 
7
+ interface OwnerRequirement {
8
+ exports: readonly string[];
9
+ minimumVersion: string;
10
+ packageName: string;
11
+ specifier: string;
12
+ }
13
+
14
+ interface Diagnostic extends Record<string, unknown> {
15
+ severity?: unknown;
16
+ }
17
+
18
+ interface ComputedTheme extends Record<string, unknown> {
19
+ diagnostics: Diagnostic[];
20
+ }
21
+
22
+ interface DesignCompilation extends Record<string, unknown> {
23
+ computedTheme: ComputedTheme;
24
+ diagnostics: Diagnostic[];
25
+ }
26
+
27
+ interface ManifestComposition extends Record<string, unknown> {
28
+ diagnostics: Diagnostic[];
29
+ status: string;
30
+ }
31
+
32
+ interface ColorTheoryApi extends Record<string, unknown> {
33
+ COLOR_HARMONIES: readonly string[];
34
+ COLOR_HARMONY_CATALOG: readonly Record<string, unknown>[];
35
+ }
36
+
37
+ interface ContractsApi extends Record<string, unknown> {
38
+ APP_CATEGORIES: readonly string[];
39
+ NAVIGATOR_TYPES: readonly string[];
40
+ }
41
+
42
+ interface TemplatesApi extends Record<string, unknown> {
43
+ CATEGORY_PRESETS: Record<string, Record<string, unknown>>;
44
+ TONE_PAIR_CATALOG: readonly Record<string, unknown>[];
45
+ assertTemplateManifestReady: (composition: ManifestComposition) => Record<string, unknown>;
46
+ compileCategoryDesign: (category: string, theme: Record<string, unknown>) => DesignCompilation;
47
+ composeCategoryAppManifest: (input: Record<string, unknown>) => ManifestComposition;
48
+ resolveCategoryDesignPreset: (...arguments_: unknown[]) => unknown;
49
+ resolveTonePair: (...arguments_: unknown[]) => unknown;
50
+ validateTemplateManifest: (
51
+ manifest: Record<string, unknown>,
52
+ authoringState: string,
53
+ ) => ManifestComposition;
54
+ }
55
+
56
+ interface ZoraThemeApi extends Record<string, unknown> {
57
+ compileZoraTheme: (...arguments_: unknown[]) => unknown;
58
+ }
59
+
60
+ interface EventMetadata extends Record<string, unknown> {
61
+ description: string;
62
+ eventType: string;
63
+ label: string;
64
+ payloadFields?: unknown;
65
+ }
66
+
67
+ interface ComponentMetadata extends Record<string, unknown> {
68
+ allowedChildren: readonly string[];
69
+ directManifestNode: boolean;
70
+ events?: Record<string, EventMetadata>;
71
+ manifestPolicy?: { kind?: unknown };
72
+ name: string;
73
+ props: Record<string, unknown>;
74
+ }
75
+
76
+ interface RecipeFieldMetadata extends Record<string, unknown> {
77
+ options?: readonly string[];
78
+ type: string;
79
+ }
80
+
81
+ interface RecipeMetadata extends Record<string, unknown> {
82
+ fields: Record<string, RecipeFieldMetadata | undefined>;
83
+ kind: string;
84
+ }
85
+
86
+ interface ZoraMetadataApi extends Record<string, unknown> {
87
+ ZORA_COMPONENT_META: Record<string, ComponentMetadata | undefined>;
88
+ ZORA_THEME_RECIPE_META: Record<string, RecipeMetadata | undefined>;
89
+ }
90
+
91
+ interface ManifestNode extends Record<string, unknown> {
92
+ children?: ManifestNode[];
93
+ id: string;
94
+ props?: Record<string, unknown>;
95
+ type: string;
96
+ }
97
+
98
+ interface ManifestScreen extends Record<string, unknown> {
99
+ root: ManifestNode;
100
+ }
101
+
102
+ interface Region extends Record<string, unknown> {
103
+ component?: unknown;
104
+ evidenceId?: unknown;
105
+ id: string;
106
+ parentNodeId?: unknown;
107
+ props?: unknown;
108
+ reason?: unknown;
109
+ requestedCapability: string;
110
+ screenId: string;
111
+ }
112
+
113
+ interface LoadedOwnerModule {
114
+ module: Record<string, unknown>;
115
+ version: string;
116
+ }
117
+
7
118
  const OWNER_RELEASES = {
8
119
  colorTheory: { packageName: '@ankhorage/color-theory', minimumVersion: '0.3.0' },
9
120
  contracts: { packageName: '@ankhorage/contracts', minimumVersion: '8.2.0' },
@@ -48,33 +159,52 @@ const OWNER_REQUIREMENTS = {
48
159
  },
49
160
  };
50
161
 
162
+ /*** Expose the runtime owner gates so tests and callers never duplicate managed versions. */
163
+ export function inspectOwnerRequirements() {
164
+ return structuredClone(OWNER_RELEASES);
165
+ }
166
+
51
167
  /*** Load and validate every released owner API from one target repository. */
52
168
  export async function loadOwnerApis(targetDirectory = process.cwd()) {
53
- const loaded = {};
54
-
55
- for (const [ownerKey, requirement] of Object.entries(OWNER_REQUIREMENTS)) {
56
- loaded[ownerKey] = await loadOwnerModule(targetDirectory, requirement);
57
- }
169
+ const colorTheory = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.colorTheory);
170
+ const contracts = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.contracts);
171
+ const templates = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.templates);
172
+ const zoraTheme = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.zoraTheme);
173
+ const zoraMetadata = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.zoraMetadata);
174
+ assertColorTheoryApi(colorTheory.module);
175
+ assertContractsApi(contracts.module);
176
+ assertTemplatesApi(templates.module);
177
+ assertZoraThemeApi(zoraTheme.module);
178
+ assertZoraMetadataApi(zoraMetadata.module);
58
179
 
59
180
  return {
60
- colorTheory: loaded.colorTheory.module,
61
- contracts: loaded.contracts.module,
62
- templates: loaded.templates.module,
63
- zoraTheme: loaded.zoraTheme.module,
64
- zoraMetadata: loaded.zoraMetadata.module,
181
+ colorTheory: colorTheory.module,
182
+ contracts: contracts.module,
183
+ templates: templates.module,
184
+ zoraTheme: zoraTheme.module,
185
+ zoraMetadata: zoraMetadata.module,
65
186
  versions: {
66
- colorTheory: loaded.colorTheory.version,
67
- contracts: loaded.contracts.version,
68
- templates: loaded.templates.version,
69
- zora: loaded.zoraTheme.version,
187
+ colorTheory: colorTheory.version,
188
+ contracts: contracts.version,
189
+ templates: templates.version,
190
+ zora: zoraTheme.version,
70
191
  },
71
192
  };
72
193
  }
73
194
 
195
+ /*** Load only Contracts for tooling that runs before another owner package has been built. */
196
+ export async function loadContractsApi(targetDirectory = process.cwd()): Promise<ContractsApi> {
197
+ const loaded = await loadOwnerModule(targetDirectory, OWNER_REQUIREMENTS.contracts);
198
+ assertContractsApi(loaded.module);
199
+ return loaded.module;
200
+ }
201
+
74
202
  /*** Return installed catalogs and metadata names without copying owner definitions. */
75
203
  export async function inspectOwnerApis(targetDirectory = process.cwd()) {
76
204
  const owners = await loadOwnerApis(targetDirectory);
77
- const componentMetadata = Object.values(owners.zoraMetadata.ZORA_COMPONENT_META);
205
+ const componentMetadata = Object.values(owners.zoraMetadata.ZORA_COMPONENT_META).filter(
206
+ (metadata) => metadata !== undefined,
207
+ );
78
208
  return {
79
209
  versions: owners.versions,
80
210
  appCategories: owners.contracts.APP_CATEGORIES,
@@ -104,20 +234,22 @@ export async function inspectOwnerApis(targetDirectory = process.cwd()) {
104
234
  }
105
235
 
106
236
  /*** Compose one design without turning missing runtime/UI capabilities into a design blocker. */
107
- export async function composeDesign(input, targetDirectory = process.cwd()) {
237
+ export async function composeDesign(input: unknown, targetDirectory = process.cwd()) {
108
238
  const owners = await loadOwnerApis(targetDirectory);
109
239
  assertRecord(input, 'Design input');
110
240
  assertNonEmptyString(input.category, 'category');
111
241
  assertRecord(input.navigator, 'navigator');
112
- assertRecord(input.screens, 'screens');
113
- assertSupportedThemeRecipes(input.theme?.recipes, owners.zoraMetadata.ZORA_THEME_RECIPE_META);
242
+ const screens = readManifestScreens(input.screens);
243
+ const theme = input.theme === undefined ? {} : input.theme;
244
+ assertRecord(theme, 'theme');
245
+ assertSupportedThemeRecipes(theme.recipes, owners.zoraMetadata.ZORA_THEME_RECIPE_META);
114
246
 
115
247
  const regionResult = resolveRegionNodes(
116
- input.screens,
248
+ screens,
117
249
  Array.isArray(input.regions) ? input.regions : [],
118
250
  owners.zoraMetadata.ZORA_COMPONENT_META,
119
251
  );
120
- const design = owners.templates.compileCategoryDesign(input.category, input.theme ?? {});
252
+ const design = owners.templates.compileCategoryDesign(input.category, theme);
121
253
  const { computedTheme, ...resolvedDesign } = design;
122
254
  const requestedAuthoringState = input.authoringState === 'release' ? 'release' : 'draft';
123
255
  const composition = owners.templates.composeCategoryAppManifest({
@@ -156,7 +288,11 @@ export async function composeDesign(input, targetDirectory = process.cwd()) {
156
288
  }
157
289
 
158
290
  /*** Resolve explicit region decisions using exact metadata or a visible non-blocking Box placeholder. */
159
- export function resolveRegionNodes(screens, regions, componentMeta) {
291
+ export function resolveRegionNodes(
292
+ screens: Record<string, ManifestScreen | undefined>,
293
+ regions: readonly unknown[],
294
+ componentMeta: Record<string, ComponentMetadata | undefined>,
295
+ ) {
160
296
  const resolvedScreens = structuredClone(screens);
161
297
  const diagnostics = [];
162
298
  const gaps = [];
@@ -166,6 +302,7 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
166
302
  assertNonEmptyString(region.id, 'region.id');
167
303
  assertNonEmptyString(region.screenId, 'region.screenId');
168
304
  assertNonEmptyString(region.requestedCapability, 'region.requestedCapability');
305
+ assertRegion(region);
169
306
  const screen = resolvedScreens[region.screenId];
170
307
  if (!screen) {
171
308
  throw new Error(`Region "${region.id}" targets unknown screen "${region.screenId}".`);
@@ -182,7 +319,10 @@ export function resolveRegionNodes(screens, regions, componentMeta) {
182
319
  return { screens: resolvedScreens, diagnostics, gaps };
183
320
  }
184
321
 
185
- function resolveRegionNode(region, componentMeta) {
322
+ function resolveRegionNode(
323
+ region: Region,
324
+ componentMeta: Record<string, ComponentMetadata | undefined>,
325
+ ) {
186
326
  const component = typeof region.component === 'string' ? region.component : null;
187
327
  const meta = component === null ? null : componentMeta[component];
188
328
  if (meta && meta.directManifestNode && meta.manifestPolicy?.kind !== 'unresolved-element') {
@@ -218,7 +358,6 @@ function resolveRegionNode(region, componentMeta) {
218
358
  return {
219
359
  node: { id: region.id, type: placeholderMeta.name, props: {} },
220
360
  diagnostic: {
221
- regionId: region.id,
222
361
  status: 'placeholder',
223
362
  component: placeholderMeta.name,
224
363
  ...gap,
@@ -228,7 +367,11 @@ function resolveRegionNode(region, componentMeta) {
228
367
  }
229
368
 
230
369
  /*** Validate that manifest props are declared by the selected component metadata. */
231
- function assertSupportedProps(props, meta, regionId) {
370
+ function assertSupportedProps(
371
+ props: unknown,
372
+ meta: ComponentMetadata,
373
+ regionId: string,
374
+ ): Record<string, unknown> {
232
375
  assertRecord(props, `props for region "${regionId}"`);
233
376
  const unsupported = Object.keys(props).filter((name) => !(name in meta.props));
234
377
  if (unsupported.length > 0) {
@@ -240,7 +383,12 @@ function assertSupportedProps(props, meta, regionId) {
240
383
  }
241
384
 
242
385
  /*** Insert a region node under the declared parent or the target screen root. */
243
- function insertRegionNode(screen, parentNodeId, node, componentMeta) {
386
+ function insertRegionNode(
387
+ screen: ManifestScreen,
388
+ parentNodeId: unknown,
389
+ node: ManifestNode,
390
+ componentMeta: Record<string, ComponentMetadata | undefined>,
391
+ ): void {
244
392
  const parent =
245
393
  typeof parentNodeId === 'string' && parentNodeId !== ''
246
394
  ? findNode(screen.root, parentNodeId)
@@ -249,7 +397,7 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
249
397
  throw new Error(`Region parent node not found: ${String(parentNodeId)}`);
250
398
  }
251
399
  const parentMeta = componentMeta[parent.type];
252
- if (!parentMeta || !parentMeta.directManifestNode) {
400
+ if (!parentMeta?.directManifestNode) {
253
401
  throw new Error(`Region parent "${parent.id}" is absent from direct ZORA manifest metadata.`);
254
402
  }
255
403
  if (!parentMeta.allowedChildren.includes(node.type)) {
@@ -261,7 +409,10 @@ function insertRegionNode(screen, parentNodeId, node, componentMeta) {
261
409
  }
262
410
 
263
411
  /*** Validate persisted component and pattern recipe overrides against exact ZORA metadata fields. */
264
- function assertSupportedThemeRecipes(recipes, recipeMeta) {
412
+ function assertSupportedThemeRecipes(
413
+ recipes: unknown,
414
+ recipeMeta: Record<string, RecipeMetadata | undefined>,
415
+ ): void {
265
416
  if (recipes === undefined) return;
266
417
  assertRecord(recipes, 'theme.recipes');
267
418
  for (const [group, kind] of [
@@ -273,13 +424,16 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
273
424
  assertRecord(overrides, `theme.recipes.${group}`);
274
425
  for (const [recipeName, fields] of Object.entries(overrides)) {
275
426
  const meta = recipeMeta[recipeName];
276
- if (!meta || meta.kind !== kind) {
427
+ if (meta?.kind !== kind) {
277
428
  throw new Error(`Unknown ZORA ${kind} theme recipe: ${recipeName}.`);
278
429
  }
279
430
  assertRecord(fields, `theme recipe ${recipeName}`);
280
431
  for (const [fieldName, value] of Object.entries(fields)) {
281
432
  const fieldMeta = meta.fields[fieldName];
282
- if (!fieldMeta || !isRecipeValueSupported(value, fieldMeta)) {
433
+ if (fieldMeta === undefined) {
434
+ throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
435
+ }
436
+ if (!isRecipeValueSupported(value, fieldMeta)) {
283
437
  throw new Error(`Unsupported ZORA theme recipe value: ${recipeName}.${fieldName}.`);
284
438
  }
285
439
  }
@@ -288,14 +442,14 @@ function assertSupportedThemeRecipes(recipes, recipeMeta) {
288
442
  }
289
443
 
290
444
  /*** Validate one recipe value using its owner-defined boolean, choice, or token field metadata. */
291
- function isRecipeValueSupported(value, fieldMeta) {
445
+ function isRecipeValueSupported(value: unknown, fieldMeta: RecipeFieldMetadata): boolean {
292
446
  if (fieldMeta.type === 'boolean') return typeof value === 'boolean';
293
447
  if (typeof value !== 'string') return false;
294
- return fieldMeta.type !== 'choice' || fieldMeta.options.includes(value);
448
+ return fieldMeta.type !== 'choice' || fieldMeta.options?.includes(value) === true;
295
449
  }
296
450
 
297
451
  /*** Find a manifest node recursively by stable node ID. */
298
- function findNode(node, nodeId) {
452
+ function findNode(node: ManifestNode, nodeId: string): ManifestNode | null {
299
453
  if (node.id === nodeId) return node;
300
454
  for (const child of Array.isArray(node.children) ? node.children : []) {
301
455
  const found = findNode(child, nodeId);
@@ -305,8 +459,11 @@ function findNode(node, nodeId) {
305
459
  }
306
460
 
307
461
  /*** Resolve, version-check, and import one public owner module from the target repository. */
308
- async function loadOwnerModule(targetDirectory, requirement) {
309
- let packageManifestPath;
462
+ async function loadOwnerModule(
463
+ targetDirectory: string,
464
+ requirement: OwnerRequirement,
465
+ ): Promise<LoadedOwnerModule> {
466
+ let packageManifestPath: string;
310
467
  try {
311
468
  packageManifestPath = await findInstalledPackageManifest(
312
469
  targetDirectory,
@@ -320,8 +477,9 @@ async function loadOwnerModule(targetDirectory, requirement) {
320
477
  );
321
478
  }
322
479
 
323
- const packageManifest = JSON.parse(await readFile(packageManifestPath, 'utf8'));
324
- const version = packageManifest.version;
480
+ const packageManifest: unknown = JSON.parse(await readFile(packageManifestPath, 'utf8'));
481
+ assertRecord(packageManifest, `${requirement.packageName} package manifest`);
482
+ const { version } = packageManifest;
325
483
  if (typeof version !== 'string' || compareVersions(version, requirement.minimumVersion) < 0) {
326
484
  throw ownerError(
327
485
  requirement,
@@ -334,7 +492,8 @@ async function loadOwnerModule(targetDirectory, requirement) {
334
492
  requirement.specifier,
335
493
  requirement,
336
494
  );
337
- const ownerModule = await import(pathToFileURL(modulePath).href);
495
+ const ownerModule: unknown = await import(pathToFileURL(modulePath).href);
496
+ assertRecord(ownerModule, `${requirement.packageName} public module`);
338
497
  const missingExports = requirement.exports.filter((name) => !(name in ownerModule));
339
498
  if (missingExports.length > 0) {
340
499
  throw ownerError(requirement, `is missing public exports: ${missingExports.join(', ')}`);
@@ -343,11 +502,15 @@ async function loadOwnerModule(targetDirectory, requirement) {
343
502
  }
344
503
 
345
504
  /*** Find the nearest installed package manifest without falling back to the skill's own tree. */
346
- async function findInstalledPackageManifest(targetDirectory, packageName) {
505
+ async function findInstalledPackageManifest(
506
+ targetDirectory: string,
507
+ packageName: string,
508
+ ): Promise<string> {
347
509
  const resolvedTarget = resolve(targetDirectory);
348
510
  const selfManifestPath = join(resolvedTarget, 'package.json');
349
511
  try {
350
- const selfManifest = JSON.parse(await readFile(selfManifestPath, 'utf8'));
512
+ const selfManifest: unknown = JSON.parse(await readFile(selfManifestPath, 'utf8'));
513
+ assertRecord(selfManifest, 'Target package manifest');
351
514
  if (selfManifest.name === packageName) return selfManifestPath;
352
515
  } catch (error) {
353
516
  if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
@@ -355,7 +518,7 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
355
518
 
356
519
  const filesystemRoot = parse(resolvedTarget).root;
357
520
  let directory = resolvedTarget;
358
- while (true) {
521
+ for (;;) {
359
522
  const candidate = join(directory, 'node_modules', ...packageName.split('/'), 'package.json');
360
523
  try {
361
524
  await readFile(candidate, 'utf8');
@@ -370,7 +533,12 @@ async function findInstalledPackageManifest(targetDirectory, packageName) {
370
533
  }
371
534
 
372
535
  /*** Resolve one import-condition public export from an installed package manifest. */
373
- function resolvePublicExportPath(packageManifest, packageManifestPath, specifier, requirement) {
536
+ function resolvePublicExportPath(
537
+ packageManifest: Record<string, unknown>,
538
+ packageManifestPath: string,
539
+ specifier: string,
540
+ requirement: OwnerRequirement,
541
+ ): string {
374
542
  const exportKey =
375
543
  specifier === requirement.packageName
376
544
  ? '.'
@@ -378,14 +546,14 @@ function resolvePublicExportPath(packageManifest, packageManifestPath, specifier
378
546
  const exportsField = packageManifest.exports;
379
547
  const rawExport = isRecord(exportsField) ? exportsField[exportKey] : null;
380
548
  const exportTarget = selectImportExportTarget(rawExport);
381
- if (exportTarget === null || !exportTarget.startsWith('./')) {
549
+ if (!exportTarget?.startsWith('./')) {
382
550
  throw ownerError(requirement, `does not expose the import target for ${specifier}`);
383
551
  }
384
552
  return resolve(dirname(packageManifestPath), exportTarget);
385
553
  }
386
554
 
387
555
  /*** Select the canonical ESM import target without resolving a CommonJS compatibility condition. */
388
- function selectImportExportTarget(rawExport) {
556
+ function selectImportExportTarget(rawExport: unknown): string | null {
389
557
  if (typeof rawExport === 'string') return rawExport;
390
558
  if (!isRecord(rawExport)) return null;
391
559
  for (const condition of ['import', 'default', 'bun', 'browser', 'react-native']) {
@@ -395,7 +563,7 @@ function selectImportExportTarget(rawExport) {
395
563
  }
396
564
 
397
565
  /*** Create an actionable released-owner diagnostic without offering a compatibility fallback. */
398
- function ownerError(requirement, detail, cause) {
566
+ function ownerError(requirement: OwnerRequirement, detail: string, cause?: unknown): Error {
399
567
  return new Error(
400
568
  `zora-designer requires ${requirement.packageName} >=${requirement.minimumVersion}; ${detail}. ` +
401
569
  `Update the target dependency through its normal Renovate/release workflow and rerun inspection.`,
@@ -404,7 +572,7 @@ function ownerError(requirement, detail, cause) {
404
572
  }
405
573
 
406
574
  /*** Compare stable semantic versions needed by the released public API gates. */
407
- function compareVersions(left, right) {
575
+ function compareVersions(left: string, right: string): number {
408
576
  const leftParts = parseVersion(left);
409
577
  const rightParts = parseVersion(right);
410
578
  for (let index = 0; index < 3; index += 1) {
@@ -415,26 +583,165 @@ function compareVersions(left, right) {
415
583
  }
416
584
 
417
585
  /*** Parse the numeric major, minor, and patch tuple from a semantic version. */
418
- function parseVersion(version) {
586
+ function parseVersion(version: string): [number, number, number] {
419
587
  const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/u.exec(version);
420
588
  if (!match) return [-1, -1, -1];
421
- return match.slice(1, 4).map(Number);
589
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
590
+ }
591
+
592
+ /*** Read and validate the screen tree boundary supplied by portable JSON input. */
593
+ function readManifestScreens(value: unknown): Record<string, ManifestScreen | undefined> {
594
+ assertRecord(value, 'screens');
595
+ const screens: Record<string, ManifestScreen> = {};
596
+ for (const [screenId, screen] of Object.entries(value)) {
597
+ assertRecord(screen, `screen "${screenId}"`);
598
+ assertManifestNode(screen.root, `screen "${screenId}" root`);
599
+ screens[screenId] = { ...screen, root: screen.root };
600
+ }
601
+ return screens;
602
+ }
603
+
604
+ /*** Validate one manifest node recursively before region insertion. */
605
+ function assertManifestNode(value: unknown, label: string): asserts value is ManifestNode {
606
+ assertRecord(value, label);
607
+ assertNonEmptyString(value.id, `${label}.id`);
608
+ assertNonEmptyString(value.type, `${label}.type`);
609
+ if (value.props !== undefined) assertRecord(value.props, `${label}.props`);
610
+ if (value.children !== undefined) {
611
+ if (!Array.isArray(value.children)) throw new Error(`${label}.children must be an array.`);
612
+ value.children.forEach((child, index) =>
613
+ assertManifestNode(child, `${label}.children[${index}]`),
614
+ );
615
+ }
616
+ }
617
+
618
+ /*** Finish narrowing a region after its required string fields have been checked. */
619
+ function assertRegion(value: Record<string, unknown>): asserts value is Region {
620
+ if (value.props !== undefined) {
621
+ assertRecord(value.props, `props for region "${String(value.id)}"`);
622
+ }
623
+ }
624
+
625
+ /*** Narrow the released Color Theory capability surface used by this script. */
626
+ function assertColorTheoryApi(value: Record<string, unknown>): asserts value is ColorTheoryApi {
627
+ assertStringArray(value.COLOR_HARMONIES, 'COLOR_HARMONIES');
628
+ assertRecordArray(value.COLOR_HARMONY_CATALOG, 'COLOR_HARMONY_CATALOG');
629
+ }
630
+
631
+ /*** Narrow the released Contracts capability surface used by this script. */
632
+ function assertContractsApi(value: Record<string, unknown>): asserts value is ContractsApi {
633
+ assertStringArray(value.APP_CATEGORIES, 'APP_CATEGORIES');
634
+ assertStringArray(value.NAVIGATOR_TYPES, 'NAVIGATOR_TYPES');
635
+ }
636
+
637
+ /*** Narrow the released Templates capability surface used by this script. */
638
+ function assertTemplatesApi(value: Record<string, unknown>): asserts value is TemplatesApi {
639
+ assertRecord(value.CATEGORY_PRESETS, 'CATEGORY_PRESETS');
640
+ assertRecordArray(value.TONE_PAIR_CATALOG, 'TONE_PAIR_CATALOG');
641
+ for (const exportName of [
642
+ 'assertTemplateManifestReady',
643
+ 'compileCategoryDesign',
644
+ 'composeCategoryAppManifest',
645
+ 'resolveCategoryDesignPreset',
646
+ 'resolveTonePair',
647
+ 'validateTemplateManifest',
648
+ ]) {
649
+ if (typeof value[exportName] !== 'function') {
650
+ throw new Error(`${exportName} must be a function.`);
651
+ }
652
+ }
653
+ }
654
+
655
+ /*** Narrow the released ZORA theme compiler capability. */
656
+ function assertZoraThemeApi(value: Record<string, unknown>): asserts value is ZoraThemeApi {
657
+ if (typeof value.compileZoraTheme !== 'function') {
658
+ throw new Error('compileZoraTheme must be a function.');
659
+ }
660
+ }
661
+
662
+ /*** Narrow released ZORA metadata into only the fields the orchestration needs. */
663
+ function assertZoraMetadataApi(value: Record<string, unknown>): asserts value is ZoraMetadataApi {
664
+ assertRecord(value.ZORA_COMPONENT_META, 'ZORA_COMPONENT_META');
665
+ for (const [name, metadata] of Object.entries(value.ZORA_COMPONENT_META)) {
666
+ assertComponentMetadata(metadata, name);
667
+ }
668
+ assertRecord(value.ZORA_THEME_RECIPE_META, 'ZORA_THEME_RECIPE_META');
669
+ for (const [name, metadata] of Object.entries(value.ZORA_THEME_RECIPE_META)) {
670
+ assertRecipeMetadata(metadata, name);
671
+ }
672
+ }
673
+
674
+ /*** Validate one component metadata entry obtained from the released owner. */
675
+ function assertComponentMetadata(value: unknown, name: string): asserts value is ComponentMetadata {
676
+ assertRecord(value, `ZORA component metadata ${name}`);
677
+ assertNonEmptyString(value.name, `ZORA component metadata ${name}.name`);
678
+ if (typeof value.directManifestNode !== 'boolean') {
679
+ throw new Error(`ZORA component metadata ${name}.directManifestNode must be a boolean.`);
680
+ }
681
+ assertStringArray(value.allowedChildren, `ZORA component metadata ${name}.allowedChildren`);
682
+ assertRecord(value.props, `ZORA component metadata ${name}.props`);
683
+ if (value.events !== undefined) {
684
+ assertRecord(value.events, `ZORA component metadata ${name}.events`);
685
+ for (const [eventName, event] of Object.entries(value.events)) {
686
+ assertEventMetadata(event, `${name}.${eventName}`);
687
+ }
688
+ }
689
+ if (value.manifestPolicy !== undefined) {
690
+ assertRecord(value.manifestPolicy, `ZORA component metadata ${name}.manifestPolicy`);
691
+ }
692
+ }
693
+
694
+ /*** Validate one event metadata entry used by interactive discovery. */
695
+ function assertEventMetadata(value: unknown, name: string): asserts value is EventMetadata {
696
+ assertRecord(value, `ZORA event metadata ${name}`);
697
+ assertNonEmptyString(value.eventType, `ZORA event metadata ${name}.eventType`);
698
+ assertNonEmptyString(value.label, `ZORA event metadata ${name}.label`);
699
+ assertNonEmptyString(value.description, `ZORA event metadata ${name}.description`);
700
+ }
701
+
702
+ /*** Validate one theme recipe metadata entry and its supported fields. */
703
+ function assertRecipeMetadata(value: unknown, name: string): asserts value is RecipeMetadata {
704
+ assertRecord(value, `ZORA theme recipe metadata ${name}`);
705
+ assertNonEmptyString(value.kind, `ZORA theme recipe metadata ${name}.kind`);
706
+ assertRecord(value.fields, `ZORA theme recipe metadata ${name}.fields`);
707
+ for (const [fieldName, field] of Object.entries(value.fields)) {
708
+ assertRecord(field, `ZORA theme recipe field ${name}.${fieldName}`);
709
+ assertNonEmptyString(field.type, `ZORA theme recipe field ${name}.${fieldName}.type`);
710
+ if (field.options !== undefined) {
711
+ assertStringArray(field.options, `ZORA theme recipe field ${name}.${fieldName}.options`);
712
+ }
713
+ }
714
+ }
715
+
716
+ /*** Require an array containing only non-empty strings. */
717
+ function assertStringArray(value: unknown, label: string): asserts value is string[] {
718
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
719
+ value.forEach((entry, index) => assertNonEmptyString(entry, `${label}[${index}]`));
720
+ }
721
+
722
+ /*** Require an array containing only records. */
723
+ function assertRecordArray(
724
+ value: unknown,
725
+ label: string,
726
+ ): asserts value is Record<string, unknown>[] {
727
+ if (!Array.isArray(value)) throw new Error(`${label} must be an array.`);
728
+ value.forEach((entry, index) => assertRecord(entry, `${label}[${index}]`));
422
729
  }
423
730
 
424
731
  /*** Require an object-shaped input value. */
425
- function assertRecord(value, label) {
732
+ function assertRecord(value: unknown, label: string): asserts value is Record<string, unknown> {
426
733
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
427
734
  throw new Error(`${label} must be an object.`);
428
735
  }
429
736
  }
430
737
 
431
738
  /*** Narrow unknown package export metadata to a record. */
432
- function isRecord(value) {
739
+ function isRecord(value: unknown): value is Record<string, unknown> {
433
740
  return typeof value === 'object' && value !== null && !Array.isArray(value);
434
741
  }
435
742
 
436
743
  /*** Require a non-empty string input field. */
437
- function assertNonEmptyString(value, label) {
744
+ function assertNonEmptyString(value: unknown, label: string): asserts value is string {
438
745
  if (typeof value !== 'string' || value.trim() === '') {
439
746
  throw new Error(`${label} must be a non-empty string.`);
440
747
  }
@@ -448,11 +755,11 @@ async function main() {
448
755
  return;
449
756
  }
450
757
  if (command === 'compose' && inputPath) {
451
- const input = JSON.parse(await readFile(inputPath, 'utf8'));
758
+ const input: unknown = JSON.parse(await readFile(inputPath, 'utf8'));
452
759
  console.log(JSON.stringify(await composeDesign(input), null, 2));
453
760
  return;
454
761
  }
455
- throw new Error('Usage: owner-api.mjs inspect | owner-api.mjs compose <input.json>');
762
+ throw new Error('Usage: owner-api.ts inspect | owner-api.ts compose <input.json>');
456
763
  }
457
764
 
458
765
  if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {