@open-agent-toolkit/cli 0.2.4 → 0.2.6

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.
Files changed (106) hide show
  1. package/assets/docs/cli-utilities/configuration.md +40 -0
  2. package/assets/docs/cli-utilities/tool-packs.md +31 -0
  3. package/assets/docs/workflows/projects/artifacts.md +27 -0
  4. package/assets/docs/workflows/projects/implementation-execution.md +30 -5
  5. package/assets/docs/workflows/skills/explainer-kit.md +125 -0
  6. package/assets/docs/workflows/skills/index.md +4 -0
  7. package/assets/migration/pjm-restructure.md +12 -0
  8. package/assets/public-package-versions.json +4 -4
  9. package/assets/skills/explainer-kit/SKILL.md +95 -0
  10. package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
  11. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
  12. package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
  13. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
  14. package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
  15. package/assets/skills/explainer-kit/palettes/ember.json +37 -0
  16. package/assets/skills/explainer-kit/palettes/forest.json +37 -0
  17. package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
  18. package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
  19. package/assets/skills/explainer-kit/palettes/violet.json +37 -0
  20. package/assets/skills/explainer-kit/profiles/clean.json +42 -0
  21. package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
  22. package/assets/skills/explainer-kit/profiles/technical.json +39 -0
  23. package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
  24. package/assets/skills/explainer-kit/recipes/program-recap.json +34 -0
  25. package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
  26. package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
  27. package/assets/skills/explainer-kit/references/contracts.md +101 -0
  28. package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
  29. package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
  30. package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
  31. package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
  32. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
  33. package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
  34. package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
  35. package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
  36. package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
  37. package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
  38. package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
  39. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
  40. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
  41. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
  42. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
  43. package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
  44. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
  45. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
  46. package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
  47. package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
  48. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
  49. package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
  50. package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
  51. package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
  52. package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
  53. package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
  54. package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
  55. package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
  56. package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
  57. package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
  58. package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
  59. package/assets/skills/explainer-kit/templates/house-style.html +218 -0
  60. package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
  61. package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
  62. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
  63. package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
  64. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
  65. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
  66. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
  67. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
  68. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
  69. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
  70. package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
  71. package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
  72. package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
  73. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
  74. package/assets/skills/oat-project-complete/SKILL.md +166 -28
  75. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
  76. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  77. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +84 -9
  78. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
  79. package/assets/skills/oat-project-next/SKILL.md +31 -11
  80. package/assets/skills/oat-project-plan/SKILL.md +30 -1
  81. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
  82. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
  83. package/assets/skills/oat-project-summary/SKILL.md +20 -1
  84. package/assets/templates/state.md +4 -1
  85. package/dist/commands/config/index.d.ts.map +1 -1
  86. package/dist/commands/config/index.js +232 -1
  87. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  88. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  89. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  90. package/dist/commands/project/archive/archive-utils.d.ts +12 -0
  91. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  92. package/dist/commands/project/archive/archive-utils.js +320 -8
  93. package/dist/commands/project/archive/index.d.ts.map +1 -1
  94. package/dist/commands/project/archive/index.js +1 -0
  95. package/dist/commands/project/archive/push-runner.d.ts +1 -0
  96. package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
  97. package/dist/commands/project/archive/push-runner.js +9 -2
  98. package/dist/config/oat-config.d.ts +26 -0
  99. package/dist/config/oat-config.d.ts.map +1 -1
  100. package/dist/config/oat-config.js +88 -0
  101. package/dist/config/resolve.d.ts.map +1 -1
  102. package/dist/config/resolve.js +18 -0
  103. package/dist/validation/project-state.d.ts +3 -0
  104. package/dist/validation/project-state.d.ts.map +1 -1
  105. package/dist/validation/project-state.js +91 -0
  106. package/package.json +2 -2
