@open-agent-toolkit/cli 0.2.73 → 0.2.74

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 (149) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/cli-utilities/configuration.md +3 -56
  3. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  4. package/assets/docs/contributing/code.md +2 -2
  5. package/assets/docs/contributing/index.md +0 -1
  6. package/assets/docs/reference/cli-reference.md +1 -1
  7. package/assets/docs/reference/troubleshooting.md +17 -40
  8. package/assets/docs/workflows/projects/artifacts.md +16 -34
  9. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  10. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  11. package/assets/docs/workflows/skills/index.md +1 -2
  12. package/assets/public-package-versions.json +4 -4
  13. package/assets/skills/explainer-kit/SKILL.md +96 -171
  14. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  15. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  16. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  17. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  18. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  19. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  20. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  21. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  22. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  23. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  24. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  25. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  26. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  27. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  28. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  29. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  30. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  31. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  32. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  33. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  34. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  35. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  36. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  37. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  38. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  39. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  40. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  41. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  42. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  43. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  44. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  45. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  46. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  47. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  48. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  49. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  50. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  51. package/assets/skills/oat-project-document/SKILL.md +1 -1
  52. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  53. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  54. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  55. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  57. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  58. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  59. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  60. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  62. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  63. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  64. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  65. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  66. package/dist/commands/config/index.d.ts.map +1 -1
  67. package/dist/commands/config/index.js +9 -113
  68. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  69. package/dist/commands/project/archive/archive-utils.js +32 -184
  70. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  71. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  72. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  73. package/dist/config/oat-config.d.ts +0 -11
  74. package/dist/config/oat-config.d.ts.map +1 -1
  75. package/dist/config/oat-config.js +0 -34
  76. package/dist/config/resolve.d.ts.map +1 -1
  77. package/dist/config/resolve.js +0 -8
  78. package/dist/release/public-package-contract.d.ts.map +1 -1
  79. package/dist/release/public-package-contract.js +2 -10
  80. package/dist/validation/project-state.d.ts.map +1 -1
  81. package/dist/validation/project-state.js +42 -3
  82. package/package.json +2 -2
  83. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  84. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  85. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  86. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  87. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  88. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  89. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  90. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  91. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  92. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  93. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  94. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  95. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  96. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  97. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  98. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  99. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  100. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  101. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  102. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  103. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  104. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  105. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  106. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  107. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  108. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  109. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  110. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  111. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  112. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  113. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  114. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  115. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  116. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  117. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  118. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  119. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  120. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  121. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  122. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  123. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  124. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  125. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  126. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  127. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  128. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  129. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  130. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  131. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  132. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  133. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  134. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  135. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  136. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  137. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  138. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  139. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  140. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  141. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  142. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  143. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  144. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  145. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  146. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  147. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  148. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  149. package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
