@open-agent-toolkit/cli 0.2.72 → 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 (165) hide show
  1. package/assets/agents/oat-reviewer.md +4 -3
  2. package/assets/agents/recon-worker.md +42 -8
  3. package/assets/bundle-metadata.json +1 -1
  4. package/assets/docs/cli-utilities/configuration.md +3 -56
  5. package/assets/docs/cli-utilities/tool-packs.md +4 -4
  6. package/assets/docs/contributing/code.md +2 -2
  7. package/assets/docs/contributing/index.md +0 -1
  8. package/assets/docs/reference/cli-reference.md +1 -1
  9. package/assets/docs/reference/troubleshooting.md +17 -40
  10. package/assets/docs/workflows/projects/artifacts.md +16 -34
  11. package/assets/docs/workflows/projects/lifecycle.md +20 -19
  12. package/assets/docs/workflows/skills/explainer-kit.md +180 -440
  13. package/assets/docs/workflows/skills/index.md +1 -2
  14. package/assets/docs/workflows/skills/recon.md +114 -23
  15. package/assets/public-package-versions.json +4 -4
  16. package/assets/skills/explainer-kit/SKILL.md +96 -171
  17. package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
  18. package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
  19. package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
  20. package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
  21. package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
  22. package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
  23. package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
  24. package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
  25. package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
  26. package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
  27. package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
  28. package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
  29. package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
  30. package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
  31. package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
  32. package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
  33. package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
  34. package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
  35. package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
  36. package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
  37. package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
  38. package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
  39. package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
  40. package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
  41. package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
  42. package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
  43. package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
  44. package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
  45. package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
  46. package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
  47. package/assets/skills/oat-project-complete/SKILL.md +168 -349
  48. package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
  49. package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
  50. package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
  51. package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
  52. package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
  53. package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
  54. package/assets/skills/oat-project-document/SKILL.md +1 -1
  55. package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
  56. package/assets/skills/oat-project-implement/SKILL.md +1 -1
  57. package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
  58. package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
  59. package/assets/skills/oat-project-lite/SKILL.md +1 -1
  60. package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
  61. package/assets/skills/oat-project-plan/SKILL.md +5 -6
  62. package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
  63. package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
  64. package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
  65. package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
  66. package/assets/skills/oat-project-review-receive/SKILL.md +14 -1
  67. package/assets/skills/oat-project-summary/SKILL.md +35 -25
  68. package/assets/skills/oat-wave-execute/SKILL.md +11 -36
  69. package/assets/skills/oat-wave-program/SKILL.md +13 -37
  70. package/assets/skills/recon/SKILL.md +103 -40
  71. package/assets/skills/recon/references/packet-contract.md +150 -67
  72. package/assets/skills/recon/references/profiles.md +38 -16
  73. package/assets/skills/recon/references/worker-contract.md +39 -8
  74. package/assets/skills/recon/scripts/lib/contracts.mjs +703 -55
  75. package/assets/skills/recon/scripts/lib/routing.mjs +588 -0
  76. package/assets/skills/recon/scripts/lib/validated-run.mjs +2 -0
  77. package/assets/skills/recon/scripts/prepare-routing.mjs +88 -0
  78. package/assets/skills/recon/scripts/render-packet.mjs +69 -1
  79. package/assets/skills/recon/scripts/validate-packet.mjs +473 -56
  80. package/assets/skills/subagent-orchestration/SKILL.md +18 -9
  81. package/assets/skills/subagent-orchestration/references/model-selection-principles.md +17 -8
  82. package/dist/commands/config/index.d.ts.map +1 -1
  83. package/dist/commands/config/index.js +9 -113
  84. package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
  85. package/dist/commands/project/archive/archive-utils.js +32 -184
  86. package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
  87. package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
  88. package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
  89. package/dist/config/oat-config.d.ts +0 -11
  90. package/dist/config/oat-config.d.ts.map +1 -1
  91. package/dist/config/oat-config.js +0 -34
  92. package/dist/config/resolve.d.ts.map +1 -1
  93. package/dist/config/resolve.js +0 -8
  94. package/dist/release/public-package-contract.d.ts.map +1 -1
  95. package/dist/release/public-package-contract.js +2 -10
  96. package/dist/validation/project-state.d.ts.map +1 -1
  97. package/dist/validation/project-state.js +42 -3
  98. package/package.json +2 -2
  99. package/assets/docs/contributing/explainer-kit-verification.md +0 -133
  100. package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
  101. package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
  102. package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
  103. package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
  104. package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
  105. package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
  106. package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
  107. package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
  108. package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
  109. package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
  110. package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
  111. package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
  112. package/assets/skills/explainer-kit/references/contracts.md +0 -324
  113. package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
  114. package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
  115. package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
  116. package/assets/skills/explainer-kit/references/visual-review.md +0 -71
  117. package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
  118. package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
  119. package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
  120. package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
  121. package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
  122. package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
  123. package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
  124. package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
  125. package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
  126. package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
  127. package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
  128. package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
  129. package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
  130. package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
  131. package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
  132. package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
  133. package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
  134. package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
  135. package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
  136. package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
  137. package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
  138. package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
  139. package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
  140. package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
  141. package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
  142. package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
  143. package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
  144. package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
  145. package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
  146. package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
  147. package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
  148. package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
  149. package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
  150. package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
  151. package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
  152. package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
  153. package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
  154. package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
  155. package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
  156. package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
  157. package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
  158. package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
  159. package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
  160. package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
  161. package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
  162. package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
  163. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
  164. package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
  165. 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
- }