@stacksjs/defaults 0.75.361 → 0.75.362

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 (208) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +2 -2
  3. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  4. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  5. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  7. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  8. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  9. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  10. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  11. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  12. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  16. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  17. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  18. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  19. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  20. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  21. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  22. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  24. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  27. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  28. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  29. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  32. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  37. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  38. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  39. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  40. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  42. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  43. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  56. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  57. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  58. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  59. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  69. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  70. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  111. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  120. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  121. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  122. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  124. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  125. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  126. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  127. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  128. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  129. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  130. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  134. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  141. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  147. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  149. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  175. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  186. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  187. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  188. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  189. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  190. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  191. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  192. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  194. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  195. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  196. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  199. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  201. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  205. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  206. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  207. package/ide/vscode/package.json +1 -1
  208. package/package.json +2 -2
@@ -0,0 +1,300 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { compileExplainerDiagram } from './diagrams.mjs';
3
+
4
+ const TYPES = new Set(['architecture','workflow','sequence','dataflow','lifecycle','erd','class','tree','timeline','waterfall']);
5
+ const ID = /^[A-Za-z][A-Za-z0-9_-]*$/;
6
+ const stable = (kind, value) => `${kind}-${createHash('sha256').update(value).digest('hex').slice(0, 12)}`;
7
+ const pointerKey = value => String(value).replaceAll('~','~0').replaceAll('/','~1');
8
+ const csv = value => value ? value.split(',').map(part => part.trim()).filter(Boolean) : [];
9
+ const rowLabel = value => {
10
+ const match = value.match(/^(.*?)\s+\{#([^}\s]+)\}$/u);
11
+ return match ? {label:match[1].trim(),id:match[2]} : {label:value};
12
+ };
13
+
14
+ /** Restricted author syntax; canonical JSON uses the same model and source locations.
15
+ * Failures return document:null and repairable diagnostics; no partial document is delivered.
16
+ * Source-map start/end values are offsets in the original JS string, not byte offsets.
17
+ */
18
+ export function parseExplainer(sourceText, { inputPath, output } = {}) {
19
+ if (typeof sourceText !== 'string') throw new TypeError('Explainer input must be a string');
20
+ // The CLI owns the destination directory. Authored meta.output stays portable.
21
+ if (typeof output === 'string') output = output.split(/[\\/]/u).at(-1);
22
+ const sourceMap = Object.create(null);
23
+ const diagnostics = [];
24
+ const format = sourceText.trimStart().startsWith('{') || sourceText.trimStart().startsWith('[') ? 'json' : 'markdown';
25
+ const starts = [0];
26
+ for (let i = 0; i < sourceText.length; i++) if (sourceText[i] === '\n') starts.push(i + 1);
27
+ const lines = sourceText.split('\n').map(line => line.replace(/\r$/, ''));
28
+ function locate(start, end = start) {
29
+ let lo = 0, hi = starts.length;
30
+ while (lo + 1 < hi) { const mid = (lo + hi) >> 1; if (starts[mid] <= start) lo = mid; else hi = mid; }
31
+ return {line:lo + 1,column:start - starts[lo] + 1,start,end};
32
+ }
33
+ function map(pointer, value, descriptor) {
34
+ sourceMap[pointer] = {...descriptor};
35
+ if (Array.isArray(value)) value.forEach((entry,index) => map(`${pointer}/${index}`,entry,descriptor));
36
+ else if (value && typeof value === 'object') Object.entries(value).forEach(([key,entry]) => map(`${pointer}/${pointerKey(key)}`,entry,descriptor));
37
+ }
38
+ function fail(line, component, reason, example, path = '/') {
39
+ diagnostics.push({code:`explainer/parse-${component}`,severity:'error',message:reason,
40
+ subject:{line,column:1,component,path,...(inputPath ? {input:inputPath} : {})},
41
+ evidence:{reason,example},supportedFixes:[`Use the documented syntax, for example: ${example}`]});
42
+ }
43
+ function unsupported(text, line, component) {
44
+ if (/^\s*(?:>{1,}|#{3,}\s|(?:---|\*\*\*|___)\s*$|```|~~~|[-*+]\s+\[[ xX]\])/u.test(text)) fail(line,component,'This Markdown block is outside the restricted author grammar.','## What happens?\nA short paragraph.');
45
+ }
46
+ let document;
47
+ if (format === 'json') {
48
+ try {
49
+ document = JSON.parse(sourceText);
50
+ // Scan the already parsed JSON to preserve exact nested value locations and detect
51
+ // duplicate keys, which JSON.parse silently overwrites. No eval or JSON rewriting.
52
+ let pos = 0;
53
+ const skip = () => { while (/\s/u.test(sourceText[pos] || '') && pos < sourceText.length) pos++; };
54
+ const stringEnd = () => { const start = pos++; while (pos < sourceText.length) { const c = sourceText[pos++]; if (c === '\\') pos++; else if (c === '"') break; } return JSON.parse(sourceText.slice(start,pos)); };
55
+ function scan(pointer) {
56
+ skip(); const start = pos;
57
+ if (sourceText[pos] === '{') {
58
+ pos++; skip(); const keys = new Set();
59
+ while (sourceText[pos] !== '}') {
60
+ skip(); const keyStart = pos; const key = stringEnd();
61
+ if (keys.has(key)) fail(locate(keyStart).line,'json','Duplicate JSON keys would discard authored content.','Use each object key once.',`${pointer}/${pointerKey(key)}`);
62
+ keys.add(key); skip(); pos++; scan(`${pointer}/${pointerKey(key)}`); skip(); if (sourceText[pos] === ',') pos++; else break;
63
+ }
64
+ pos++;
65
+ } else if (sourceText[pos] === '[') {
66
+ pos++; skip(); let index = 0;
67
+ while (sourceText[pos] !== ']') { scan(`${pointer}/${index++}`); skip(); if (sourceText[pos] === ',') pos++; else break; }
68
+ pos++;
69
+ } else if (sourceText[pos] === '"') stringEnd();
70
+ else { while (pos < sourceText.length && !/[\s,\]}]/u.test(sourceText[pos])) pos++; }
71
+ sourceMap[pointer] = locate(start,pos);
72
+ }
73
+ scan('');
74
+ if (document && !Array.isArray(document) && typeof document === 'object') {
75
+ if (document.meta && typeof document.meta === 'object' && output) document.meta.output = output;
76
+ }
77
+ } catch (error) {
78
+ const match = String(error.message).match(/position (\d+)/u);
79
+ const lineMatch = String(error.message).match(/line (\d+)/u);
80
+ fail(match ? locate(Number(match[1])).line : Number(lineMatch?.[1] || 1),'json','Input is not valid JSON.','{"schema_version":1,"diagram_type":"explainer",...}');
81
+ }
82
+ } else {
83
+ document = {schema_version:1,diagram_type:'explainer',meta:{},answer:{summary:''},panels:[],sources:[]};
84
+ let panel = null, panelIndex = -1, titleSeen = false, answerSeen = false, reasonsAllowed = false, cursor = 0;
85
+ const descriptor = (line, endLine = line + 1) => locate(starts[line] ?? sourceText.length, starts[endLine] ?? sourceText.length);
86
+ const closePanel = end => { if (panelIndex >= 0) sourceMap[`/panels/${panelIndex}`].end = end; };
87
+ function heading(line, prefix) {
88
+ const match = line.slice(prefix.length).trim().match(/^(.*?)(?:\s+\{(?:#([^}\s]+))?(?:\s*span=(\d+))?\})?$/u);
89
+ return {title:match[1].trim(),id:match[2],...(match[3]?{span:Number(match[3])}:{})};
90
+ }
91
+ function addBlock(block, at, until, rows = []) {
92
+ if (!panel) { fail(at + 1,block.type,'Blocks must follow a question panel heading.','## What should I know?'); return; }
93
+ // A block named after its own panel points at the same place; keep both reachable instead of failing.
94
+ // Diagrams keep their ID because node and route links name it.
95
+ if (block.id && block.id === panel.id && block.type !== 'diagram') block.id = `${panel.id}-${block.type}`;
96
+ block.id ||= stable(block.type,`${panel.id}\0${JSON.stringify(block)}`);
97
+ if (block.type === 'comparison') for (const row of block.rows) row.id ||= stable('row', `${block.id}\0${row.label}`);
98
+ const pointer = `/panels/${panelIndex}/blocks/${panel.blocks.length}`;
99
+ panel.blocks.push(block); map(pointer,block,descriptor(at,until));
100
+ const property = {facts:'entries',comparison:'rows',steps:'items',list:'items'}[block.type];
101
+ if (property) rows.forEach((row,index) => { if (block[property]?.[index] !== undefined) map(`${pointer}/${property}/${index}`,block[property][index],descriptor(row.line,row.line+1)); });
102
+ }
103
+ function cells(raw, line) {
104
+ const trimmed = raw.trim();
105
+ let value = trimmed.startsWith('|') ? trimmed.slice(1) : trimmed;
106
+ if (value.endsWith('|') && !/(^|[^\\])(\\\\)*\\\|$/u.test(value)) value = value.slice(0,-1);
107
+ const result = []; let cell = '', code = false;
108
+ for (let i = 0; i < value.length; i++) {
109
+ const char = value[i];
110
+ if (char === '\\' && ['|','\\'].includes(value[i+1])) { cell += value[++i]; continue; }
111
+ if (char === '`') code = !code;
112
+ if (char === '|' && !code) { result.push(cell.trim()); cell = ''; } else cell += char;
113
+ }
114
+ result.push(cell.trim());
115
+ if (code) fail(line + 1,'rows','Unclosed inline code in a table row.','| label | `literal` |');
116
+ return result;
117
+ }
118
+ function attributes(raw,line) {
119
+ const attrs = Object.create(null), positional = []; let pos = 0;
120
+ while (pos < raw.length) {
121
+ while (/\s/u.test(raw[pos] || '') && pos < raw.length) pos++;
122
+ if (pos >= raw.length) break;
123
+ const token = raw.slice(pos).match(/^([^\s=]+)(=)?/u);
124
+ if (!token) { fail(line + 1,'directive','Malformed directive arguments.','@metric label="Latency" value=12 unit=ms'); break; }
125
+ const key = token[1]; pos += token[0].length;
126
+ if (!token[2]) { positional.push(key); continue; }
127
+ let value = '';
128
+ if (raw[pos] === '"' || raw[pos] === "'") {
129
+ const quote = raw[pos++]; let closed = false;
130
+ while (pos < raw.length) { const c = raw[pos++]; if (c === quote) { closed = true; break; } if (c === '\\' && [quote,'\\'].includes(raw[pos])) value += raw[pos++]; else value += c; }
131
+ if (!closed) fail(line + 1,'directive','Quoted directive value is not closed.','title="A short title"');
132
+ if (pos < raw.length && !/\s/u.test(raw[pos])) fail(line+1,'directive','Arguments must be separated by whitespace.','label="Latency" value=12');
133
+ } else { const start = pos; while (pos < raw.length && !/\s/u.test(raw[pos])) pos++; value = raw.slice(start,pos); }
134
+ if (Object.hasOwn(attrs,key)) fail(line+1,'directive','Duplicate arguments would discard authored content.','Use each key once.');
135
+ attrs[key] = value;
136
+ }
137
+ return {attrs,positional};
138
+ }
139
+ function rowWidth(rows,min,max,component) {
140
+ for (const row of rows) if (row.cells.length < min || row.cells.length > max) fail(row.line+1,component,`Expected ${min === max ? min : `${min}–${max}`} cells in this directive row.`,`| ${Array.from({length:min},(_,i)=>`field${i+1}`).join(' | ')} |`);
141
+ }
142
+ function boolean(value, at, component) {
143
+ if (value === undefined) return undefined;
144
+ if (!['true','false'].includes(value)) fail(at+1,component,'Boolean arguments require true or false.','multiple=true');
145
+ return value === 'true';
146
+ }
147
+ function number(value, at, component) {
148
+ if (value === undefined) return undefined;
149
+ if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/iu.test(value) || !Number.isFinite(Number(value))) fail(at+1,component,'Numeric arguments require a finite number.','value=12.5');
150
+ return Number(value);
151
+ }
152
+ function select(attrs, keys) { return Object.fromEntries(keys.filter(key => attrs[key] !== undefined).map(key=>[key,attrs[key]])); }
153
+ function directive(at) {
154
+ const match = lines[at].trim().match(/^@([a-z][a-z-]*)(?:\s+(.*))?$/u);
155
+ if (!match) { fail(at+1,'directive','Malformed type directive.','@facts\n| key | value |'); cursor++; return; }
156
+ let type = match[1]; const {attrs:a,positional} = attributes(match[2] || '',at);
157
+ if (TYPES.has(type)) { positional.unshift(type); type='diagram'; }
158
+ cursor = at + 1; const rows=[]; let literal;
159
+ let literalStart;
160
+ if (type === 'diff' && /^```diff\s*$/u.test(lines[cursor]?.trim() || '')) {
161
+ cursor++; const begin=cursor; literalStart=begin; while (cursor<lines.length && lines[cursor].trim()!=='```') cursor++;
162
+ if (cursor===lines.length) fail(at+1,'diff','The diff fence is not closed.','@diff\n```diff\n-old\n+new\n```');
163
+ const end=starts[cursor] ?? sourceText.length;
164
+ literal=sourceText.slice(starts[begin] ?? sourceText.length,end).replace(/\r?\n$/u,''); if (cursor<lines.length) cursor++;
165
+ } else {
166
+ // A directive row never starts with an empty cell; "|| id |" is a line-number prefix copied from a file viewer.
167
+ while (cursor<lines.length && lines[cursor].trim() && !/^(?:#{1,2}\s|@)/u.test(lines[cursor].trim())) { rows.push({cells:cells(lines[cursor].replace(/^(\s*)\|\|(?=\s)/u,'$1|'),cursor),line:cursor}); cursor++; }
168
+ // A callout with no rows takes the plain paragraph that follows it as its text.
169
+ if (type==='callout' && !rows.length && a.text===undefined) {
170
+ let next=cursor; while (next<lines.length && !lines[next].trim()) next++;
171
+ const paragraph=[]; while (next<lines.length && lines[next].trim() && !/^(?:#{1,2}\s|@|\||[-*+]\s|\d+\.\s|```|~~~)/u.test(lines[next].trim())) { paragraph.push({cells:[lines[next].trim()],line:next}); next++; }
172
+ if (paragraph.length) { rows.push(...paragraph); cursor=next; }
173
+ }
174
+ }
175
+ const allowed = {
176
+ source:['title','url','path','lines','revision'],meta:['locale','quality_profile','style','repo_url','repo_revision','repo_provider','repo_link_mode'],answer:['scope'],
177
+ prose:['id','text'],list:['id','ordered'],callout:['id','level','title','text'],facts:['id'],metric:['id','label','value','limit','unit','note','typical'],steps:['id'],diagram:['id','source','spec','title','changes'],code:['id','source','language'],diff:['id','caption'],image:['id','path','alt','caption'],comparison:['id','columns','label'],chart:['id','title','type','data','x','y','unit']
178
+ }[type];
179
+ if (!allowed) { fail(at+1,'directive',`Unsupported component @${type}.`,'@facts\n| key | value |'); return; }
180
+ for (const key of Object.keys(a)) if (!allowed.includes(key)) fail(at+1,type,`Unsupported argument ${key}.`,`@${type} ${allowed.map(key=>`${key}=...`).join(' ')}`);
181
+ if (!['source','diagram'].includes(type) && positional.length) fail(at+1,type,'This directive accepts key=value arguments only.',`@${type} id=stable-id`);
182
+ if (['meta','answer'].includes(type) && rows.length) fail(rows[0].line+1,type,'This directive accepts header arguments only.','@meta locale=en quality_profile=showcase');
183
+ if (type==='source') {
184
+ if (positional.length!==1) fail(at+1,type,'Source declarations require an ID.','@source docs title="Documentation" url=https://example.com/docs');
185
+ const source={id:positional[0],...select(a,['title','url','path','revision'])};
186
+ if (a.lines!==undefined) { const range=a.lines.match(/^(\d+):(\d+)$/u); if (!range) fail(at+1,type,'Repository lines require start:end.','lines=10:20'); source.lines=range?[Number(range[1]),Number(range[2])]:[]; }
187
+ if(rows.length) fail(at+1,type,'Source declarations do not accept rows.','@source demo title="Timing model"');
188
+ const pointer=`/sources/${document.sources.length}`;document.sources.push(source);map(pointer,source,descriptor(at,cursor));return;
189
+ }
190
+ if (type==='meta') { Object.assign(document.meta,select(a,['locale','quality_profile','style'])); if(a.repo_url || a.repo_revision) document.meta.repository={url:a.repo_url,revision:a.repo_revision,...(a.repo_provider?{provider:a.repo_provider}:{}),...(a.repo_link_mode?{link_mode:a.repo_link_mode}:{})}; for(const key of ['locale','quality_profile','style','repository'])if(document.meta[key]!==undefined)map(`/meta/${key}`,document.meta[key],descriptor(at,cursor));return; }
191
+ if (type==='answer') { if(a.scope!==undefined){document.answer.scope=a.scope;sourceMap['/answer/scope']=descriptor(at,cursor);}return; }
192
+ let block={type,...(a.id?{id:a.id}:{})};
193
+ switch(type) {
194
+ case 'prose': rowWidth(rows,1,1,type);block.text=a.text??rows.map(r=>r.cells[0]).join('\n');break;
195
+ case 'list': rowWidth(rows,1,1,type);block.items=rows.map(r=>r.cells[0]);if(a.ordered!==undefined)block.ordered=boolean(a.ordered,at,type);break;
196
+ case 'callout': rowWidth(rows,1,1,type);Object.assign(block,{level:a.level||'note',...select(a,['title']),text:a.text??rows.map(r=>r.cells[0]).join('\n')});break;
197
+ case 'facts':rowWidth(rows,2,2,type);block.entries=rows.map(r=>({key:r.cells[0],value:r.cells[1]}));break;
198
+ case 'metric':Object.assign(block,select(a,['label','unit','note']));for(const key of ['value','limit'])if(a[key]!==undefined)block[key]=number(a[key],at,type);if(a.typical!==undefined)block.typical=boolean(a.typical,at,type);break;
199
+ case 'steps':rowWidth(rows,2,7,type);block.items=rows.map(r=>{let [id,text,detail,diagram,node,route,edges]=r.cells;if(node?.includes('~')&&!route){route=node;node='';}const target=diagram?{diagram,...(node?{node}:{}),...(route?{route:route.split('~')}:{}),...(edges?{edges:csv(edges)}:{})}:undefined;if(!diagram&&(node||route||edges))fail(r.line+1,type,'A step node, route or edge reference needs its diagram ID.','| step | Read cache | | cache-flow | redis | | |');return{id,text,...(detail?{detail}:{}),...(target?{target}:{})};});break;
200
+ case 'diagram': {
201
+ if(positional.length!==1)fail(at+1,type,'A diagram requires exactly one native type.','@diagram sequence source=cache.sequence.json');
202
+ block.diagramType=positional[0];
203
+ if(a.source!==undefined)block.source=a.source;
204
+ if(a.spec!==undefined) { try{block.spec=JSON.parse(a.spec);}catch{fail(at+1,type,'Inline diagram spec must be valid JSON.','@diagram sequence spec=\'{"schema_version":1,...}\'');} }
205
+ if(rows.length&&(a.spec!==undefined||a.source!==undefined))fail(at+1,type,'Choose shorthand rows, spec or source, not several.','@diagram sequence source=cache.sequence.json');
206
+ // A "source:<id>" cell ties that row's node to a declared source; the reader shows it on hover.
207
+ for(const r of rows){const index=r.cells.findIndex(cell=>/^source:[A-Za-z][\w-]*$/u.test(cell));if(index<0)continue;const node=r.cells[0].replace(/^(?:actor|entity)\s+/u,'').trim();if(index>0&&!r.cells[0].includes('>'))(block.nodeSources||={})[node]=r.cells[index].slice(7);r.cells=r.cells.filter((_,i)=>i!==index);}
208
+ if(rows.length&&a.spec===undefined&&a.source===undefined)try{block.spec=compileExplainerDiagram(block.diagramType,rows.map(r=>({cells:r.cells,line:r.line+1})),{id:a.id||stable('diagram',`${panel?.id}\0${JSON.stringify(rows.map(r=>r.cells))}`),title:a.title||panel?.title||document.meta.title,line:at+1,quality:document.meta.quality_profile});}catch(error){const found=error.archifyDiagnostics||error.diagnostics;if(found?.length)diagnostics.push(...found);else fail(at+1,type,'The diagram shorthand could not compile.','@diagram sequence source=cache.sequence.json');}
209
+ if(a.changes!==undefined)block.changes=csv(a.changes).map(value=>{const [target,change]=value.split(':');return{target,change};});break;
210
+ }
211
+ case 'code':Object.assign(block,select(a,['source','language']));break;
212
+ case 'diff':block.text=literal??rows.map(r=>r.cells.join(' | ')).join('\n');Object.assign(block,select(a,['caption']));break;
213
+ case 'image':Object.assign(block,select(a,['path','alt','caption']));break;
214
+ case 'chart':{if(!a.data||!a.x||!a.y)fail(at+1,type,'A data chart names its file, label column and value columns.','@chart type=bar data=latency.csv x=day y=p50,p99');if(a.type!==undefined&&!['bar','line','pie'].includes(a.type))fail(at+1,type,'A data chart is a bar, line or pie chart.','@chart type=line data=latency.csv x=day y=p99');Object.assign(block,select(a,['title']),{data:{path:a.data,kind:a.type||'bar',x:a.x,y:csv(a.y),...(a.unit?{unit:a.unit}:{})}});break;}
215
+ case 'comparison':rowWidth(rows,2,500,type);if(a.label)block.labelColumn=a.label;block.columns=csv(a.columns);block.rows=rows.map(r=>({...rowLabel(r.cells[0]),cells:r.cells.slice(1).map(assessment)}));break;
216
+ }
217
+ if (['metric','code','image','chart'].includes(type)&&rows.length)fail(at+1,type,'This component accepts header arguments only.',`@${type} ${allowed.filter(k=>k!=='id').map(k=>`${k}=...`).join(' ')}`);
218
+ if(type!=='diff')for(const row of rows)for(const text of row.cells)unsupported(text,row.line+1,type);
219
+ addBlock(block,at,cursor,rows);
220
+ if(type==='diff'&&panel) {
221
+ const pointer=`/panels/${panelIndex}/blocks/${panel.blocks.length-1}/text`;
222
+ sourceMap[pointer]=literalStart!==undefined?locate(starts[literalStart]??sourceText.length,(starts[literalStart]??sourceText.length)+literal.length):descriptor(rows[0]?.line??at,cursor);
223
+ }
224
+ }
225
+ function assessment(text) {
226
+ // Citations may follow the assessment; they stay with the cell text.
227
+ const match = text.match(/^(.*?)\s*\{(good|caution|bad)(?::([^}]+))?\}((?:\s*\[[^\]]*\]\([^)\s]*\))*)\s*$/u);
228
+ // A cell holding only a badge ({bad:Drop}) shows the badge; its label doubles as the cell text.
229
+ return match?{text:`${match[1].trim()}${match[4].trim()}`||match[3]?.trim()||'',assessment:match[2],...(match[3]?{verdict:match[3].trim()}:{})}:{text};
230
+ }
231
+ while(cursor<lines.length) {
232
+ const at=cursor, trimmed=lines[at].trim();
233
+ if(!trimmed){cursor++;continue;}
234
+ if(trimmed.startsWith('# ')) {
235
+ if(titleSeen||panel)fail(at+1,'heading','Use one document title before all panels.','# What should we choose? {#choice}');
236
+ const h=heading(trimmed,'# ');document.meta.title=h.title;document.meta.id=h.id||stable('explainer',h.title);titleSeen=true;map('/meta',document.meta,descriptor(at));cursor++;continue;
237
+ }
238
+ if(trimmed.startsWith('## ')) {
239
+ closePanel(starts[at]);const h=heading(trimmed,'## ');panel={id:h.id||stable('panel',h.title),title:h.title,...(h.span!==undefined?{span:h.span}:{}),blocks:[]};panelIndex=document.panels.length;document.panels.push(panel);map(`/panels/${panelIndex}`,panel,descriptor(at,lines.length));cursor++;continue;
240
+ }
241
+ if(trimmed.startsWith('@')){reasonsAllowed=false;directive(at);continue;}
242
+ const fence=trimmed.match(/^(```+|~~~+)\s*([A-Za-z0-9_+.#-]*)\s*(.*)$/u);
243
+ if(fence) {
244
+ reasonsAllowed=false;
245
+ const {attrs:fa,positional:fp}=attributes(fence[3]||'',at);
246
+ if(fp.length||Object.keys(fa).some(key=>key!=='title'))fail(at+1,'snippet','A code fence accepts a language and an optional title only.','```bash title="Run the tests"');
247
+ cursor=at+1;const begin=cursor;while(cursor<lines.length&&lines[cursor].trim()!==fence[1])cursor++;
248
+ if(cursor===lines.length)fail(at+1,'snippet','The code fence is not closed.','```bash\nnpm test\n```');
249
+ const from=starts[begin]??sourceText.length,literal=sourceText.slice(from,starts[cursor]??sourceText.length).replace(/\r?\n$/u,'');
250
+ if(cursor<lines.length)cursor++;
251
+ if(!literal.trim()){fail(at+1,'snippet','A code fence needs content.','```bash\nnpm test\n```');continue;}
252
+ const language=fence[2];
253
+ addBlock(language==='diff'?{type:'diff',text:literal,...(fa.title?{caption:fa.title}:{})}:language==='mermaid'?{type:'chart',...(fa.title?{title:fa.title}:{}),text:literal}:{type:'snippet',...(language?{language}:{}),...(fa.title?{title:fa.title}:{}),text:literal},at,cursor);
254
+ if(panel)sourceMap[`/panels/${panelIndex}/blocks/${panel.blocks.length-1}/text`]=locate(from,from+literal.length);
255
+ continue;
256
+ }
257
+ if(trimmed.startsWith('|')) {
258
+ const rows=[];while(cursor<lines.length&&lines[cursor].trim().startsWith('|')){rows.push({cells:cells(lines[cursor],cursor),line:cursor});cursor++;}
259
+ if(rows.length<3||!rows[1].cells.every(cell=>/^:?-{3,}:?$/u.test(cell)))fail(at+1,'comparison','A Markdown table needs a header, separator and data rows.','| Criterion | A | B |\n| --- | --- | --- |\n| Cost | Low {good} | High {bad} |');
260
+ const columns=rows[0]?.cells.slice(1)||[];const data=rows.slice(2);for(const r of data)if(r.cells.length!==columns.length+1)fail(r.line+1,'comparison','All table rows must match the header width.','| Cost | Low {good} | High {bad} |');
261
+ addBlock({type:'comparison',...(rows[0]?.cells[0]?{labelColumn:rows[0].cells[0]}:{}),columns,rows:data.map(r=>({...rowLabel(r.cells[0]),cells:r.cells.slice(1).map(assessment)}))},at,cursor,data);continue;
262
+ }
263
+ const list=trimmed.match(/^(?:([-*+])|(\d+)\.)\s+(.+)$/u);
264
+ if(list) {
265
+ const rows=[];const ordered=Boolean(list[2]);while(cursor<lines.length){const found=lines[cursor].trim().match(/^(?:([-*+])|(\d+)\.)\s+(.+)$/u);if(!found||Boolean(found[2])!==ordered)break;if(/^\s{2,}/u.test(lines[cursor]))fail(cursor+1,'list','Nested lists are unsupported.','- One flat item');unsupported(found[3],cursor+1,'list');rows.push({cells:[found[3]],line:cursor});cursor++;}
266
+ if(!panel){if(!reasonsAllowed||ordered||document.answer.reasons)fail(at+1,'answer','Use one flat unordered reason list after the conclusion.','A conclusion.\n\n- First reason\n- Second reason');document.answer.reasons=rows.map(r=>r.cells[0]);map('/answer/reasons',document.answer.reasons,descriptor(at,cursor));rows.forEach((r,i)=>map(`/answer/reasons/${i}`,r.cells[0],descriptor(r.line)));}else{const task=/^\[[ xX]\]\s+/u,tasks=rows.filter(r=>task.test(r.cells[0])).length;if(tasks&&(ordered||tasks<rows.length))fail(at+1,'list','Write every checklist item as "- [ ] item" in one unordered list.','- [ ] Check the deposit terms\n- [ ] Photograph the meter readings');addBlock({type:'list',ordered,...(tasks?{checklist:true}:{}),items:rows.map(r=>tasks?r.cells[0].replace(task,''):r.cells[0])},at,cursor,rows);}continue;
267
+ }
268
+ const chunks=[];while(cursor<lines.length&&lines[cursor].trim()&&!/^(?:#{1,2}\s|@|\||[-*+]\s|\d+\.\s|```|~~~)/u.test(lines[cursor].trim())){unsupported(lines[cursor],cursor+1,'prose');chunks.push(lines[cursor].trim());cursor++;}
269
+ if(cursor===at){unsupported(trimmed,at+1,'prose');cursor++;continue;}
270
+ const text=chunks.join('\n');
271
+ if(!panel) {
272
+ if(!titleSeen)fail(at+1,'answer','The document title must precede the short answer.','# A title\n\nA direct answer.');
273
+ if(answerSeen)fail(at+1,'answer','The short answer is one paragraph before the panels.','A direct answer in one paragraph.\n\n## Why?');
274
+ document.answer.summary=text;answerSeen=true;reasonsAllowed=true;map('/answer',document.answer,descriptor(at,cursor));
275
+ } else addBlock({type:'prose',text},at,cursor);
276
+ }
277
+ closePanel(sourceText.length);
278
+ if(!titleSeen)fail(1,'title','A document requires one # title.','# What should we choose?');
279
+ if(!answerSeen)fail(1,'answer','A short answer is required before question panels.','# A title\n\nA direct answer.\n\n## Why?');
280
+ if(!document.panels.length)fail(1,'panel','At least one question panel is required.','## Why does this happen?');
281
+ if(output)document.meta.output=output;
282
+ }
283
+ const identities=new Map();
284
+ function identity(value,pointer,component) {
285
+ if(typeof value!=='string')return;
286
+ const line=sourceMap[pointer]?.line||sourceMap[pointer.replace(/\/id$/u,'')]?.line||1;
287
+ if(!ID.test(value)||value.length>100)fail(line,component,'IDs must begin with an ASCII letter and contain letters, numbers, hyphens or underscores.','id=cache-flow',pointer);
288
+ const previous=identities.get(value);
289
+ if(previous)fail(line,component,`Duplicate ID "${value}": the ${previous.component} on line ${previous.line} already uses it. Panels, blocks, diagrams and steps share one ID namespace.`,component==='panel'?`## Another question? {#${value}-2}`:`omit id=, or use id=${value}-${component}`,pointer);
290
+ identities.set(value,{pointer,component,line});
291
+ }
292
+ if(document&&typeof document==='object'&&!Array.isArray(document)) {
293
+ identity(document.meta?.id,'/meta/id','document');
294
+ for(const [pi,p]of (Array.isArray(document.panels)?document.panels:[]).entries()) {
295
+ identity(p?.id,`/panels/${pi}/id`,'panel');
296
+ for(const [bi,b]of (Array.isArray(p?.blocks)?p.blocks:[]).entries())identity(b?.id,`/panels/${pi}/blocks/${bi}/id`,b?.type||'block');
297
+ }
298
+ }
299
+ return {document:diagnostics.some(d=>d.severity==='error')?null:document,sourceMap,format,sourceText,diagnostics};
300
+ }
@@ -0,0 +1,117 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { parseValidatedExplainer, rebindPreparedBundle, validatePreparedTargets } from './prepare.mjs';
5
+ import { resolveExplainerSources } from './sources.mjs';
6
+ import { prepareExplainerDiagrams } from './diagrams.mjs';
7
+ import { computeExplainerRevision } from './revision.mjs';
8
+ import { unpackPageBundle } from './page-bundle.mjs';
9
+ import { throwDiagnosticError } from '../renderers/shared/diagnostics.mjs';
10
+
11
+ function fail(reason, message, input, panelId, example) {
12
+ throwDiagnosticError(message, [{code:`explainer/patch-${reason}`,severity:'error',message,
13
+ subject:{input,panelId,line:1,component:'panel'},evidence:{reason,example},supportedFixes:[example]}]);
14
+ }
15
+ function embedded(html, id, input) {
16
+ const matches = [...html.matchAll(new RegExp(`<script\\b[^>]*\\bid=["']${id}["'][^>]*>([\\s\\S]*?)<\\/script>`, 'gi'))];
17
+ if(matches.length !== 1) fail('payload','The artifact must contain one original draft and prepared resource payload.',input,undefined,'Use an artifact produced by the formal explainer CLI.');
18
+ try { return JSON.parse(matches[0][1]); } catch { fail('payload','Embedded draft payload is invalid.',input,undefined,'Rebuild the artifact from its original draft.'); }
19
+ }
20
+ /** Replace an authored stable panel while retaining immutable resources outside that panel. */
21
+ export async function preparePanelPatch({artifactPath,panelId,replacementPath,repoRoot,quality,writingMode,expectedArtifact}) {
22
+ const artifactBytes=fs.readFileSync(artifactPath);
23
+ if(expectedArtifact && (artifactBytes.length!==expectedArtifact.bytes || createHash('sha256').update(artifactBytes).digest('hex')!==expectedArtifact.sha256)) fail('artifact-changed','The original artifact changed after validation.',artifactPath,panelId,'Finish other writes to the artifact and retry the panel patch.');
24
+ const html = artifactBytes.toString('utf8');
25
+ let previous;
26
+ try { previous = unpackPageBundle(embedded(html,'archify-explainer-prepared',artifactPath)); } catch (error) { if (error.diagnostics) throw error; fail('payload','Embedded native diagram payload cannot be inflated.',artifactPath,panelId,'Rebuild the original artifact before patching.'); }
27
+ // The page names local files relative to itself; resolve them before reading or re-preparing.
28
+ const near = value => typeof value === 'string' ? path.resolve(path.dirname(artifactPath), value) : value;
29
+ const draft = {...previous,sourcePath:near(previous.sourcePath)};
30
+ if(typeof draft.sourceText !== 'string' || draft.sourceText !== previous.sourceText
31
+ || createHash('sha256').update(draft.sourceText).digest('hex') !== previous.source?.sha256
32
+ || createHash('sha256').update(JSON.stringify({...previous,digest:undefined})).digest('hex') !== previous.digest) {
33
+ fail('payload','The embedded original draft is not bound to the prepared resources.',artifactPath,panelId,'Rebuild the original artifact before patching.');
34
+ }
35
+ const oldParsed = parseValidatedExplainer(draft.sourceText,{inputPath:draft.sourcePath || artifactPath,writingMode:'off'});
36
+ if(JSON.stringify(oldParsed.document)!==JSON.stringify(previous.document)) fail('payload','The original draft and normalized document disagree.',artifactPath,panelId,'Rebuild the original artifact before patching.');
37
+ const index = oldParsed.document.panels.findIndex(panel=>panel.id===panelId);
38
+ if(index<0) fail('target','The requested panel does not exist.',artifactPath,panelId,'Choose a stable panel id from the original artifact.');
39
+ const range=oldParsed.sourceMap[`/panels/${index}`];
40
+ if(!Number.isInteger(range?.start)||!Number.isInteger(range?.end)) fail('range','The original draft has no replaceable panel range.',artifactPath,panelId,'Rebuild the artifact using the current parser.');
41
+ const replacementBytes = fs.readFileSync(replacementPath);
42
+ const replacement = replacementBytes.toString('utf8');
43
+ if(!Buffer.from(replacement).equals(replacementBytes)) fail('source-encoding','Panel replacement must contain valid UTF-8 bytes.',replacementPath,panelId,'Save the replacement panel as UTF-8 text.');
44
+ const sourceText = draft.sourceText.slice(0,range.start)+replacement+draft.sourceText.slice(range.end);
45
+ const parsed=parseValidatedExplainer(sourceText,{inputPath:draft.sourcePath || artifactPath,writingMode:writingMode || previous.writingMode || 'warn'});
46
+ if(parsed.document.panels.length!==oldParsed.document.panels.length || parsed.document.panels[index]?.id!==panelId) {
47
+ fail('identity','A panel replacement must retain the target stable id and panel count.',replacementPath,panelId,oldParsed.format==='json'?'{"id":"'+panelId+'","title":"Question?","blocks":[...]}':'## Question? {#'+panelId+'}\n\nReplacement content.');
48
+ }
49
+ const expected=structuredClone(oldParsed.document);
50
+ expected.panels[index]=parsed.document.panels[index];
51
+ if(JSON.stringify(expected)!==JSON.stringify(parsed.document)) fail('scope','The replacement changes content outside the selected panel.',replacementPath,panelId,'Replace only the target panel; preserve document metadata, sources and other panels.');
52
+ const oldBlocks=new Map(oldParsed.document.panels[index].blocks.map(block=>[block.id,block]));
53
+ const changed=parsed.document.panels[index].blocks.filter(block=>JSON.stringify(oldBlocks.get(block.id))!==JSON.stringify(block));
54
+ // Already verified immutable code excerpts remain usable after the original checkout disappears.
55
+ const changedCodeIds=new Set(changed.filter(block=>block.type==='code' && typeof previous.preparedSources?.sources?.[block.source]?.text !== 'string').map(block=>block.source));
56
+ const selectedQuality=quality || previous.quality || 'standard';
57
+ const qualityChanged=selectedQuality !== previous.quality;
58
+ const oldDiagramBlocks=new Map(oldParsed.document.panels.flatMap(panel=>panel.blocks).filter(block=>block.type==='diagram').map(block=>[block.id,block]));
59
+ const reusedDiagramSources=new Set();
60
+ const frozenDiagramInput=block=>{
61
+ if(block.type==='diagram' && block.source && oldDiagramBlocks.get(block.id)?.source===block.source && previous.preparedDiagrams?.diagrams?.[block.id]?.spec) {
62
+ reusedDiagramSources.add(block.id);
63
+ return {...block,source:undefined,spec:previous.preparedDiagrams.diagrams[block.id].spec};
64
+ }
65
+ return block;
66
+ };
67
+ const partial={...parsed.document,panels:[{...parsed.document.panels[index],blocks:changed.filter(block=>
68
+ (block.type!=='code'||changedCodeIds.has(block.source))
69
+ && !(block.type==='image' && oldBlocks.get(block.id)?.path===block.path && previous.preparedSources?.assets?.[block.id])
70
+ && !(block.type==='chart' && block.data && JSON.stringify(oldBlocks.get(block.id)?.data)===JSON.stringify(block.data) && previous.preparedSources?.charts?.[block.id]))
71
+ .map(frozenDiagramInput)}],sources:parsed.document.sources.filter(source=>changedCodeIds.has(source.id))};
72
+ const common={repoRoot,baseDir:previous.baseDir===null ? null : near(previous.baseDir) || path.dirname(draft.sourcePath || artifactPath),sourceMap:parsed.sourceMap,sourcePath:draft.sourcePath || artifactPath,quality:selectedQuality};
73
+ const partialMap={...parsed.sourceMap};
74
+ for(const [partialIndex,block] of partial.panels[0].blocks.entries()) {
75
+ const originalIndex=parsed.document.panels[index].blocks.findIndex(entry=>entry.id===block.id);
76
+ const originalPrefix=`/panels/${index}/blocks/${originalIndex}`;
77
+ for(const [pointer,location] of Object.entries(parsed.sourceMap)) if(pointer===originalPrefix || pointer.startsWith(`${originalPrefix}/`)) partialMap[`/panels/0/blocks/${partialIndex}${pointer.slice(originalPrefix.length)}`]=location;
78
+ }
79
+ for(const [partialIndex,source] of partial.sources.entries()) {
80
+ const originalIndex=parsed.document.sources.findIndex(entry=>entry.id===source.id);
81
+ const originalPrefix=`/sources/${originalIndex}`;
82
+ for(const [pointer,location] of Object.entries(parsed.sourceMap)) if(pointer===originalPrefix || pointer.startsWith(`${originalPrefix}/`)) partialMap[`/sources/${partialIndex}${pointer.slice(originalPrefix.length)}`]=location;
83
+ }
84
+ const newSources=await resolveExplainerSources(partial,{...common,sourceMap:partialMap});
85
+ // A profile change needs fresh native validation for every child, including
86
+ // untouched panels. Frozen original specs keep this independent of old files.
87
+ const diagramDocument=qualityChanged ? {...parsed.document,panels:parsed.document.panels.map(panel=>({...panel,blocks:panel.blocks.map(frozenDiagramInput)}))} : partial;
88
+ const newDiagrams=await prepareExplainerDiagrams(diagramDocument,{...common,sourceMap:qualityChanged ? parsed.sourceMap : partialMap});
89
+ newDiagrams.dependencies=newDiagrams.dependencies.map(entry=>{
90
+ if(!reusedDiagramSources.has(entry.id)) return entry;
91
+ const original=(previous.dependencies || []).find(dep=>dep.kind==='diagram-spec' && dep.id===entry.id);
92
+ if(!original) fail('payload','The original diagram has no frozen specification evidence.',artifactPath,panelId,'Rebuild the original artifact before patching.');
93
+ newDiagrams.diagrams[entry.id].specification=previous.preparedDiagrams.diagrams[entry.id].specification;
94
+ return {...original,path:near(original.path),retained:true};
95
+ });
96
+ const preparedSources={...previous.preparedSources,sources:{...previous.preparedSources.sources,...newSources.sources},assets:{...previous.preparedSources.assets,...newSources.assets},charts:{...previous.preparedSources.charts,...newSources.charts}};
97
+ const preparedDiagrams={...previous.preparedDiagrams,diagrams:{...previous.preparedDiagrams.diagrams,...newDiagrams.diagrams}};
98
+ const activeBlocks=parsed.document.panels.flatMap(panel=>panel.blocks);
99
+ const imageIds=new Set(activeBlocks.filter(block=>block.type==='image').map(block=>block.id));
100
+ const diagramIds=new Set(activeBlocks.filter(block=>block.type==='diagram').map(block=>block.id));
101
+ const dataChartIds=new Set(activeBlocks.filter(block=>block.type==='chart'&&block.data).map(block=>block.id));
102
+ preparedSources.assets=Object.fromEntries(Object.entries(preparedSources.assets).filter(([id])=>imageIds.has(id)));
103
+ preparedSources.charts=Object.fromEntries(Object.entries(preparedSources.charts).filter(([id])=>dataChartIds.has(id)));
104
+ if(!Object.keys(preparedSources.charts).length)delete preparedSources.charts;
105
+ preparedDiagrams.diagrams=Object.fromEntries(Object.entries(preparedDiagrams.diagrams).filter(([id])=>diagramIds.has(id)));
106
+ validatePreparedTargets(parsed.document,preparedDiagrams.diagrams,common);
107
+ const dependencyKey=entry=>`${entry.kind}:${entry.id}`;
108
+ const newDependencies=[...(newSources.dependencies||[]),...(newDiagrams.dependencies||[])];
109
+ const preparedIds=new Set(newDependencies.map(dependencyKey));
110
+ const localDependency=(entry,baseDir)=>({...entry,...(entry.kind==='repository'||entry.inline||!entry.path?{}:{path:path.resolve(baseDir,entry.path)})});
111
+ const dependencies=[...(previous.dependencies||[]).filter(entry=>!preparedIds.has(dependencyKey(entry)) && (entry.kind!=='image'||imageIds.has(entry.id)) && (entry.kind!=='data'||dataChartIds.has(entry.id)) && (entry.kind!=='diagram-spec'||diagramIds.has(entry.id))).map(entry=>localDependency(entry,path.dirname(artifactPath))),
112
+ ...newDependencies.map(entry=>localDependency(entry,common.baseDir))];
113
+ // Old dependencies are embedded immutable bytes. Only newly prepared resources are reread/verified.
114
+ const bundle=rebindPreparedBundle({...previous,sourcePath:common.sourcePath,baseDir:common.baseDir,preparedSources,preparedDiagrams,dependencies,quality:selectedQuality,writingMode:writingMode || previous.writingMode || 'warn'},parsed);
115
+ bundle.dependencies = dependencies.map(entry => ({...entry, retained: entry.retained===true || !preparedIds.has(dependencyKey(entry))}));
116
+ return {bundle:rebindPreparedBundle(bundle,parsed),sourceText,sourcePath:draft.sourcePath || artifactPath,previousRevision:computeExplainerRevision(previous),previousSourceSha256:previous.source.sha256,panelId};
117
+ }
@@ -0,0 +1,131 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { parseExplainer } from './parse.mjs';
5
+ import { validateExplainer } from './validate.mjs';
6
+ import { resolveExplainerSources } from './sources.mjs';
7
+ import { parseInline } from './inline.mjs';
8
+ import { prepareExplainerDiagrams, validateExplainerDiagramTarget } from './diagrams.mjs';
9
+ import { throwDiagnosticError } from '../renderers/shared/diagnostics.mjs';
10
+
11
+ const MAX_BUNDLE_BYTES = 64 * 1024 * 1024;
12
+ const MAX_SOURCE_BYTES = 16 * 1024 * 1024;
13
+ const identity = bytes => ({ sha256: createHash('sha256').update(bytes).digest('hex'), bytes: Buffer.byteLength(bytes) });
14
+ const digest = bundle => identity(JSON.stringify({ ...bundle, digest: undefined })).sha256;
15
+ function fail(code, message, input, reason, example = 'Retry from the unchanged original draft and local resources.') {
16
+ throwDiagnosticError(message, [{ code: `explainer/${code}`, severity: 'error', message,
17
+ subject: { input, line: 1, component: 'document' }, evidence: { reason, example }, supportedFixes: [example] }]);
18
+ }
19
+ export function parseValidatedExplainer(sourceText, options = {}) {
20
+ if (Buffer.byteLength(sourceText) > MAX_SOURCE_BYTES) fail('source-limit', 'Explainer draft exceeds the 16 MiB source budget.', options.inputPath, 'source-byte-limit', 'Reduce the draft below 16 MiB and use relative native diagram sources for large specifications.');
21
+ const parsed = parseExplainer(sourceText, { inputPath: options.inputPath, output: options.output });
22
+ if (!parsed.document || parsed.diagnostics.some(entry => entry.severity !== 'warning')) {
23
+ throwDiagnosticError('Explainer draft could not be parsed.', parsed.diagnostics);
24
+ }
25
+ const validated = validateExplainer(parsed.document, { sourceMap: parsed.sourceMap, inputPath: options.inputPath, writingMode: options.writingMode });
26
+ if (!validated.ok) throwDiagnosticError('Explainer draft is invalid.', validated.diagnostics);
27
+ return { ...parsed, diagnostics: [...parsed.diagnostics, ...validated.diagnostics] };
28
+ }
29
+ export async function prepareExplainer(sourceText, options = {}) {
30
+ const inputPath = path.resolve(options.inputPath);
31
+ const parsed = parseValidatedExplainer(sourceText, { ...options, inputPath });
32
+ const baseDir = options.baseDir || path.dirname(inputPath);
33
+ const quality = options.quality || parsed.document.meta?.quality_profile || 'standard';
34
+ const common = { repoRoot: options.repoRoot, baseDir, sourceMap: parsed.sourceMap, sourcePath: inputPath, quality };
35
+ const preparedSources = await resolveExplainerSources(parsed.document, common);
36
+ const preparedDiagrams = await prepareExplainerDiagrams(parsed.document, common);
37
+ validatePreparedTargets(parsed.document, preparedDiagrams.diagrams, common);
38
+ const dependencies = [...(preparedSources.dependencies || []), ...(preparedDiagrams.dependencies || [])].map(entry => ({ ...entry, id: entry.id || entry.blockId,
39
+ ...(entry.kind === 'repository' || entry.inline ? {} : { path: path.resolve(baseDir, entry.path) }) }));
40
+ const bundle = { schemaVersion: 1, kind: 'archify-explainer-prepared', ...parsed, sourcePath: inputPath, baseDir,
41
+ quality, writingMode: options.writingMode || 'warn', source: identity(Buffer.from(sourceText)),
42
+ preparedSources: { sources: preparedSources.sources, assets: preparedSources.assets, ...(Object.keys(preparedSources.charts || {}).length ? { charts: preparedSources.charts } : {}), ...(preparedSources.repository ? { repository: preparedSources.repository } : {}) },
43
+ preparedDiagrams: { diagrams: preparedDiagrams.diagrams }, dependencies };
44
+ bundle.digest = digest(bundle);
45
+ assertPreparedDependencies(bundle);
46
+ return bundle;
47
+ }
48
+ export function assertPreparedDependencies(bundle) {
49
+ for (const dependency of bundle.dependencies || []) {
50
+ // Repository excerpts were read from the pinned Git object, never the mutable checkout.
51
+ if (dependency.kind === 'repository' || dependency.inline || dependency.retained) {
52
+ if (typeof dependency.contentBase64 === 'string') {
53
+ const frozen = identity(Buffer.from(dependency.contentBase64, 'base64'));
54
+ if (frozen.sha256 !== dependency.sha256 || frozen.bytes !== dependency.bytes) fail('prepared-mismatch', 'Frozen resource bytes do not match their identity.', bundle.sourcePath, 'frozen-resource-byte-mismatch');
55
+ } else if (dependency.retained) fail('prepared-invalid', 'Retained resources require their original frozen bytes.', bundle.sourcePath, 'missing-frozen-resource');
56
+ continue;
57
+ }
58
+ let actual;
59
+ try { actual = identity(fs.readFileSync(dependency.path)); } catch (error) {
60
+ fail('dependency-changed', 'A frozen explainer resource is no longer readable.', bundle.sourcePath, error.code || 'read-failed');
61
+ }
62
+ if (actual.sha256 !== dependency.sha256 || actual.bytes !== dependency.bytes) {
63
+ fail('dependency-changed', 'An explainer resource changed during preparation.', bundle.sourcePath, 'frozen-resource-byte-mismatch');
64
+ }
65
+ }
66
+ }
67
+ export function writePreparedBundle(directory, bundle) {
68
+ const file = path.join(directory, 'explainer.prepared.json');
69
+ const bytes = Buffer.from(JSON.stringify(bundle));
70
+ if (bytes.length > MAX_BUNDLE_BYTES) fail('resource-limit', 'Prepared explainer exceeds the resource budget.', bundle.sourcePath, 'bundle-byte-limit');
71
+ fs.writeFileSync(file, bytes, { flag: 'wx', mode: 0o600 });
72
+ return file;
73
+ }
74
+ export function readPreparedBundle(file, sourceText, inputPath, { checkDependencies = true, expectedInputPath = inputPath, quality, writingMode } = {}) {
75
+ let bundle;
76
+ try {
77
+ const stat = fs.statSync(file);
78
+ if (!stat.isFile() || stat.size > MAX_BUNDLE_BYTES) throw new Error('bundle-byte-limit');
79
+ bundle = JSON.parse(fs.readFileSync(file, 'utf8'));
80
+ } catch (error) { fail('prepared-invalid', 'Prepared explainer could not be read.', inputPath, error.code || 'invalid-bundle'); }
81
+ const canonical = value => { try { return fs.realpathSync.native(path.resolve(value)); } catch { return path.resolve(value); } };
82
+ if (typeof (bundle.inputPath || bundle.sourcePath) !== 'string' || canonical(bundle.inputPath || bundle.sourcePath) !== canonical(expectedInputPath)) {
83
+ fail('prepared-mismatch', 'Prepared explainer belongs to a different author input path.', inputPath, 'draft-path-mismatch');
84
+ }
85
+ const source = identity(Buffer.from(sourceText));
86
+ if (bundle?.kind !== 'archify-explainer-prepared' || bundle.schemaVersion !== 1 || bundle.digest !== digest(bundle)
87
+ || source.sha256 !== bundle.source?.sha256 || source.bytes !== bundle.source?.bytes || bundle.sourceText !== sourceText) {
88
+ fail('prepared-mismatch', 'Prepared explainer does not match the original draft bytes.', inputPath, 'draft-or-bundle-digest-mismatch');
89
+ }
90
+ if (quality && quality !== bundle.quality) fail('prepared-mismatch', 'Prepared explainer uses a different quality profile.', inputPath, 'quality-profile-mismatch', 'Prepare the original draft again under the requested quality profile.');
91
+ if (writingMode && writingMode !== (bundle.writingMode || 'warn')) fail('prepared-mismatch', 'Prepared explainer uses a different writing check mode.', inputPath, 'writing-mode-mismatch', 'Prepare the original draft again under the requested writing check mode.');
92
+ const parsed = parseValidatedExplainer(sourceText, { inputPath: bundle.sourcePath, writingMode: bundle.writingMode || 'warn' });
93
+ if (JSON.stringify(parsed.document) !== JSON.stringify(bundle.document)) fail('prepared-mismatch', 'Prepared document differs from the authored draft.', inputPath, 'normalized-document-mismatch');
94
+ if (checkDependencies) assertPreparedDependencies(bundle);
95
+ return bundle;
96
+ }
97
+ export function rebindPreparedBundle(bundle, parsed) {
98
+ const next = { ...bundle, ...parsed, source: identity(Buffer.from(parsed.sourceText)) };
99
+ next.digest = digest(next);
100
+ return next;
101
+ }
102
+
103
+ export function validatePreparedTargets(document, diagrams, options = {}) {
104
+ const visit = (value, pointer, panelId, component) => {
105
+ if (typeof value === 'string') {
106
+ const location = options.sourceMap?.[pointer] || {};
107
+ const inline = parseInline(value, { line: location.line || 1, input: options.sourcePath, panelId, component });
108
+ for (const reference of inline.references) {
109
+ if (!['node','route'].includes(reference.kind)) continue;
110
+ const target = reference.kind === 'node' ? {diagram:reference.diagram,node:reference.node} : {diagram:reference.diagram,route:[reference.from,reference.to]};
111
+ const line = (location.line || 1) + (value.slice(0,reference.offset).match(/\n/g)?.length || 0);
112
+ validateExplainerDiagramTarget(diagrams,target,{...options,pointer,line});
113
+ }
114
+ } else if (Array.isArray(value)) value.forEach((entry,index)=>visit(entry,`${pointer}/${index}`,panelId,component));
115
+ else if (value && typeof value === 'object') for(const [key,entry] of Object.entries(value)) {
116
+ if (['spec','source','path','language','target','changes'].includes(key) || (['diff','snippet'].includes(component) && key==='text')) continue;
117
+ visit(entry,`${pointer}/${key}`,panelId,component);
118
+ }
119
+ };
120
+ visit(document.answer,'/answer',undefined,'answer');
121
+ document.panels.forEach((panel,index)=>{
122
+ visit(panel.title,`/panels/${index}/title`,panel.id,'panel');
123
+ panel.blocks.forEach((block,blockIndex)=>{
124
+ const pointer=`/panels/${index}/blocks/${blockIndex}`;
125
+ visit(block,pointer,panel.id,block.type);
126
+ if(block.type==='steps') block.items.forEach((item,itemIndex)=>{
127
+ if(item.target)validateExplainerDiagramTarget(diagrams,item.target,{...options,pointer:`${pointer}/items/${itemIndex}/target`,line:options.sourceMap?.[`${pointer}/items/${itemIndex}`]?.line || options.sourceMap?.[pointer]?.line || 1});
128
+ });
129
+ });
130
+ });
131
+ }