@@ -1,703 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { posix } from 'node:path';
3
-
4
- import { canonicalHash, validateContract } from './contracts.mjs';
5
- import { renderDiagram as renderDiagramBlock } from './diagram.mjs';
6
- import { MarkdownSafetyError, parseMarkdownDocument } from './markdown.mjs';
7
-
8
- const RENDER_STRATEGIES = new Set(['default-only', 'user-switchable']);
9
- const TEMPLATE_BY_TYPE = new Map([
10
- ['hub', 'house-style'],
11
- ['diagram', 'diagram-shell'],
12
- ['explainer', 'engineer-tour'],
13
- ['deck', 'deck-shell'],
14
- ]);
15
- const TYPE_DIRECTORIES = new Map([
16
- ['hub', 'initiatives'],
17
- ['diagram', 'diagrams'],
18
- ['explainer', 'explainers'],
19
- ['deck', 'decks'],
20
- ]);
21
- const CONTENT_KEYS = new Set([
22
- 'artifactId',
23
- 'artifactLinks',
24
- 'description',
25
- 'eyebrow',
26
- 'footer',
27
- 'sections',
28
- 'slug',
29
- 'title',
30
- ]);
31
- const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
32
- const TOKEN_PATTERN = /{{([A-Z][A-Z_]*)}}/g;
33
-
34
- export async function renderArtifact({
35
- recipeArtifact,
36
- content,
37
- factBase,
38
- theme,
39
- renderStrategy,
40
- publicBaseUrl,
41
- }) {
42
- assertRecipeArtifact(recipeArtifact);
43
- assertContent(content, recipeArtifact);
44
- assertFactBase(factBase);
45
- assertTheme(theme);
46
- assertRenderStrategy(renderStrategy);
47
-
48
- const baseUrl = normalizePublicBaseUrl(publicBaseUrl);
49
- const renderedPath = artifactPath(recipeArtifact, content.slug);
50
- const template = await readFile(
51
- new URL(`../../templates/${recipeArtifact.template}.html`, import.meta.url),
52
- 'utf8',
53
- );
54
- const sections = content.sections.map((section) => ({
55
- ...section,
56
- anchor: section.id,
57
- backlinks: sourceBacklinksForSection(factBase, section.id),
58
- ...prepareSectionMarkdown(section.content, theme),
59
- }));
60
- const links = content.artifactLinks ?? [];
61
- const values = templateValues({
62
- recipeArtifact,
63
- content,
64
- sections,
65
- links,
66
- theme,
67
- renderedPath,
68
- baseUrl,
69
- });
70
-
71
- let html = substituteTemplate(template, values);
72
- html = html.replace(
73
- '<html lang="en">',
74
- `<html lang="en" data-render-strategy="${renderStrategy}" data-theme-mode="${theme.defaultMode}">`,
75
- );
76
- if (renderStrategy === 'user-switchable') {
77
- html = html
78
- .replace('</head>', `${switchableThemeStyle(theme)}\n </head>`)
79
- .replace('</body>', `${switchableThemeControl(theme)}\n </body>`);
80
- }
81
-
82
- return {
83
- artifactId: recipeArtifact.id,
84
- type: recipeArtifact.type,
85
- renderedPath,
86
- publicUrl: baseUrl
87
- ? `${baseUrl}/${renderedPath.slice('site/'.length)}`
88
- : undefined,
89
- mediaType: 'text/html',
90
- html,
91
- warnings: sections.flatMap((section) => section.warnings),
92
- };
93
- }
94
-
95
- export function substituteTemplate(template, values) {
96
- if (typeof template !== 'string' || !isObject(values)) {
97
- throw new TypeError('Template substitution requires a string and values.');
98
- }
99
-
100
- const expected = new Set(
101
- [...template.matchAll(TOKEN_PATTERN)].map(([, key]) => key),
102
- );
103
- const unknown = Object.keys(values).filter((key) => !expected.has(key));
104
- if (unknown.length > 0) {
105
- throw new Error(`Unknown template token: ${unknown[0]}.`);
106
- }
107
-
108
- const missing = [...expected].filter(
109
- (key) => !Object.hasOwn(values, key) || typeof values[key] !== 'string',
110
- );
111
- if (missing.length > 0) {
112
- throw new Error(`Unresolved template token: ${missing[0]}.`);
113
- }
114
-
115
- const normalizedTemplate = normalizeEmptySubstitutionWhitespace(
116
- template,
117
- values,
118
- );
119
- const rendered = normalizedTemplate.replace(
120
- TOKEN_PATTERN,
121
- (_, key) => values[key],
122
- );
123
- const unresolved = rendered.match(TOKEN_PATTERN)?.[1];
124
- if (unresolved) {
125
- throw new Error(`Unresolved template token: ${unresolved}.`);
126
- }
127
- return rendered;
128
- }
129
-
130
- function normalizeEmptySubstitutionWhitespace(template, values) {
131
- let normalized = template;
132
- let previous;
133
- do {
134
- previous = normalized;
135
- normalized = normalized.replace(
136
- /[ \t]*{{([A-Z][A-Z_]*)}}[ \t]*(?=\r?$)/gm,
137
- (match, key) => (values[key] === '' ? '' : match),
138
- );
139
- } while (normalized !== previous);
140
- return normalized;
141
- }
142
-
143
- function templateValues({
144
- recipeArtifact,
145
- content,
146
- sections,
147
- links,
148
- theme,
149
- renderedPath,
150
- baseUrl,
151
- }) {
152
- const title = escapeHtml(content.title ?? humanize(recipeArtifact.id));
153
- const description = escapeHtml(content.description ?? '');
154
- const eyebrow = escapeHtml(content.eyebrow ?? recipeArtifact.type);
155
- const footer = [
156
- escapeHtml(content.footer ?? ''),
157
- renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
158
- ]
159
- .filter(Boolean)
160
- .join(' ');
161
- const common = {
162
- THEME_CSS: themeDeclarations(theme.modes[theme.defaultMode], theme),
163
- TITLE: title,
164
- DESCRIPTION: description,
165
- };
166
-
167
- switch (recipeArtifact.template) {
168
- case 'house-style':
169
- return {
170
- ...common,
171
- EYEBROW: eyebrow,
172
- NAVIGATION: renderNavigation(
173
- sections,
174
- links,
175
- content.slug,
176
- renderedPath,
177
- baseUrl,
178
- ),
179
- CONTENT: renderSections(sections),
180
- FOOTER: footer,
181
- };
182
- case 'engineer-tour':
183
- return {
184
- ...common,
185
- EYEBROW: eyebrow,
186
- NAVIGATION: renderNavigation(
187
- sections,
188
- links,
189
- content.slug,
190
- renderedPath,
191
- baseUrl,
192
- ),
193
- CONTENT: renderSections(sections, { tour: true }),
194
- DIAGRAM: renderLegacyDiagram(sections),
195
- FOOTER: footer,
196
- };
197
- case 'deck-shell':
198
- return {
199
- ...common,
200
- SLIDES: renderSlides(
201
- sections,
202
- links,
203
- content.slug,
204
- renderedPath,
205
- baseUrl,
206
- ),
207
- };
208
- case 'diagram-shell':
209
- return {
210
- ...common,
211
- DIAGRAM: renderLegacyDiagram(sections),
212
- LEGEND: [
213
- sections
214
- .map(
215
- (section) =>
216
- `<span>${escapeHtml(section.title ?? humanize(section.id))}</span>`,
217
- )
218
- .join(''),
219
- renderSourceBacklinks(sections.flatMap(({ backlinks }) => backlinks)),
220
- renderRelatedLinks(links, content.slug, renderedPath, baseUrl),
221
- ].join(''),
222
- };
223
- default:
224
- throw new Error(`Unknown bundled template: ${recipeArtifact.template}.`);
225
- }
226
- }
227
-
228
- function renderNavigation(sections, links, slug, renderedPath, baseUrl) {
229
- const sectionLinks = sections
230
- .map(
231
- (section) =>
232
- `<a href="#${escapeAttribute(section.anchor)}" data-target="${escapeAttribute(section.anchor)}">${escapeHtml(section.title ?? humanize(section.id))}</a>`,
233
- )
234
- .join('');
235
- return `${sectionLinks}${renderRelatedLinks(links, slug, renderedPath, baseUrl)}`;
236
- }
237
-
238
- function renderSections(sections, { tour = false } = {}) {
239
- return sections
240
- .map((section, index) => {
241
- const tourAttributes = tour
242
- ? ` data-active-nodes="node-${index + 1}" data-active-edges=""`
243
- : '';
244
- return `<section id="${escapeAttribute(section.anchor)}"${tourAttributes}><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2>${renderMarkdownNodes(section.ast.children)}${renderSourceBacklinks(section.backlinks)}</section>`;
245
- })
246
- .join('');
247
- }
248
-
249
- function prepareSectionMarkdown(content, theme) {
250
- try {
251
- const parsed = parseMarkdownDocument(content);
252
- const diagramWarnings = [];
253
- visitMarkdownNodes(parsed.ast.children, (node) => {
254
- if (node.type !== 'diagram') return;
255
- const rendered = renderDiagramBlock(node.source, { theme });
256
- node.renderedHtml = rendered.html;
257
- diagramWarnings.push(...rendered.warnings);
258
- });
259
- return {
260
- ...parsed,
261
- warnings: [...parsed.warnings, ...diagramWarnings],
262
- };
263
- } catch (error) {
264
- if (
265
- !(error instanceof MarkdownSafetyError) ||
266
- !/raw html|node type: html/i.test(error.message)
267
- ) {
268
- throw error;
269
- }
270
- return {
271
- ast: {
272
- type: 'document',
273
- children: [
274
- {
275
- type: 'paragraph',
276
- children: [{ type: 'text', value: content }],
277
- },
278
- ],
279
- },
280
- warnings: [
281
- {
282
- code: 'legacy-raw-html-escaped',
283
- message: 'Legacy section markup was rendered as escaped text.',
284
- },
285
- ],
286
- };
287
- }
288
- }
289
-
290
- function visitMarkdownNodes(nodes, callback) {
291
- for (const node of nodes) {
292
- callback(node);
293
- if (Array.isArray(node.children)) {
294
- visitMarkdownNodes(node.children, callback);
295
- }
296
- }
297
- }
298
-
299
- function renderMarkdownNodes(nodes) {
300
- return nodes.map(renderMarkdownNode).join('');
301
- }
302
-
303
- function renderMarkdownNode(node) {
304
- switch (node.type) {
305
- case 'text':
306
- return escapeHtml(node.value);
307
- case 'paragraph':
308
- if (node.children.length === 1 && node.children[0].type === 'figure') {
309
- return renderMarkdownNode(node.children[0]);
310
- }
311
- return `<p>${renderMarkdownNodes(node.children)}</p>`;
312
- case 'heading':
313
- return `<h${node.depth}>${renderMarkdownNodes(node.children)}</h${node.depth}>`;
314
- case 'strong':
315
- return `<strong>${renderMarkdownNodes(node.children)}</strong>`;
316
- case 'emphasis':
317
- return `<em>${renderMarkdownNodes(node.children)}</em>`;
318
- case 'delete':
319
- return `<del>${renderMarkdownNodes(node.children)}</del>`;
320
- case 'inlineCode':
321
- return `<code>${escapeHtml(node.value)}</code>`;
322
- case 'link':
323
- return `<a href="${escapeAttribute(node.url)}"${node.title ? ` title="${escapeAttribute(node.title)}"` : ''}>${renderMarkdownNodes(node.children)}</a>`;
324
- case 'figure':
325
- return `<figure><a href="${escapeAttribute(node.url)}">${escapeHtml(node.alt || 'View figure')}</a>${node.title || node.alt ? `<figcaption>${escapeHtml(node.title ?? node.alt)}</figcaption>` : ''}</figure>`;
326
- case 'blockquote':
327
- return `<blockquote>${renderMarkdownNodes(node.children)}</blockquote>`;
328
- case 'callout':
329
- return `<aside class="callout callout--${escapeAttribute(node.kind)}" data-callout="${escapeAttribute(node.kind)}"><div class="callout__label">${escapeHtml(humanize(node.kind))}</div>${renderMarkdownNodes(node.children)}</aside>`;
330
- case 'timeline':
331
- return `<ol class="timeline">${node.entries
332
- .map(
333
- (entry) =>
334
- `<li><time>${escapeHtml(entry.date)}</time><span>${escapeHtml(entry.label)}</span></li>`,
335
- )
336
- .join('')}</ol>`;
337
- case 'code':
338
- return `<pre><code${node.language ? ` class="language-${escapeAttribute(node.language)}"` : ''}>${escapeHtml(node.value)}</code></pre>`;
339
- case 'diagram':
340
- return `<div class="diagram-scroll">${node.renderedHtml}</div>`;
341
- case 'list':
342
- return renderMarkdownList(node);
343
- case 'listItem':
344
- return `<li>${node.checked === null ? '' : `<input type="checkbox" disabled${node.checked ? ' checked' : ''} aria-label="${node.checked ? 'Completed' : 'Incomplete'} task" />`}${renderMarkdownNodes(node.children)}</li>`;
345
- case 'table':
346
- return `<div class="table-scroll"><table><thead><tr>${node.header
347
- .map((cell) => `<th>${renderMarkdownNodes(cell)}</th>`)
348
- .join('')}</tr></thead><tbody>${node.rows
349
- .map(
350
- (row) =>
351
- `<tr>${row
352
- .map((cell) => `<td>${renderMarkdownNodes(cell)}</td>`)
353
- .join('')}</tr>`,
354
- )
355
- .join('')}</tbody></table></div>`;
356
- default:
357
- throw new MarkdownSafetyError(
358
- `Unsupported markdown render node: ${String(node.type)}.`,
359
- );
360
- }
361
- }
362
-
363
- function renderMarkdownList(node) {
364
- const tag = node.ordered ? 'ol' : 'ul';
365
- const attributes = [
366
- node.task ? ' class="task-list"' : '',
367
- node.ordered && node.start !== 1 ? ` start="${node.start}"` : '',
368
- ].join('');
369
- return `<${tag}${attributes}>${renderMarkdownNodes(node.children)}</${tag}>`;
370
- }
371
-
372
- function renderSlides(sections, links, slug, renderedPath, baseUrl) {
373
- const slides = sections
374
- .map(
375
- (section) =>
376
- `<section class="slide" id="${escapeAttribute(section.anchor)}"><div class="slide__content"><h2>${escapeHtml(section.title ?? humanize(section.id))}</h2><p>${escapeHtml(section.content)}</p>${renderSourceBacklinks(section.backlinks)}</div></section>`,
377
- )
378
- .join('');
379
- const related = renderRelatedLinks(links, slug, renderedPath, baseUrl);
380
- return related
381
- ? `${slides}<section class="slide"><div class="slide__content"><h2>Related artifacts</h2>${related}</div></section>`
382
- : slides;
383
- }
384
-
385
- // Nodes must fit the shell's 360x540 viewBox, so the step shrinks as sections
386
- // are added rather than overflowing the canvas.
387
- function renderLegacyDiagram(sections) {
388
- const margin = 16;
389
- const span = 540 - margin * 2;
390
- const step = Math.min(96, span / Math.max(sections.length, 1));
391
- const height = Math.max(24, step - 16);
392
- return sections
393
- .map((section, index) => {
394
- const y = margin + index * step;
395
- const label = section.title ?? humanize(section.id);
396
- return `<g data-node="node-${index + 1}" class="node"><rect x="${margin}" y="${y}" width="${360 - margin * 2}" height="${height}" rx="8"></rect><text x="${margin + 14}" y="${y + height / 2 + 5}">${escapeHtml(label)}</text></g>`;
397
- })
398
- .join('');
399
- }
400
-
401
- function renderRelatedLinks(links, slug, renderedPath, baseUrl) {
402
- if (links.length === 0) return '';
403
- return links
404
- .map((link) => {
405
- const targetPath = artifactPath(link, slug);
406
- const href = baseUrl
407
- ? `${baseUrl}/${targetPath.slice('site/'.length)}`
408
- : posix.relative(posix.dirname(renderedPath), targetPath);
409
- return `<a href="${escapeAttribute(href)}">${escapeHtml(link.label)}</a>`;
410
- })
411
- .join(' ');
412
- }
413
-
414
- function sourceBacklinksForSection(factBase, sectionId) {
415
- if (factBase === undefined) return [];
416
- return [...factBase.claims, ...factBase.unresolvedClaims]
417
- .filter(
418
- (claim) =>
419
- claim.sections === undefined || claim.sections.includes(sectionId),
420
- )
421
- .flatMap((claim) =>
422
- claim.citations
423
- .filter(({ url }) => typeof url === 'string')
424
- .map((citation) => ({
425
- claimId: claim.id,
426
- claimText: claim.text,
427
- repository: citation.repository,
428
- path: citation.path,
429
- lineRange: citation.lineRange,
430
- url: citation.url,
431
- })),
432
- );
433
- }
434
-
435
- function renderSourceBacklinks(backlinks) {
436
- if (backlinks.length === 0) return '';
437
- const items = backlinks
438
- .map(
439
- (backlink) =>
440
- `<li><span>${escapeHtml(backlink.claimText)}</span> <a href="${escapeAttribute(backlink.url)}">${escapeHtml(`${backlink.repository}/${backlink.path}:L${backlink.lineRange.start}${backlink.lineRange.end === backlink.lineRange.start ? '' : `-L${backlink.lineRange.end}`}`)}</a></li>`,
441
- )
442
- .join('');
443
- return `<aside class="source-backlinks" aria-label="Sources"><h3>Sources</h3><ul>${items}</ul></aside>`;
444
- }
445
-
446
- export function artifactPath(artifact, slug) {
447
- const directory = TYPE_DIRECTORIES.get(artifact.type);
448
- if (!directory)
449
- throw new Error(`Unsupported artifact type: ${artifact.type}.`);
450
- const parts = ['site', directory, slug];
451
- const origin = artifact.origin ?? 'floor';
452
- if (
453
- origin === 'expansion' ||
454
- (origin === 'floor' && ['diagram', 'deck'].includes(artifact.type))
455
- ) {
456
- parts.push(artifact.id);
457
- }
458
- parts.push('index.html');
459
- return parts.join('/');
460
- }
461
-
462
- function themeDeclarations(mode, theme) {
463
- return [
464
- ['canvas', mode.surface.canvas],
465
- ['panel', mode.surface.panel],
466
- ['panel-muted', mode.surface.elevated],
467
- ['border', mode.surface.elevated],
468
- ['ink', mode.ink.primary],
469
- ['muted', mode.ink.muted],
470
- ['accent', mode.accent.primary],
471
- ['success', mode.status.success],
472
- ['warning', mode.status.warning],
473
- ['danger', mode.status.danger],
474
- ['sans', theme.typography.sans.join(', ')],
475
- ['serif', theme.typography.serif.join(', ')],
476
- ['mono', theme.typography.mono.join(', ')],
477
- ['radius', `${theme.geometry.radius.md}px`],
478
- ]
479
- .map(([name, value]) => `--${name}: ${value};`)
480
- .join('\n ');
481
- }
482
-
483
- function switchableThemeStyle(theme) {
484
- return `<style data-theme-mode="${oppositeMode(theme.defaultMode)}">
485
- :root[data-theme-mode="${oppositeMode(theme.defaultMode)}"] {
486
- ${themeDeclarations(theme.modes[oppositeMode(theme.defaultMode)], theme)}
487
- }
488
- [data-theme-toggle] {
489
- position: fixed;
490
- z-index: 10;
491
- inset: 1rem 1rem auto auto;
492
- border: 1px solid var(--border);
493
- border-radius: var(--radius);
494
- background: var(--panel);
495
- color: var(--ink);
496
- padding: 0.55rem 0.75rem;
497
- font: 0.875rem var(--sans);
498
- cursor: pointer;
499
- }
500
- [data-theme-toggle]:focus-visible {
501
- outline: 2px solid var(--accent);
502
- outline-offset: 2px;
503
- }
504
- </style>`;
505
- }
506
-
507
- function switchableThemeControl(theme) {
508
- const defaultMode = theme.defaultMode;
509
- const alternateMode = oppositeMode(defaultMode);
510
- return `<button type="button" data-theme-toggle aria-pressed="${defaultMode === 'dark'}" aria-label="Switch color theme">Theme: ${humanize(defaultMode)}</button>
511
- <script>
512
- (() => {
513
- const root = document.documentElement;
514
- const toggle = document.querySelector('[data-theme-toggle]');
515
- const storageKey = 'explainer-theme-mode:' + location.pathname;
516
- const supported = new Set(['${defaultMode}', '${alternateMode}']);
517
- const apply = (mode, persist = true) => {
518
- root.dataset.themeMode = mode;
519
- toggle.setAttribute('aria-pressed', String(mode === 'dark'));
520
- toggle.textContent = 'Theme: ' + mode[0].toUpperCase() + mode.slice(1);
521
- if (persist) {
522
- try { localStorage.setItem(storageKey, mode); } catch {}
523
- }
524
- };
525
- let saved;
526
- try { saved = localStorage.getItem(storageKey); } catch {}
527
- if (supported.has(saved)) apply(saved, false);
528
- toggle.addEventListener('click', () =>
529
- apply(root.dataset.themeMode === '${defaultMode}' ? '${alternateMode}' : '${defaultMode}')
530
- );
531
- })();
532
- </script>`;
533
- }
534
-
535
- function assertRecipeArtifact(artifact) {
536
- const hasLegacyKeys =
537
- isObject(artifact) &&
538
- hasExactKeys(artifact, ['id', 'type', 'template', 'required']);
539
- const hasOriginKeys =
540
- isObject(artifact) &&
541
- hasExactKeys(artifact, ['id', 'type', 'template', 'required', 'origin']);
542
- if (
543
- !isObject(artifact) ||
544
- (!hasLegacyKeys && !hasOriginKeys) ||
545
- !SLUG_PATTERN.test(artifact.id ?? '') ||
546
- typeof artifact.required !== 'boolean' ||
547
- (artifact.origin !== undefined &&
548
- !['floor', 'expansion'].includes(artifact.origin))
549
- ) {
550
- throw new TypeError(
551
- 'Recipe artifact is not a validated artifact descriptor.',
552
- );
553
- }
554
- const expectedTemplate = TEMPLATE_BY_TYPE.get(artifact.type);
555
- if (!expectedTemplate || artifact.template !== expectedTemplate) {
556
- throw new Error(
557
- `Unknown or incompatible bundled template: ${String(artifact.template)}.`,
558
- );
559
- }
560
- }
561
-
562
- function assertContent(content, recipeArtifact) {
563
- if (!isObject(content))
564
- throw new TypeError('Render content must be an object.');
565
- const unknown = Object.keys(content).filter((key) => !CONTENT_KEYS.has(key));
566
- if (unknown.length > 0) {
567
- throw new TypeError(`Unknown render content field: ${unknown[0]}.`);
568
- }
569
- if (
570
- content.artifactId !== recipeArtifact.id ||
571
- !SLUG_PATTERN.test(content.slug ?? '') ||
572
- !Array.isArray(content.sections) ||
573
- content.sections.length === 0
574
- ) {
575
- throw new TypeError('Render content does not match the recipe artifact.');
576
- }
577
- for (const field of ['title', 'description', 'eyebrow', 'footer']) {
578
- if (content[field] !== undefined && typeof content[field] !== 'string') {
579
- throw new TypeError(`Render content ${field} must be a string.`);
580
- }
581
- }
582
- for (const section of content.sections) {
583
- if (
584
- !isObject(section) ||
585
- !hasOnlyKeys(section, ['id', 'title', 'content']) ||
586
- !SLUG_PATTERN.test(section.id ?? '') ||
587
- typeof section.content !== 'string' ||
588
- (section.title !== undefined && typeof section.title !== 'string')
589
- ) {
590
- throw new TypeError('Render sections require safe ids and text content.');
591
- }
592
- }
593
- const links = content.artifactLinks ?? [];
594
- if (!Array.isArray(links)) {
595
- throw new TypeError('Artifact links must be an array.');
596
- }
597
- for (const link of links) {
598
- const hasLegacyKeys =
599
- isObject(link) && hasExactKeys(link, ['id', 'type', 'label']);
600
- const hasOriginKeys =
601
- isObject(link) && hasExactKeys(link, ['id', 'type', 'label', 'origin']);
602
- if (
603
- !isObject(link) ||
604
- (!hasLegacyKeys && !hasOriginKeys) ||
605
- !SLUG_PATTERN.test(link.id ?? '') ||
606
- !TYPE_DIRECTORIES.has(link.type) ||
607
- typeof link.label !== 'string' ||
608
- (link.origin !== undefined &&
609
- !['floor', 'expansion'].includes(link.origin))
610
- ) {
611
- throw new TypeError(
612
- 'Artifact cross-links must use typed artifact targets.',
613
- );
614
- }
615
- }
616
- }
617
-
618
- function assertFactBase(factBase) {
619
- if (factBase === undefined) return;
620
- const validation = validateContract('fact-base', factBase);
621
- if (!validation.valid) {
622
- throw new TypeError('Renderer requires a validated fact base.');
623
- }
624
- }
625
-
626
- function assertTheme(theme) {
627
- const validation = validateContract('theme', theme);
628
- const identity = isObject(theme) ? structuredClone(theme) : {};
629
- delete identity.bundleHash;
630
- if (!validation.valid || theme.bundleHash !== canonicalHash(identity)) {
631
- throw new Error('Renderer requires a validated resolved theme.');
632
- }
633
- }
634
-
635
- function assertRenderStrategy(renderStrategy) {
636
- if (!RENDER_STRATEGIES.has(renderStrategy)) {
637
- throw new Error(`Unsupported render strategy: ${String(renderStrategy)}.`);
638
- }
639
- }
640
-
641
- function normalizePublicBaseUrl(value) {
642
- if (value === undefined) return undefined;
643
- if (typeof value !== 'string') {
644
- throw new TypeError('Public base URL must be an HTTPS URL.');
645
- }
646
- let url;
647
- try {
648
- url = new URL(value);
649
- } catch {
650
- throw new TypeError('Public base URL must be an HTTPS URL.');
651
- }
652
- if (
653
- url.protocol !== 'https:' ||
654
- url.username ||
655
- url.password ||
656
- url.search ||
657
- url.hash
658
- ) {
659
- throw new TypeError(
660
- 'Public base URL must be an HTTPS URL without credentials.',
661
- );
662
- }
663
- return url.href.replace(/\/+$/, '');
664
- }
665
-
666
- function escapeHtml(value) {
667
- return String(value)
668
- .replaceAll('&', '&amp;')
669
- .replaceAll('<', '&lt;')
670
- .replaceAll('>', '&gt;')
671
- .replaceAll('"', '&quot;')
672
- .replaceAll("'", '&#39;');
673
- }
674
-
675
- function escapeAttribute(value) {
676
- return escapeHtml(value);
677
- }
678
-
679
- function humanize(value) {
680
- return value
681
- .split('-')
682
- .map((part) => `${part[0].toUpperCase()}${part.slice(1)}`)
683
- .join(' ');
684
- }
685
-
686
- function oppositeMode(mode) {
687
- return mode === 'light' ? 'dark' : 'light';
688
- }
689
-
690
- function hasExactKeys(value, keys) {
691
- return (
692
- Object.keys(value).length === keys.length &&
693
- keys.every((key) => Object.hasOwn(value, key))
694
- );
695
- }
696
-
697
- function hasOnlyKeys(value, keys) {
698
- return Object.keys(value).every((key) => keys.includes(key));
699
- }
700
-
701
- function isObject(value) {
702
- return typeof value === 'object' && value !== null && !Array.isArray(value);
703
- }