@@ -0,0 +1,485 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { posix } from 'node:path';
3
+
4
+ import { canonicalHash, validateContract } from './contracts.mjs';
5
+
6
+ const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
7
+ const TEMPLATE_BY_TYPE = new Map([
8
+ ['hub', 'house-style'],
9
+ ['diagram', 'diagram-shell'],
10
+ ['explainer', 'engineer-tour'],
11
+ ['deck', 'deck-shell'],
12
+ ]);
13
+ const TYPE_DIRECTORIES = new Map([
14
+ ['hub', 'initiatives'],
15
+ ['diagram', 'diagrams'],
16
+ ['explainer', 'explainers'],
17
+ ['deck', 'decks'],
18
+ ]);
19
+ const CONTENT_KEYS = new Set([
20
+ 'artifactId',
21
+ 'artifactLinks',
22
+ 'description',
23
+ 'eyebrow',
24
+ 'footer',
25
+ 'sections',
26
+ 'slug',
27
+ 'title',
28
+ ]);
29
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
30
+ const TOKEN_PATTERN = /{{([A-Z][A-Z_]*)}}/g;
31
+
32
+ export async function renderArtifact({
33
+ recipeArtifact,
34
+ content,
35
+ theme,
36
+ renderStrategy,
37
+ publicBaseUrl,
38
+ }) {
39
+ assertRecipeArtifact(recipeArtifact);
40
+ assertContent(content, recipeArtifact);
41
+ assertTheme(theme);
42
+ assertRenderStrategy(renderStrategy);
43
+
44
+ const baseUrl = normalizePublicBaseUrl(publicBaseUrl);
45
+ const renderedPath = artifactPath(recipeArtifact, content.slug);
46
+ const template = await readFile(
47
+ new URL(`../../templates/${recipeArtifact.template}.html`, import.meta.url),
48
+ 'utf8',
49
+ );
50
+ const sections = content.sections.map((section) => ({
51
+ ...section,
52
+ anchor: section.id,
53
+ }));
54
+ const links = content.artifactLinks ?? [];
55
+ const values = templateValues({
56
+ recipeArtifact,
57
+ content,
58
+ sections,
59
+ links,
60
+ theme,
61
+ renderedPath,
62
+ baseUrl,
63
+ });
64
+
65
+ let html = substituteTemplate(template, values);
66
+ html = html.replace(
67
+ '<html lang="en">',
68
+ `<html lang="en" data-render-strategy="${renderStrategy}" data-theme-mode="${theme.defaultMode}">`,
69
+ );
70
+ if (renderStrategy === 'user-switchable') {
71
+ html = html
72
+ .replace('</head>', `${switchableThemeStyle(theme)}\n </head>`)
73
+ .replace('</body>', `${switchableThemeControl(theme)}\n </body>`);
74
+ }
75
+
76
+ return {
77
+ artifactId: recipeArtifact.id,
78
+ type: recipeArtifact.type,
79
+ renderedPath,
80
+ publicUrl: baseUrl
81
+ ? `${baseUrl}/${renderedPath.slice('site/'.length)}`
82
+ : undefined,
83
+ mediaType: 'text/html',
84
+ html,
85
+ };
86
+ }
87
+
88
+ export function substituteTemplate(template, values) {
89
+ if (typeof template !== 'string' || !isObject(values)) {
90
+ throw new TypeError('Template substitution requires a string and values.');
91
+ }
92
+
93
+ const expected = new Set(
94
+ [...template.matchAll(TOKEN_PATTERN)].map(([, key]) => key),
95
+ );
96
+ const unknown = Object.keys(values).filter((key) => !expected.has(key));
97
+ if (unknown.length > 0) {
98
+ throw new Error(`Unknown template token: ${unknown[0]}.`);
99
+ }
100
+
101
+ const missing = [...expected].filter(
102
+ (key) => !Object.hasOwn(values, key) || typeof values[key] !== 'string',
103
+ );
104
+ if (missing.length > 0) {
105
+ throw new Error(`Unresolved template token: ${missing[0]}.`);
106
+ }
107
+
108
+ const rendered = template.replace(TOKEN_PATTERN, (_, key) => values[key]);
109
+ const unresolved = rendered.match(TOKEN_PATTERN)?.[1];
110
+ if (unresolved) {
111
+ throw new Error(`Unresolved template token: ${unresolved}.`);
112
+ }
113
+ return rendered;
114
+ }
115
+
116
+ function templateValues({
117
+ recipeArtifact,
118
+ content,
119
+ sections,
120
+ links,
121
+ theme,
122
+ renderedPath,
123
+ baseUrl,
124
+ }) {
125
+ const title = escapeHtml(content.title ?? humanize(recipeArtifact.id));
126
+ const description = escapeHtml(content.description ?? '');
127
+ const eyebrow = escapeHtml(content.eyebrow ?? recipeArtifact.type);
128
+ const footer = [
129
+ escapeHtml(content.footer ?? ''),
130
+ renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
131
+ ]
132
+ .filter(Boolean)
133
+ .join(' ');
134
+ const common = {
135
+ THEME_CSS: themeDeclarations(theme.modes[theme.defaultMode], theme),
136
+ TITLE: title,
137
+ DESCRIPTION: description,
138
+ };
139
+
140
+ switch (recipeArtifact.template) {
141
+ case 'house-style':
142
+ return {
143
+ ...common,
144
+ EYEBROW: eyebrow,
145
+ NAVIGATION: renderNavigation(
146
+ sections,
147
+ links,
148
+ content.slug,
149
+ renderedPath,
150
+ baseUrl,
151
+ ),
152
+ CONTENT: renderSections(sections),
153
+ FOOTER: footer,
154
+ };
155
+ case 'engineer-tour':
156
+ return {
157
+ ...common,
158
+ EYEBROW: eyebrow,
159
+ NAVIGATION: renderNavigation(
160
+ sections,
161
+ links,
162
+ content.slug,
163
+ renderedPath,
164
+ baseUrl,
165
+ ),
166
+ CONTENT: renderSections(sections, { tour: true }),
167
+ DIAGRAM: renderDiagram(sections),
168
+ FOOTER: footer,
169
+ };
170
+ case 'deck-shell':
171
+ return {
172
+ ...common,
173
+ SLIDES: renderSlides(
174
+ sections,
175
+ links,
176
+ content.slug,
177
+ renderedPath,
178
+ baseUrl,
179
+ ),
180
+ };
181
+ case 'diagram-shell':
182
+ return {
183
+ ...common,
184
+ DIAGRAM: renderDiagram(sections),
185
+ LEGEND: [
186
+ sections
187
+ .map(
188
+ (section) =>
189
+ `<span>${escapeHtml(section.title ?? humanize(section.id))}</span>`,
190
+ )
191
+ .join(''),
192
+ renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
193
+ ].join(''),
194
+ };
195
+ default:
196
+ throw new Error(`Unknown bundled template: ${recipeArtifact.template}.`);
197
+ }
198
+ }
199
+
200
+ function renderNavigation(sections, links, slug, renderedPath, baseUrl) {
201
+ const sectionLinks = sections
202
+ .map(
203
+ (section) =>
204
+ `<a href="#${escapeAttribute(section.anchor)}" data-target="${escapeAttribute(section.anchor)}">${escapeHtml(section.title ?? humanize(section.id))}</a>`,
205
+ )
206
+ .join('');
207
+ return `${sectionLinks}${renderRelatedLinks(links, slug, renderedPath, baseUrl)}`;
208
+ }
209
+
210
+ function renderSections(sections, { tour = false } = {}) {
211
+ return sections
212
+ .map((section, index) => {
213
+ const tourAttributes = tour
214
+ ? ` data-active-nodes="node-${index + 1}" data-active-edges=""`
215
+ : '';
216
+ return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><div class="section-number">${index + 1}</div><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2><p>${escapeHtml(section.content)}</p></section>`;
217
+ })
218
+ .join('');
219
+ }
220
+
221
+ function renderSlides(sections, links, slug, renderedPath, baseUrl) {
222
+ const slides = sections
223
+ .map(
224
+ (section) =>
225
+ `<section class="slide" id="${escapeAttribute(section.anchor)}"><div class="slide__content"><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2><p>${escapeHtml(section.content)}</p></div></section>`,
226
+ )
227
+ .join('');
228
+ const related = renderRelatedLinks(links, slug, renderedPath, baseUrl);
229
+ return related
230
+ ? `${slides}<section class="slide"><div class="slide__content"><h2>Related artifacts</h2>${related}</div></section>`
231
+ : slides;
232
+ }
233
+
234
+ function renderDiagram(sections) {
235
+ return sections
236
+ .map((section, index) => {
237
+ const y = 80 + index * 120;
238
+ return `<g data-node="node-${index + 1}" class="node"><rect x="80" y="${y}" width="360" height="72" rx="8"></rect><text x="104" y="${y + 43}">${escapeHtml(section.title ?? humanize(section.id))}</text></g>`;
239
+ })
240
+ .join('');
241
+ }
242
+
243
+ function renderRelatedLinks(links, slug, renderedPath, baseUrl) {
244
+ if (links.length === 0) return '';
245
+ return links
246
+ .map((link) => {
247
+ const targetPath = artifactPath(link, slug);
248
+ const href = baseUrl
249
+ ? `${baseUrl}/${targetPath.slice('site/'.length)}`
250
+ : posix.relative(posix.dirname(renderedPath), targetPath);
251
+ return `<a href="${escapeAttribute(href)}">${escapeHtml(link.label)}</a>`;
252
+ })
253
+ .join(' ');
254
+ }
255
+
256
+ function artifactPath(artifact, slug) {
257
+ const directory = TYPE_DIRECTORIES.get(artifact.type);
258
+ if (!directory)
259
+ throw new Error(`Unsupported artifact type: ${artifact.type}.`);
260
+ const parts = ['site', directory, slug];
261
+ if (['diagram', 'deck'].includes(artifact.type)) parts.push(artifact.id);
262
+ parts.push('index.html');
263
+ return parts.join('/');
264
+ }
265
+
266
+ function themeDeclarations(mode, theme) {
267
+ return [
268
+ ['canvas', mode.surface.canvas],
269
+ ['panel', mode.surface.panel],
270
+ ['panel-muted', mode.surface.elevated],
271
+ ['border', mode.surface.elevated],
272
+ ['ink', mode.ink.primary],
273
+ ['muted', mode.ink.muted],
274
+ ['accent', mode.accent.primary],
275
+ ['success', mode.status.success],
276
+ ['warning', mode.status.warning],
277
+ ['danger', mode.status.danger],
278
+ ['sans', theme.typography.sans.join(', ')],
279
+ ['serif', theme.typography.serif.join(', ')],
280
+ ['mono', theme.typography.mono.join(', ')],
281
+ ['radius', `${theme.geometry.radius.md}px`],
282
+ ]
283
+ .map(([name, value]) => `--${name}: ${value};`)
284
+ .join('\n ');
285
+ }
286
+
287
+ function switchableThemeStyle(theme) {
288
+ return `<style data-theme-mode="${oppositeMode(theme.defaultMode)}">
289
+ :root[data-theme-mode="${oppositeMode(theme.defaultMode)}"] {
290
+ ${themeDeclarations(theme.modes[oppositeMode(theme.defaultMode)], theme)}
291
+ }
292
+ [data-theme-toggle] {
293
+ position: fixed;
294
+ z-index: 10;
295
+ inset: 1rem 1rem auto auto;
296
+ border: 1px solid var(--border);
297
+ border-radius: var(--radius);
298
+ background: var(--panel);
299
+ color: var(--ink);
300
+ padding: 0.55rem 0.75rem;
301
+ font: 0.875rem var(--sans);
302
+ cursor: pointer;
303
+ }
304
+ [data-theme-toggle]:focus-visible {
305
+ outline: 2px solid var(--accent);
306
+ outline-offset: 2px;
307
+ }
308
+ </style>`;
309
+ }
310
+
311
+ function switchableThemeControl(theme) {
312
+ const defaultMode = theme.defaultMode;
313
+ const alternateMode = oppositeMode(defaultMode);
314
+ return `<button type="button" data-theme-toggle aria-pressed="${defaultMode === 'dark'}" aria-label="Switch color theme">Theme: ${humanize(defaultMode)}</button>
315
+ <script>
316
+ (() => {
317
+ const root = document.documentElement;
318
+ const toggle = document.querySelector('[data-theme-toggle]');
319
+ const storageKey = 'explainer-theme-mode:' + location.pathname;
320
+ const supported = new Set(['${defaultMode}', '${alternateMode}']);
321
+ const apply = (mode, persist = true) => {
322
+ root.dataset.themeMode = mode;
323
+ toggle.setAttribute('aria-pressed', String(mode === 'dark'));
324
+ toggle.textContent = 'Theme: ' + mode[0].toUpperCase() + mode.slice(1);
325
+ if (persist) {
326
+ try { localStorage.setItem(storageKey, mode); } catch {}
327
+ }
328
+ };
329
+ let saved;
330
+ try { saved = localStorage.getItem(storageKey); } catch {}
331
+ if (supported.has(saved)) apply(saved, false);
332
+ toggle.addEventListener('click', () =>
333
+ apply(root.dataset.themeMode === '${defaultMode}' ? '${alternateMode}' : '${defaultMode}')
334
+ );
335
+ })();
336
+ </script>`;
337
+ }
338
+
339
+ function assertRecipeArtifact(artifact) {
340
+ if (
341
+ !isObject(artifact) ||
342
+ !hasExactKeys(artifact, ['id', 'type', 'template', 'required']) ||
343
+ !SLUG_PATTERN.test(artifact.id ?? '') ||
344
+ typeof artifact.required !== 'boolean'
345
+ ) {
346
+ throw new TypeError(
347
+ 'Recipe artifact is not a validated artifact descriptor.',
348
+ );
349
+ }
350
+ const expectedTemplate = TEMPLATE_BY_TYPE.get(artifact.type);
351
+ if (!expectedTemplate || artifact.template !== expectedTemplate) {
352
+ throw new Error(
353
+ `Unknown or incompatible bundled template: ${String(artifact.template)}.`,
354
+ );
355
+ }
356
+ }
357
+
358
+ function assertContent(content, recipeArtifact) {
359
+ if (!isObject(content))
360
+ throw new TypeError('Render content must be an object.');
361
+ const unknown = Object.keys(content).filter((key) => !CONTENT_KEYS.has(key));
362
+ if (unknown.length > 0) {
363
+ throw new TypeError(`Unknown render content field: ${unknown[0]}.`);
364
+ }
365
+ if (
366
+ content.artifactId !== recipeArtifact.id ||
367
+ !SLUG_PATTERN.test(content.slug ?? '') ||
368
+ !Array.isArray(content.sections) ||
369
+ content.sections.length === 0
370
+ ) {
371
+ throw new TypeError('Render content does not match the recipe artifact.');
372
+ }
373
+ for (const field of ['title', 'description', 'eyebrow', 'footer']) {
374
+ if (content[field] !== undefined && typeof content[field] !== 'string') {
375
+ throw new TypeError(`Render content ${field} must be a string.`);
376
+ }
377
+ }
378
+ for (const section of content.sections) {
379
+ if (
380
+ !isObject(section) ||
381
+ !hasOnlyKeys(section, ['id', 'title', 'content']) ||
382
+ !SLUG_PATTERN.test(section.id ?? '') ||
383
+ typeof section.content !== 'string' ||
384
+ (section.title !== undefined && typeof section.title !== 'string')
385
+ ) {
386
+ throw new TypeError('Render sections require safe ids and text content.');
387
+ }
388
+ }
389
+ const links = content.artifactLinks ?? [];
390
+ if (!Array.isArray(links)) {
391
+ throw new TypeError('Artifact links must be an array.');
392
+ }
393
+ for (const link of links) {
394
+ if (
395
+ !isObject(link) ||
396
+ !hasExactKeys(link, ['id', 'type', 'label']) ||
397
+ !SLUG_PATTERN.test(link.id ?? '') ||
398
+ !TYPE_DIRECTORIES.has(link.type) ||
399
+ typeof link.label !== 'string'
400
+ ) {
401
+ throw new TypeError(
402
+ 'Artifact cross-links must use typed artifact targets.',
403
+ );
404
+ }
405
+ }
406
+ }
407
+
408
+ function assertTheme(theme) {
409
+ const validation = validateContract('theme', theme);
410
+ const identity = isObject(theme) ? structuredClone(theme) : {};
411
+ delete identity.bundleHash;
412
+ if (!validation.valid || theme.bundleHash !== canonicalHash(identity)) {
413
+ throw new Error('Renderer requires a validated resolved theme.');
414
+ }
415
+ }
416
+
417
+ function assertRenderStrategy(renderStrategy) {
418
+ if (!RENDER_STRATEGIES.has(renderStrategy)) {
419
+ throw new Error(`Unsupported render strategy: ${String(renderStrategy)}.`);
420
+ }
421
+ }
422
+
423
+ function normalizePublicBaseUrl(value) {
424
+ if (value === undefined) return undefined;
425
+ if (typeof value !== 'string') {
426
+ throw new TypeError('Public base URL must be an HTTPS URL.');
427
+ }
428
+ let url;
429
+ try {
430
+ url = new URL(value);
431
+ } catch {
432
+ throw new TypeError('Public base URL must be an HTTPS URL.');
433
+ }
434
+ if (
435
+ url.protocol !== 'https:' ||
436
+ url.username ||
437
+ url.password ||
438
+ url.search ||
439
+ url.hash
440
+ ) {
441
+ throw new TypeError(
442
+ 'Public base URL must be an HTTPS URL without credentials.',
443
+ );
444
+ }
445
+ return url.href.replace(/\/+$/, '');
446
+ }
447
+
448
+ function escapeHtml(value) {
449
+ return String(value)
450
+ .replaceAll('&', '&amp;')
451
+ .replaceAll('<', '&lt;')
452
+ .replaceAll('>', '&gt;')
453
+ .replaceAll('"', '&quot;')
454
+ .replaceAll("'", '&#39;');
455
+ }
456
+
457
+ function escapeAttribute(value) {
458
+ return escapeHtml(value);
459
+ }
460
+
461
+ function humanize(value) {
462
+ return value
463
+ .split('-')
464
+ .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
465
+ .join(' ');
466
+ }
467
+
468
+ function oppositeMode(mode) {
469
+ return mode === 'light' ? 'dark' : 'light';
470
+ }
471
+
472
+ function hasExactKeys(value, keys) {
473
+ return (
474
+ Object.keys(value).length === keys.length &&
475
+ keys.every((key) => Object.hasOwn(value, key))
476
+ );
477
+ }
478
+
479
+ function hasOnlyKeys(value, keys) {
480
+ return Object.keys(value).every((key) => keys.includes(key));
481
+ }
482
+
483
+ function isObject(value) {
484
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
485
+ }