@stacksjs/defaults 0.75.360 → 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 (209) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +4 -4
  3. package/ai/skills/stacks-payments/SKILL.md +28 -0
  4. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  5. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  6. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  7. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  8. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  9. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  10. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  11. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  12. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  16. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  17. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  18. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  19. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  20. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  21. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  22. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  24. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  27. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  28. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  29. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  32. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  37. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  38. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  39. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  40. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  42. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  43. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  56. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  57. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  58. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  59. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  69. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  70. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  111. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  120. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  121. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  122. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  124. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  125. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  126. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  127. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  128. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  129. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  130. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  134. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  141. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  147. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  149. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  175. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  186. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  187. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  188. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  189. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  190. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  191. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  192. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  194. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  195. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  196. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  199. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  201. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  205. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  206. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  207. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  208. package/ide/vscode/package.json +1 -1
  209. package/package.json +2 -2
@@ -0,0 +1,208 @@
1
+ import fs from 'node:fs';
2
+ import { pathToFileURL } from 'node:url';
3
+ import { scanHtml } from './check.mjs';
4
+
5
+ export const EXPLAINER_VIEWPORTS = Object.freeze([Object.freeze({width:1280,height:900}),Object.freeze({width:1440,height:900})]);
6
+ async function evaluate(browser, expression, awaitPromise=false) {
7
+ let result;try {result=await browser.cdp.send('Runtime.evaluate',{expression,returnByValue:true,awaitPromise},await browser.sessionPromise);}catch(error){error.message += ` Inspector stage: ${expression.slice(0,100)}`;throw error;}
8
+ if (result.exceptionDetails) throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text);
9
+ return result.result?.value;
10
+ }
11
+ const expression = fn => `(${fn.toString()})()`;
12
+
13
+ function measureNativePreviews() {
14
+ const rect=e=>{const r=e.getBoundingClientRect();return {left:r.left,right:r.right,top:r.top,bottom:r.bottom,width:r.width,height:r.height};};
15
+ const visible=e=>{const c=getComputedStyle(e),r=e.getBoundingClientRect();return c.display!=='none'&&c.visibility!=='hidden'&&Number(c.opacity)!==0&&r.width>0&&r.height>0;};
16
+ return Array.from(document.querySelectorAll('.diagram-preview svg')).map(svg=>{
17
+ const block=svg.closest('[data-block-type]'),panel=block.closest('.panel'),preview=svg.closest('.diagram-preview');
18
+ const prose=panel.querySelector('.block-prose > p')||document.querySelector('.block-prose > p'),body=parseFloat(getComputedStyle(prose||panel.querySelector('.panel-body')||block).fontSize);
19
+ const texts=Array.from(svg.querySelectorAll('text')).filter(e=>e.textContent.trim()&&visible(e)).map(e=>{const m=e.getScreenCTM(),sum=m.a*m.a+m.b*m.b+m.c*m.c+m.d*m.d,det=m.a*m.d-m.b*m.c,scale=Math.sqrt(Math.max(0,(sum-Math.sqrt(Math.max(0,sum*sum-4*det*det)))/2));return {text:e.textContent.trim(),size:parseFloat(getComputedStyle(e).fontSize)*scale/(e.closest('[data-edge-id]')?.7:1)};});
20
+ const minimum=texts.length?Math.min(...texts.map(t=>t.size)):null,bounds=rect(preview),svgBounds=rect(svg);
21
+ const nodes=Array.from(svg.querySelectorAll('[data-node-id]')).filter(visible).map(e=>({id:e.dataset.nodeId,...rect(e)}));
22
+ const outside=nodes.filter(n=>n.left<bounds.left-1||n.right>bounds.right+1||n.top<bounds.top-1||n.bottom>bounds.bottom+1);
23
+ return {component:block.id,minimum,body,ratio:minimum===null?null:minimum/body,smallest:texts.find(t=>t.size===minimum),rect:{width:svgBounds.width,height:svgBounds.height},computed:{width:getComputedStyle(svg).width,height:getComputedStyle(svg).height},fit:preview.dataset.diagramFit,requiredWidth:Number(preview.dataset.diagramRequiredWidth)||null,minimumText:Number(preview.dataset.diagramMinimumText)||null,minWidth:svg.style.minWidth,viewBox:svg.getAttribute('viewBox'),preview:bounds,scrollWidth:preview.scrollWidth,clientWidth:preview.clientWidth,nodes,outsidePreview:outside};
24
+ });
25
+ }
26
+
27
+ /** Actual CSS/CTM geometry of the current native previews, including live resize. */
28
+ export async function inspectNativePreviewLayout(browser) {return evaluate(browser,expression(measureNativePreviews));}
29
+
30
+ async function measurePublicHtml(nativeClipping=true) {
31
+ const visible=e=>{const c=getComputedStyle(e),r=e.getBoundingClientRect();return c.display!=='none'&&c.visibility!=='hidden'&&Number(c.opacity)!==0&&r.width>0&&r.height>0;};
32
+ const owner=e=>e.closest('[data-block-type],.panel')?.id || e.id || e.tagName.toLowerCase();
33
+ const domPath=e=>{const parts=[];for(let p=e;p;p=p.parentElement){const tag=p.localName,peers=p.parentElement?Array.from(p.parentElement.children).filter(c=>c.localName===tag):[p];parts.unshift(`${tag}:nth-of-type(${peers.indexOf(p)+1})`);}return parts.join(' > ');};
34
+ const identity=e=>({stableComponent:domPath(e),text:(e.textContent||'').trim().slice(0,160),label:e.getAttribute('aria-label')||e.getAttribute('title')||'',href:e.getAttribute('href')||null});
35
+ const ancestors=e=>{const result=[];for(let p=e.parentElement;p;p=p.parentElement)result.push(p);return result;};
36
+ const regions=Array.from(document.querySelectorAll('body *')).filter(visible);
37
+ // Chrome computes the actual containing-block/clip chain. Expanding only the
38
+ // observer root exposes off-viewport boxes while retaining every local clip,
39
+ // including SVG, nested scrollers, transforms and out-of-flow descendants.
40
+ const extent=Math.ceil(regions.reduce((maximum,e)=>{const r=e.getBoundingClientRect();return Math.max(maximum,Math.abs(r.left),Math.abs(r.right),Math.abs(r.top),Math.abs(r.bottom));},Math.max(innerWidth,innerHeight,document.documentElement.scrollWidth,document.documentElement.scrollHeight)))+1;
41
+ const intersections=nativeClipping?await new Promise((resolve,reject)=>{
42
+ if(!regions.length){resolve(new Map());return;}
43
+ const entries=new Map();let observer;
44
+ const timeout=setTimeout(()=>{observer.disconnect();reject(new Error('Native clipping measurement did not complete.'));},2000);
45
+ observer=new IntersectionObserver(batch=>{for(const entry of batch)entries.set(entry.target,entry);if(entries.size===regions.length){clearTimeout(timeout);observer.disconnect();resolve(entries);}},{root:null,rootMargin:`${extent}px`});
46
+ regions.forEach(e=>observer.observe(e));
47
+ }):new Map();
48
+ const overflows=(nativeClipping?regions:[]).filter(e=>{const r=intersections.get(e).intersectionRect;return r.width>0&&r.height>0&&(r.left<-1||r.right>innerWidth+1);}).slice(0,20).map(e=>{const entry=intersections.get(e),r=entry.intersectionRect;return {...identity(e),component:owner(e),tag:e.tagName,left:r.left,right:r.right,bounds:{left:entry.boundingClientRect.left,right:entry.boundingClientRect.right}};});
49
+ const localClipping=(nativeClipping?regions:[]).filter(e=>{const entry=intersections.get(e),r=entry.boundingClientRect,i=entry.intersectionRect;const style=getComputedStyle(e),ownCrop=(['hidden','clip'].includes(style.overflowX)&&e.scrollWidth>e.clientWidth+1)||(['hidden','clip'].includes(style.overflowY)&&e.scrollHeight>e.clientHeight+1);return (ownCrop||i.width<r.width-1||i.height<r.height-1)&&(Array.from(e.childNodes).some(n=>n.nodeType===3&&n.textContent.trim())||e.matches('a[href],button,input,select,textarea,[role=button],[role=link]'));}).map(e=>{
50
+ const clips=ancestors(e).filter(p=>{const s=getComputedStyle(p);return ['auto','scroll','hidden','clip'].includes(s.overflowX)||['auto','scroll','hidden','clip'].includes(s.overflowY)||s.clipPath!=='none'||s.clip!=='auto'||/(?:paint|strict|content)/.test(s.contain);}).map(p=>{const s=getComputedStyle(p),b=p.getBoundingClientRect(),t=e.getBoundingClientRect(),cropX=t.left<b.left-1||t.right>b.right+1,cropY=t.top<b.top-1||t.bottom>b.bottom+1;return {...identity(p),overflowX:s.overflowX,overflowY:s.overflowY,scrollableX:['auto','scroll'].includes(s.overflowX)&&p.scrollWidth>p.clientWidth+1,scrollableY:['auto','scroll'].includes(s.overflowY)&&p.scrollHeight>p.clientHeight+1,nonScrollableClip:(cropX&&['hidden','clip'].includes(s.overflowX))||(cropY&&['hidden','clip'].includes(s.overflowY))||s.clipPath!=='none'||s.clip!=='auto'||((cropX||cropY)&&/(?:paint|strict|content)/.test(s.contain))};});
51
+ const entry=intersections.get(e),r=entry.boundingClientRect,i=entry.intersectionRect,scrollable=clips.some(c=>c.scrollableX||c.scrollableY),ownStyle=getComputedStyle(e),ownCrop=(['hidden','clip'].includes(ownStyle.overflowX)&&e.scrollWidth>e.clientWidth+1)||(['hidden','clip'].includes(ownStyle.overflowY)&&e.scrollHeight>e.clientHeight+1),nonScrollable=ownCrop||clips.some(c=>c.nonScrollableClip);
52
+ return {...identity(e),component:owner(e),tag:e.tagName,access:scrollable?(nonScrollable?'mixed-scroll-and-clipping':'manual-scroll'):'clipped-no-manual-scroll',ownContentClipped:ownCrop,actionable:e.matches('a[href],button,input,select,textarea,[role=button],[role=link]'),keyboardReachability:'not-measured-by-clipping',bounds:{left:r.left,right:r.right,top:r.top,bottom:r.bottom},painted:{left:i.left,right:i.right,top:i.top,bottom:i.bottom,width:i.width,height:i.height},clipAncestors:clips};
53
+ });
54
+ const refs=Array.from(document.querySelectorAll('a[href^="#"]')).filter(e=>e.getAttribute('href').length>1).filter(e=>{let id=e.getAttribute('href').slice(1);try{id=decodeURIComponent(id);}catch{}return !document.getElementById(id);}).map(e=>({...identity(e),component:owner(e)}));
55
+ const ids=new Set(),duplicateIds=[];document.querySelectorAll('[id]').forEach(e=>{if(ids.has(e.id))duplicateIds.push(e.id);ids.add(e.id);});
56
+ const controls=Array.from(document.querySelectorAll('a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[onclick],[tabindex]')).filter(e=>{const c=getComputedStyle(e),r=e.getBoundingClientRect();return c.display!=='none'&&c.visibility!=='hidden'&&r.width>0&&r.height>0&&!e.disabled&&e.type!=='hidden'&&!e.closest('[inert]');});
57
+ const keyboardIssues=controls.filter(e=>e.tabIndex<0 || (!['A','BUTTON','INPUT','SELECT','TEXTAREA','SUMMARY'].includes(e.tagName)&&!['button','link'].includes(e.getAttribute('role')))).map(e=>({...identity(e),component:owner(e),tag:e.tagName,reason:'An active control is outside sequential keyboard focus or lacks its semantic role.'}));
58
+ controls.forEach((e,i)=>{e.setAttribute('data-qa-key',String(i));e.setAttribute('data-qa-path',domPath(e));});
59
+ const occluded=controls.filter(e=>{const r=e.getBoundingClientRect();if(r.top<0||r.bottom>innerHeight)return false;const hit=document.elementFromPoint(r.x+r.width/2,r.y+r.height/2);return hit&&!e.contains(hit)&&!hit.contains(e);}).map(e=>({...identity(e),component:owner(e),tag:e.tagName}));
60
+ const texts=regions.filter(e=>Array.from(e.childNodes).some(n=>n.nodeType===3&&n.textContent.trim())).map(e=>({...identity(e),component:owner(e),fontSize:(function(){const size=parseFloat(getComputedStyle(e).fontSize);if(typeof e.getScreenCTM!=='function')return size;const m=e.getScreenCTM();if(!m)return size;const sum=m.a*m.a+m.b*m.b+m.c*m.c+m.d*m.d,det=m.a*m.d-m.b*m.c;return size*Math.sqrt(Math.max(0,(sum-Math.sqrt(Math.max(0,sum*sum-4*det*det)))/2));})()}));
61
+ const smallText=texts.filter(e=>e.fontSize<10);
62
+ const staticReading=Array.from(document.querySelectorAll('.answer-panel,.typical-footnotes li')).map(e=>({component:owner(e),visible:visible(e),text:e.textContent.trim()}));
63
+ const background=getComputedStyle(document.body).backgroundColor || getComputedStyle(document.documentElement).backgroundColor;
64
+ return {measurementMethod:{version:'public-html-qa-v2',clipping:nativeClipping?'native-intersection-observer-expanded-root':'not-measured-script-execution-disabled',clippingMeasured:nativeClipping,clipCoverage:nativeClipping?'native-clip-chain':'not-measured-script-disabled',documentExtentMeasured:true,rootMargin:nativeClipping?extent:null,limits:['Axis-aligned element intersections; does not inspect pixels, shadows, masks or visual quality.','Local clipping access classification uses ancestor axis-aligned bounds; mixed clips and non-rectangular clipping need human review. Keyboard traversal is separate.']},staticReading,innerWidth,innerHeight,scrollWidth:Math.ceil(document.documentElement.scrollWidth),scrollHeight:Math.ceil(document.documentElement.scrollHeight),overflowElements:overflows,localClipping,references:refs,duplicateIds,keyboardIssues,controlCount:controls.length,occluded,smallText,bodyText:document.body.innerText,background,resolvedTheme:document.documentElement.dataset.theme || (matchMedia('(prefers-color-scheme:dark)').matches?'dark':'light'),panels:Array.from(document.querySelectorAll('.panel,[data-block-type]')).map(e=>({id:e.id,visible:visible(e),text:e.innerText})),interactiveVisible:document.querySelectorAll('.interactive').length ? Array.from(document.querySelectorAll('.interactive')).filter(visible).map(owner):[]};
65
+ }
66
+
67
+ async function keyboardTraversal(browser, controlCount) {
68
+ const session=await browser.sessionPromise;
69
+ await evaluate(browser,"(function(){document.activeElement.blur();window.scrollTo(0,0);})()");
70
+ const reached=new Set();
71
+ // Radio groups intentionally expose one tab stop; Arrow keys reach siblings.
72
+ const record=async()=>{const state=await evaluate(browser,"({key:document.activeElement?.dataset.qaKey,radio:document.activeElement?.type==='radio',name:document.activeElement?.name})");if(state?.key!==undefined)reached.add(state.key);return state;};
73
+ const arrowGroups=new Set();
74
+ for(let i=0;i<controlCount+3;i++) {
75
+ await browser.cdp.send('Input.dispatchKeyEvent',{type:'keyDown',key:'Tab',code:'Tab',windowsVirtualKeyCode:9},session);
76
+ await browser.cdp.send('Input.dispatchKeyEvent',{type:'keyUp',key:'Tab',code:'Tab',windowsVirtualKeyCode:9},session);
77
+ const state=await record();
78
+ if(state.radio&&!arrowGroups.has(state.name)) {
79
+ arrowGroups.add(state.name);
80
+ const siblings=await evaluate(browser,`document.querySelectorAll('input[type=radio][name="${state.name}"][data-qa-key]').length`);
81
+ for(let step=0;step<siblings;step++) {for(const type of ['keyDown','keyUp'])await browser.cdp.send('Input.dispatchKeyEvent',{type,key:'ArrowRight',code:'ArrowRight',windowsVirtualKeyCode:39},session);await record();}
82
+ }
83
+ }
84
+ const missing=await evaluate(browser,`Array.from(document.querySelectorAll('[data-qa-key]')).filter(e=>!${JSON.stringify([...reached])}.includes(e.dataset.qaKey)).map(e=>({key:e.dataset.qaKey,stableComponent:e.dataset.qaPath,text:e.textContent.trim().slice(0,160),href:e.getAttribute('href')||null,tag:e.tagName,component:e.closest('[data-block-type],.panel')?.id||e.id||e.tagName.toLowerCase(),label:e.getAttribute('aria-label')||e.textContent.trim()||e.name,tabIndex:e.tabIndex,type:e.type,reason:'This active control was not reached by Tab or native radio ArrowRight navigation.'}))`);
85
+ return {count:controlCount,reached:reached.size,missing,ok:reached.size===controlCount};
86
+ }
87
+
88
+ export async function inspectPublicHtml(browser,{artifactPath,width=1440,height=900,theme='light',themeControl='query',javascript=true,media='screen',keyboard=true,screenshotPath,writeScreenshot}={}) {
89
+ const session=await browser.sessionPromise;
90
+ const errors=[];
91
+ let clearStorage;
92
+ if(!['query','system'].includes(themeControl))throw new Error('themeControl must be query or system.');
93
+ const unsubscribe=browser.cdp.subscribe?.(message=>{
94
+ if(message.sessionId!==session)return;
95
+ if(message.method==='Runtime.exceptionThrown')errors.push(message.params.exceptionDetails.exception?.description || message.params.exceptionDetails.text);
96
+ if(message.method==='Runtime.consoleAPICalled'&&message.params.type==='error')errors.push(message.params.args.map(a=>a.value??a.description).join(' '));
97
+ if(message.method==='Log.entryAdded'&&message.params.entry.level==='error')errors.push(message.params.entry.text);
98
+ });
99
+ try {
100
+ await browser.cdp.send('Page.bringToFront',{},session);
101
+ await browser.cdp.send('Log.enable',{},session);
102
+ await browser.cdp.send('Emulation.setDeviceMetricsOverride',{width,height,deviceScaleFactor:1,mobile:false},session);
103
+ await browser.cdp.send('Emulation.setEmulatedMedia',{media,features:[{name:'prefers-color-scheme',value:theme},{name:'prefers-reduced-motion',value:'reduce'}]},session);
104
+ await browser.cdp.send('Emulation.setScriptExecutionDisabled',{value:!javascript},session);
105
+ const url=pathToFileURL(artifactPath);if(themeControl==='query')url.searchParams.set('theme',theme);else clearStorage=await browser.cdp.send('Page.addScriptToEvaluateOnNewDocument',{source:'try {localStorage.clear();sessionStorage.clear();} catch {}'},session);
106
+ const loaded=browser.cdp.waitFor('Page.loadEventFired',session);loaded.catch(()=>{});
107
+ const navigation=await browser.cdp.send('Page.navigate',{url:url.href},session);if(navigation.errorText)throw new Error(navigation.errorText);await loaded;
108
+ if(themeControl==='system'&&!javascript)await evaluate(browser,"(function(){try{localStorage.clear();sessionStorage.clear();return true;}catch{return false;}})()");
109
+ // Inspector evaluation works with page JavaScript disabled; it does not run
110
+ // or repair the artifact's reader. Font readiness is separate from reader JS.
111
+ const fontStart=Date.now();let fontStatus;
112
+ do {fontStatus=await evaluate(browser,"(function(){void document.body.offsetHeight;return document.fonts.status;})()");if(fontStatus==='loaded')break;await new Promise(resolve=>setTimeout(resolve,50));}while(Date.now()-fontStart<5000);
113
+ const fontReadiness={status:fontStatus,waitMs:Date.now()-fontStart};
114
+ await new Promise(resolve=>setTimeout(resolve,50));
115
+ const metrics=await evaluate(browser,`(${measurePublicHtml.toString()})(${javascript})`,true);
116
+ metrics.fontReadiness=fontReadiness;metrics.themeControl=themeControl;metrics.navigationUrl=url.href;
117
+ if(javascript&&keyboard)metrics.keyboardTraversal=await keyboardTraversal(browser,metrics.controlCount);
118
+ metrics.consoleErrors=errors;
119
+ if(screenshotPath) {
120
+ await evaluate(browser,'window.scrollTo(0,0)');
121
+ const result=await browser.cdp.send('Page.captureScreenshot',{format:'png',fromSurface:true,captureBeyondViewport:false},session,20000);
122
+ if(!result.data)throw new Error('Chrome returned an empty screenshot.');
123
+ const bytes=Buffer.from(result.data,'base64');if(writeScreenshot)writeScreenshot(bytes);else fs.writeFileSync(screenshotPath,bytes,{flag:'wx'});
124
+ }
125
+ return metrics;
126
+ } finally {unsubscribe?.();if(clearStorage)await browser.cdp.send('Page.removeScriptToEvaluateOnNewDocument',{identifier:clearStorage.identifier},session);await browser.cdp.send('Emulation.setScriptExecutionDisabled',{value:false},session);}
127
+ }
128
+
129
+ export async function inspectNativeViewer(browser) {
130
+ const parent=await browser.cdp.send('Page.getFrameTree',{},await browser.sessionPromise);
131
+ const targets=await browser.cdp.send('Target.getTargets');
132
+ const target=targets.targetInfos.find(t=>t.type==='iframe'&&t.parentId===parent.frameTree.frame.id&&t.url.startsWith('about:srcdoc'));
133
+ if(!target)return {available:false,errors:[]};
134
+ const attached=await browser.cdp.send('Target.attachToTarget',{targetId:target.targetId,flatten:true});
135
+ const errors=[];
136
+ const stop=browser.cdp.subscribe?.(m=>{if(m.sessionId!==attached.sessionId)return;if(m.method==='Runtime.exceptionThrown')errors.push(m.params.exceptionDetails.exception?.description||m.params.exceptionDetails.text);if(m.method==='Runtime.consoleAPICalled'&&m.params.type==='error')errors.push(m.params.args.map(a=>a.value??a.description).join(' '));});
137
+ try {
138
+ await browser.cdp.send('Runtime.enable',{},attached.sessionId);
139
+ const result=await browser.cdp.send('Runtime.evaluate',{expression:"({ready:document.readyState,visibility:document.visibilityState,href:location.href,active:document.querySelector('svg[data-focus-active]')?.dataset.focusActive||'',nodes:[...new Set(Array.from(document.querySelectorAll('[data-focus-selected]')).map(e=>e.dataset.nodeId).filter(Boolean))],edges:[...new Set(Array.from(document.querySelectorAll('[data-edge-id][data-focus-match]')).map(e=>e.dataset.edgeId))],hasViewer:Boolean(window.Archify?.focus)})",returnByValue:true},attached.sessionId);
140
+ if(result.exceptionDetails)throw new Error(result.exceptionDetails.text);
141
+ return {available:true,targetId:target.targetId,...result.result.value,errors};
142
+ } finally {stop?.();await browser.cdp.send('Target.detachFromTarget',{sessionId:attached.sessionId});}
143
+ }
144
+
145
+ async function press(browser,key) {const session=await browser.sessionPromise;const codes={Enter:13,Escape:27,' ':32};for(const type of ['keyDown','keyUp'])await browser.cdp.send('Input.dispatchKeyEvent',{type,key,code:key===' '?'Space':key,windowsVirtualKeyCode:codes[key],...(key==='Enter'&&type==='keyDown'?{text:'\r',unmodifiedText:'\r'}:{})},session);await evaluate(browser,'new Promise(resolve=>setTimeout(resolve,0))',true);}
146
+ async function activate(browser,selector,key='Enter') {const exists=await evaluate(browser,`(function(){const e=document.querySelector(${JSON.stringify(selector)});if(!e)return false;e.scrollIntoView({block:'center'});e.focus();return document.activeElement===e;})()`);if(!exists)return false;await press(browser,key);return true;}
147
+
148
+ export async function inspectExplainerBrowser(browser,options) {
149
+ if (browser.resetPage) await browser.resetPage();
150
+ const interactionErrors=[];
151
+ const session=await browser.sessionPromise;
152
+ const stop=browser.cdp.subscribe?.(message=>{if(message.sessionId!==session)return;if(message.method==='Runtime.exceptionThrown')interactionErrors.push(message.params.exceptionDetails.exception?.description||message.params.exceptionDetails.text);if(message.method==='Runtime.consoleAPICalled'&&message.params.type==='error')interactionErrors.push(message.params.args.map(a=>a.value??a.description).join(' '));});
153
+ try {
154
+ const clear=await browser.cdp.send('Page.addScriptToEvaluateOnNewDocument',{source:'try {localStorage.clear();} catch {}'},session);
155
+ let metrics;try{metrics=await inspectPublicHtml(browser,options);}finally{await browser.cdp.send('Page.removeScriptToEvaluateOnNewDocument',{identifier:clear.identifier},session);}
156
+ const failures=[];
157
+ const check=(name,ok,reason,component='reader',measurement)=>{if(!ok)failures.push({name,reason,component,...(measurement?{measurement}:{})});};
158
+ const diagram=await evaluate(browser,"document.querySelector('[data-block-type=steps] [data-diagram-link]')?.dataset.diagramLink || document.querySelector('[data-diagram-link]')?.dataset.diagramLink || document.querySelector('[data-open-diagram]')?.dataset.openDiagram || null");
159
+ if(diagram) {
160
+ const selector=await evaluate(browser,"document.querySelector('[data-block-type=steps] [data-diagram-link]')?'[data-block-type=steps] [data-diagram-link]':document.querySelector('[data-diagram-link]')?'[data-diagram-link]':'[data-open-diagram]'");
161
+ check('diagram-keyboard',await activate(browser,selector),'Diagram link cannot be activated by keyboard.',diagram);
162
+ for(let attempt=0;attempt<30;attempt++) {if(await evaluate(browser,"Boolean(document.querySelector('#diagram-dialog')?.dataset.nativeFocus)"))break;await new Promise(resolve=>setTimeout(resolve,100));}
163
+ const frame=await evaluate(browser,"(function(){const d=document.querySelector('#diagram-dialog'),f=d.querySelector('iframe');return {open:d.open,frame:Boolean(f),sandbox:f?.getAttribute('sandbox'),loaded:d.dataset.nativeLoaded,ready:d.dataset.nativeReady,loadingHidden:d.querySelector('.dialog-loading')?.hidden,visible:document.visibilityState,frameRect:f?{width:f.getBoundingClientRect().width,height:f.getBoundingClientRect().height}:null,error:d.querySelector('.dialog-loading')?.textContent};})()");
164
+ check('diagram-viewer',frame.open&&frame.frame,'Diagram activation did not open the canonical Viewer.',diagram,frame);
165
+ const focus=await evaluate(browser,`(function(){const link=document.querySelector(${JSON.stringify(selector)}),hash=link.dataset.diagramHash||'',edges=JSON.parse(link.dataset.diagramEdges||'[]'),bundle=JSON.parse(document.getElementById('archify-explainer-prepared').textContent),child=bundle.preparedDiagrams.diagrams[link.dataset.diagramLink||link.dataset.openDiagram],nodes=hash.startsWith('focus=')?[decodeURIComponent(hash.slice(6))]:hash.startsWith('route=')?hash.slice(6).split('~').map(decodeURIComponent):[...new Set((child.semantic?.edges||[]).filter(e=>edges.includes(e.id)).flatMap(e=>[e.from,e.to]))];let actual;try{actual=JSON.parse(document.querySelector('#diagram-dialog').dataset.nativeFocus||'null');}catch{}return {component:link.closest('[data-block-type]')?.id||link.dataset.diagramLink,nodes,edges,actual,frame:{ready:document.querySelector('#diagram-dialog').dataset.nativeReady,loaded:document.querySelector('#diagram-dialog').dataset.nativeLoaded,loadingHidden:document.querySelector('#diagram-dialog .dialog-loading').hidden}};})()`);
166
+ const native=await inspectNativeViewer(browser);
167
+ metrics.nativeViewer=native;
168
+ interactionErrors.push(...native.errors);
169
+ if(focus.nodes.length||focus.edges.length)check('diagram-focus',native.available&&native.ready==='complete'&&native.hasViewer&&Boolean(native.active)&&focus.nodes.length===native.nodes?.length&&focus.nodes.every(id=>native.nodes?.includes(id))&&(!focus.edges.length||focus.edges.length===native.edges?.length)&&focus.edges.every(id=>native.edges?.includes(id)),'The canonical Viewer did not focus the step’s exact nodes and edges.',focus.component,{...focus,dialog:frame,native});
170
+ await press(browser,'Escape');
171
+ }
172
+ const nativeText=await inspectNativePreviewLayout(browser);
173
+ for(const entry of nativeText){check('diagram-fit',entry.fit!=='insufficient'&&entry.scrollWidth<=entry.clientWidth+1&&entry.outsidePreview.length===0,'The native preview cannot fit its complete graph at the required text size.',entry.component,entry);check('diagram-text',entry.minimum!==null&&entry.minimum>=10&&(options.width<1440||entry.ratio>=0.75),'Native diagram text is below its viewport reading floor (10px; 75% of body at 1440).',entry.component,entry);}
174
+ metrics.nativeText=nativeText;
175
+ metrics.interactionFailures=failures;
176
+ const noJavaScript=[];
177
+ for(const theme of ['light','dark']) {if(browser.resetPage)await browser.resetPage();noJavaScript.push({theme,metrics:await inspectPublicHtml(browser,{...options,theme,javascript:false,keyboard:false,screenshotPath:undefined})});}
178
+ if(browser.resetPage)await browser.resetPage();
179
+ const print=await inspectPublicHtml(browser,{...options,theme:'light',media:'print',keyboard:false,screenshotPath:undefined});
180
+ metrics.noJavaScript=noJavaScript.map(({theme,metrics:entry})=>({theme,ok:entry.panels.length>0&&entry.panels.every(p=>p.visible&&p.text.trim())&&entry.staticReading.every(p=>p.visible&&p.text)&&entry.interactiveVisible.length===0&&noJavaScript[0].metrics.background!==noJavaScript[1].metrics.background,panels:entry.panels,interactiveVisible:entry.interactiveVisible,background:entry.background,measurementMethod:entry.measurementMethod,fontReadiness:entry.fontReadiness}));
181
+ const typical=await evaluate(browser,"({links:Array.from(document.querySelectorAll('.typical-ref')).map(e=>e.getAttribute('href')),notes:Array.from(document.querySelectorAll('.typical-footnotes li')).map(e=>({id:e.id,text:e.textContent,visible:getComputedStyle(e).display!=='none'&&e.getBoundingClientRect().height>0}))})");
182
+ metrics.print={ok:print.panels.length>0&&print.panels.every(p=>p.visible&&p.text.trim())&&print.interactiveVisible.length===0&&print.staticReading.every(p=>p.visible&&p.text)&&typical.links.every(link=>typical.notes.some(note=>link==='#'+note.id&&note.visible&&note.text.trim())),panels:print.panels,interactiveVisible:print.interactiveVisible,typical,measurementMethod:print.measurementMethod,fontReadiness:print.fontReadiness};
183
+ metrics.consoleErrors=[...new Set([...metrics.consoleErrors,...interactionErrors,...noJavaScript.flatMap(x=>x.metrics.consoleErrors),...print.consoleErrors])];
184
+ return metrics;
185
+ } finally {stop?.();}
186
+ }
187
+
188
+ export function explainerObservation({width,height,theme,metrics}) {
189
+ const overflowX=metrics.scrollWidth>metrics.innerWidth+1||metrics.overflowElements.length>0;
190
+ return {...metrics,width,height,theme,overflowX,overflowY:metrics.scrollHeight>metrics.innerHeight,verticalScrollAccepted:!overflowX,ok:!overflowX,readabilityOk:metrics.smallText.length===0,viewerChromeOk:metrics.occluded.length===0};
191
+ }
192
+ export function explainerObservationDiagnostics({artifact,allObservations}) {
193
+ const diagnostics=[];
194
+ let locations=new Map(),sourceInput;
195
+ try {
196
+ const parsed=scanHtml(fs.readFileSync(artifact,'utf8'));
197
+ locations=new Map(parsed.elements.filter(e=>e.attrs.id).map(e=>[e.attrs.id,e.line]));
198
+ const payload=parsed.scripts.find(s=>s.attrs.id==='archify-explainer-prepared');
199
+ if(payload) {const bundle=JSON.parse(payload.text);sourceInput=bundle.sourcePath;bundle.document.panels.forEach((panel,index)=>{locations.set(panel.id,bundle.sourceMap?.[`/panels/${index}`]?.line||locations.get(panel.id)||1);panel.blocks.forEach((block,j)=>locations.set(block.id,bundle.sourceMap?.[`/panels/${index}/blocks/${j}`]?.line||locations.get(block.id)||1));});}
200
+ }catch{}
201
+ for(const entry of allObservations) {
202
+ const failures=[...(entry.overflowX?[{name:'horizontal-overflow',reason:'Rendered content exceeds the horizontal viewport.',component:entry.overflowElements[0]?.component || 'document'}]:[]),...entry.keyboardIssues.map(x=>({name:'keyboard',...x})),...(entry.keyboardTraversal&&!entry.keyboardTraversal.ok?entry.keyboardTraversal.missing.map(m=>({name:'keyboard-traversal',reason:`${m.reason} ${m.tag} ${m.label}`,component:m.component})):[]),...entry.consoleErrors.map(reason=>({name:'console-error',reason,component:'reader'})),...entry.references.map(x=>({name:'reference',reason:`Unresolved reference ${x.href}.`,component:x.component})),...entry.duplicateIds.map(id=>({name:'duplicate-id',reason:`Duplicate DOM ID ${id}.`,component:'document'})),...entry.smallText.map(x=>({name:'readability',reason:`Rendered text is ${x.fontSize}px, below 10px.`,component:x.component})),...entry.occluded.map(x=>({name:'control-occluded',reason:'Another element covers an active control.',component:x.component})),...(entry.interactionFailures || []),...entry.noJavaScript.filter(x=>!x.ok).map(x=>({name:'no-javascript',reason:`Static reading content or inactive controls fail with JavaScript disabled (${x.theme}).`,component:'document'})),...(!entry.print.ok?[{name:'print',reason:'Print hides necessary reading content or displays inactive controls.',component:'document'}]:[])];
203
+ if(entry.resolvedTheme!==entry.theme)failures.push({name:'theme',reason:`Requested ${entry.theme}; resolved ${entry.resolvedTheme}.`,component:'theme'});
204
+ for(const fail of failures)diagnostics.push({code:`explainer/browser-${fail.name}`,severity:'error',message:fail.reason,subject:{artifact,...(sourceInput?{input:sourceInput}:{}),line:locations.get(fail.component)||1,component:fail.component,viewport:{width:entry.width,height:entry.height,theme:entry.theme}},evidence:{reason:fail.reason,example:repairExample(fail.name),...(fail.measurement?{measurement:fail.measurement}:{})},supportedFixes:[repairExample(fail.name)]});
205
+ }
206
+ return diagnostics;
207
+ }
208
+ function repairExample(name) {return name==='horizontal-overflow'?'Use min-width:0 and bounded horizontal scrolling for tables or code.':name.startsWith('keyboard')?'Use a native button/link/input with visible focus and keyboard activation.':name==='no-javascript'||name==='print'?'Keep canonical content visible and hide interactive controls in no-JavaScript and print styles.':name==='diagram-text'?'Increase the diagram panel span until projected node text reaches 75% of body text.':'Repair the reported component and re-render the source.';}
@@ -0,0 +1,70 @@
1
+ import { PeleError, detectType, register, render } from './vendor/pele/core.js';
2
+ import pie from './vendor/pele/diagrams/pie.js';
3
+ import xychart from './vendor/pele/diagrams/xychart.js';
4
+ import quadrantChart from './vendor/pele/diagrams/quadrantChart.js';
5
+ import sankey from './vendor/pele/diagrams/sankey.js';
6
+ import radar from './vendor/pele/diagrams/radar.js';
7
+ import venn from './vendor/pele/diagrams/venn.js';
8
+
9
+ register(pie, xychart, quadrantChart, sankey, radar, venn);
10
+
11
+ const NATIVE = { flowchart: 'workflow or architecture', sequence: 'sequence', class: 'class', er: 'erd', state: 'lifecycle', timeline: 'timeline', gantt: 'timeline or waterfall', mindmap: 'tree', treeview: 'tree' };
12
+
13
+ // A pie legend reads "Label · value (share%)" rather than Pele's "Label [value]". The labels are
14
+ // rewritten before rendering so the legend is laid out for their real width.
15
+ function pieLegend(text) {
16
+ if (!/^\s*pie\b/u.test(text)) return text;
17
+ const slice = /^(\s*)"((?:[^"\\]|\\.)*)"\s*:\s*(-?\d+(?:\.\d+)?)\s*$/gmu;
18
+ const total = [...text.matchAll(slice)].reduce((sum, match) => sum + Number(match[3]), 0);
19
+ if (!(total > 0)) return text;
20
+ return text.replace(/^(\s*pie)\s+showData\b/u, '$1').replace(slice, (_, indent, label, value) => { const share = Math.round(Number(value) / total * 100); return `${indent}"${label} · ${value}${/\p{Script=Han}/u.test(label) ? `(${share}%)` : ` (${share}%)`}" : ${value}`; });
21
+ }
22
+
23
+ /** Render a Mermaid chart to static SVG, or return a located diagnostic reason. */
24
+ export function compileExplainerChart(text) {
25
+ const type = detectType(text);
26
+ if (type && NATIVE[type]) return { error: `Mermaid ${type} is drawn with the native ${NATIVE[type]} shorthand, which Archify validates and lets readers explore.`, example: `@${NATIVE[type].split(' ')[0]} id=...` };
27
+ try {
28
+ const result = render(pieLegend(text), { padding: 8, fontSize: 13, links: false, images: false, config: { xyChart: { width: 480, height: 260, showDataLabel: true }, quadrantChart: { chartWidth: 400, chartHeight: 400 } } });
29
+ // A rotated CJK axis title lies on its side; vertical writing keeps the characters upright.
30
+ const svg = result.svg.replace(/<g transform="translate\(([\d.-]+),([\d.-]+)\) rotate\(-90\)"><text x="0" y="[\d.-]+" text-anchor="middle">([^<]*\p{Script=Han}[^<]*)<\/text><\/g>/gu, (_, x, y, label) => `<text x="${x}" y="${y}" text-anchor="middle" writing-mode="vertical-rl">${label}</text>`);
31
+ return { svg, width: result.width, height: result.height, type: result.type };
32
+ } catch (error) {
33
+ if (!(error instanceof PeleError)) throw error;
34
+ const supported = 'pie, xychart-beta (bar and line), quadrantChart, sankey-beta, radar-beta or venn-beta';
35
+ return { error: error.code === 'unsupported-diagram' ? `A chart fence supports ${supported}.` : error.message.split('\n')[0], line: error.line, example: 'pie title Share\n "A" : 60\n "B" : 40' };
36
+ }
37
+ }
38
+
39
+ // A data file becomes chart text: CSV with a header row, or JSON as an array of objects. Labels come
40
+ // from column x and one series from each column in y; a pie takes the first y column as its values.
41
+ export function chartFromData(bytes, filePath, { kind, x, y, unit }) {
42
+ let rows;
43
+ const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes).replace(/^\uFEFF/u, '');
44
+ if (/\.json$/iu.test(filePath)) {
45
+ try { rows = JSON.parse(text); } catch { return { error: 'The data file is not valid JSON.' }; }
46
+ if (!Array.isArray(rows) || !rows.every(row => row && typeof row === 'object' && !Array.isArray(row))) return { error: 'JSON chart data must be an array of objects.' };
47
+ } else {
48
+ const cells = line => { const out = []; let cell = '', quoted = false; for (let i = 0; i < line.length; i++) { const c = line[i]; if (quoted) { if (c === '"' && line[i + 1] === '"') { cell += '"'; i++; } else if (c === '"') quoted = false; else cell += c; } else if (c === '"') quoted = true; else if (c === ',') { out.push(cell.trim()); cell = ''; } else cell += c; } out.push(cell.trim()); return out; };
49
+ const lines = text.split(/\r?\n/u).filter(line => line.trim());
50
+ if (lines.length < 2) return { error: 'CSV chart data needs a header row and at least one data row.' };
51
+ const header = cells(lines[0]);
52
+ rows = lines.slice(1).map(line => Object.fromEntries(cells(line).map((value, i) => [header[i], value])));
53
+ }
54
+ const columns = new Set(rows.flatMap(row => Object.keys(row)));
55
+ const missing = [x, ...y].find(column => !columns.has(column));
56
+ if (missing) return { error: `The data file has no column "${missing}". Columns: ${[...columns].join(', ')}.` };
57
+ if (rows.length > (kind === 'pie' ? 12 : 60)) return { error: `A ${kind} chart takes at most ${kind === 'pie' ? 12 : 60} rows; this file has ${rows.length}.` };
58
+ const quote = value => `"${String(value).replace(/["\\]/gu, '')}"`;
59
+ // A blank cell is missing data, not zero: Number('') is 0, so blanks are rejected before conversion.
60
+ const blank = value => value === undefined || value === null || String(value).trim() === '';
61
+ const numbers = column => rows.map(row => blank(row[column]) ? NaN : Number(String(row[column]).replace(/,/gu, '')));
62
+ for (const column of y) {
63
+ const bad = numbers(column).findIndex(value => !Number.isFinite(value));
64
+ if (bad < 0) continue;
65
+ if (blank(rows[bad][column])) return { error: `Column "${column}" is empty in data row ${bad + 1}. A blank is missing data, not zero: fill in the value, or leave the row out and say so in the caption.` };
66
+ return { error: `Column "${column}" has a non-numeric value in data row ${bad + 1}: ${rows[bad][column]}.` };
67
+ }
68
+ if (kind === 'pie') { const values = numbers(y[0]); return { text: `pie\n${rows.map((row, i) => ` ${quote(row[x])} : ${values[i]}`).join('\n')}` }; }
69
+ return { text: `xychart-beta\n x-axis [${rows.map(row => quote(row[x])).join(', ')}]\n${unit ? ` y-axis ${quote(unit)}\n` : ''}${y.map(column => ` ${kind} ${quote(column)} [${numbers(column).join(', ')}]`).join('\n')}` };
70
+ }
@@ -0,0 +1,168 @@
1
+ import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
2
+ import { createHash } from 'node:crypto';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { parseExplainer } from './parse.mjs';
9
+ import { computeExplainerRevision } from './revision.mjs';
10
+ import { unpackPageBundle } from './page-bundle.mjs';
11
+
12
+ export const isExplainerHtml = html => /<html\b[^>]*\bdata-archify-artifact\s*=\s*["']explainer["']/i.test(html) || (html.includes('archify-explainer-prepared') && scanHtml(html).scripts.some(s=>s.attrs.id==='archify-explainer-prepared'));
13
+ const hash = value => createHash('sha256').update(value).digest('hex');
14
+ const decode = value => value.replace(/&(?:amp|lt|gt|quot|apos|#39|#x[0-9a-f]+|#\d+);/gi, token => {
15
+ const names = {'&amp;':'&','&lt;':'<','&gt;':'>','&quot;':'"','&apos;':"'",'&#39;':"'"};
16
+ if (names[token.toLowerCase()]) return names[token.toLowerCase()];
17
+ const n = token[2].toLowerCase() === 'x' ? parseInt(token.slice(3,-1),16) : Number(token.slice(2,-1));
18
+ return n > 0 && n <= 0x10ffff ? String.fromCodePoint(n) : '\uFFFD';
19
+ });
20
+
21
+ // Bounded HTML token scanner: quoted attributes, comments, and raw-text elements
22
+ // are consumed together, so a JSON string containing markup is never a DOM ID.
23
+ export function scanHtml(html) {
24
+ const elements = [];
25
+ const scripts = [];
26
+ const token = /<!--[\s\S]*?(?:-->|$)|<![^>]*>|<(\/?)([a-z][\w:-]*)(?=[\s/>])(?:[^>"']|"[^"]*"|'[^']*')*>/gi;
27
+ let match, line = 1, lineOffset = 0;
28
+ while ((match = token.exec(html))) {
29
+ if (!match[2] || match[1]) continue;
30
+ const tag = match[2].toLowerCase();
31
+ const attrs = {};
32
+ const attr = /([^\s=<>"'`/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
33
+ const raw = match[0].slice(match[0].indexOf(match[2]) + match[2].length, -1);
34
+ let a;
35
+ while ((a = attr.exec(raw))) attrs[a[1].toLowerCase()] = decode(a[2] ?? a[3] ?? a[4] ?? '');
36
+ // Advance once through the input, including skipped raw text, instead of
37
+ // recounting every preceding line for each element of a large native SVG.
38
+ for (let newline = html.indexOf('\n', lineOffset); newline >= 0 && newline < match.index; newline = html.indexOf('\n', newline + 1)) line++;
39
+ lineOffset = match.index;
40
+ const element = {tag,attrs,line,index:match.index};
41
+ elements.push(element);
42
+ if (['script','style','textarea','title'].includes(tag)) {
43
+ const end = new RegExp(`</${tag}\\s*>`,'gi');
44
+ end.lastIndex = token.lastIndex;
45
+ const closing = end.exec(html);
46
+ const text = html.slice(token.lastIndex, closing?.index ?? html.length);
47
+ if (tag === 'script') scripts.push({...element,text});
48
+ token.lastIndex = closing ? end.lastIndex : html.length;
49
+ }
50
+ }
51
+ return {elements,scripts};
52
+ }
53
+
54
+ export function checkExplainerHtml(html, { file = '<html>', checkNative = true } = {}) {
55
+ const bytes = Buffer.from(html);
56
+ const {elements,scripts} = scanHtml(html);
57
+ const diagnostics = [];
58
+ const checks = [];
59
+ const issue = (code,reason,example,element,component='document') => diagnostics.push({
60
+ code:`explainer/${code}`,severity:'error',message:reason,
61
+ subject:{input:file,line:element?.line || 1,component,...(element?.attrs?.id ? {id:element.attrs.id} : {})},
62
+ evidence:{reason,example},supportedFixes:[example],
63
+ });
64
+ const add = (name, before, details=[]) => checks.push({name,ok:diagnostics.length===before,details});
65
+ let before = diagnostics.length;
66
+ if (elements.find(e=>e.tag==='html')?.attrs['data-archify-artifact']!=='explainer') issue('artifact-family','The document is missing its explainer family marker.','<html data-archify-artifact="explainer">');
67
+ const ids = new Map();
68
+ for (const e of elements) {
69
+ if (e.attrs.id) {
70
+ if (ids.has(e.attrs.id)) issue('duplicate-id',`Duplicate DOM ID: ${e.attrs.id}.`,'Give each panel, block, source and SVG preview a unique stable ID.',e);
71
+ ids.set(e.attrs.id,e);
72
+ }
73
+ if (Object.keys(e.attrs).some(key=>/^on/.test(key))) issue('inline-handler','Inline event attributes are not permitted.','Use the packaged reader event handlers.',e);
74
+ if (e.tag === 'script' && e.attrs.src) issue('external-runtime','The explainer runtime must be self-contained.','Embed the packaged reader script.',e);
75
+ if (['iframe','object','embed'].includes(e.tag)) issue('external-frame','Frames must be created from immutable native payloads on demand.','Keep a static native SVG preview and the frozen child HTML payload.',e);
76
+ for (const attr of ['href','src','xlink:href']) {
77
+ const value=e.attrs[attr];
78
+ if (!value) continue;
79
+ if (/^(?:javascript|vbscript):/i.test(value)) issue('unsafe-url','An executable URL is not allowed.','Use an https URL or a stable #anchor.',e);
80
+ if (value.startsWith('#') && !ids.has(decodeURIComponentSafe(value.slice(1)))) { /* resolved after all IDs */ }
81
+ }
82
+ if (e.tag==='img' && !/^data:image\/(?:png|jpeg|webp|gif);base64,/i.test(e.attrs.src || '')) issue('external-image','Images must contain their frozen local bytes.','Use a prepared local image with a data:image URL.',e,'image');
83
+ }
84
+ for (const e of elements) {
85
+ if (e.attrs.href?.startsWith('#') && e.attrs.href.length>1 && !ids.has(decodeURIComponentSafe(e.attrs.href.slice(1)))) issue('unresolved-reference',`Unresolved anchor: ${e.attrs.href}.`,'Link to an existing panel, block or source ID.',e,'reference');
86
+ }
87
+ add('explainer_structure',before,[`${ids.size} unique DOM IDs`]);
88
+ function payload(id) {
89
+ const found=scripts.filter(s=>s.attrs.id===id);
90
+ if (found.length!==1 || found[0].attrs.type!=='application/json') { issue('payload',`Expected one application/json payload: ${id}.`,`<script type="application/json" id="${id}">…</script>`,found[0]); return null; }
91
+ if (/[<>&\u2028\u2029]/.test(found[0].text)) issue('payload-escaping',`Unsafe raw characters in ${id}.`,'Escape <, >, &, U+2028 and U+2029 as JSON Unicode escapes.',found[0]);
92
+ try { return JSON.parse(found[0].text); } catch { issue('payload-json',`Invalid JSON payload: ${id}.`,'Re-render the validated source instead of editing embedded payloads.',found[0]); return null; }
93
+ }
94
+ before=diagnostics.length;
95
+ let bundle=payload('archify-explainer-prepared');
96
+ try { bundle=unpackPageBundle(bundle); } catch { issue('native-hash','A compressed native diagram payload cannot be inflated.','Re-render the native diagram and freeze its original HTML bytes.'); bundle=null; }
97
+ const document=bundle?.document;
98
+ const source=bundle ? {text:bundle.sourceText} : null;
99
+ const manifest=payload('explainer-manifest');
100
+ const children=bundle?.preparedDiagrams?.diagrams;
101
+ if (bundle && (bundle.kind!=='archify-explainer-prepared' || bundle.schemaVersion!==1 || hash(JSON.stringify({...bundle,digest:undefined}))!==bundle.digest)) issue('bundle-hash','The canonical prepared bundle does not match its digest.','Re-prepare and render the original draft with immutable resources.');
102
+ if (bundle && typeof bundle.sourceText==='string') {
103
+ const parsed=parseExplainer(bundle.sourceText,{inputPath:bundle.sourcePath,output:document?.meta?.output});
104
+ if (!parsed.document || parsed.diagnostics.some(d=>d.severity!=='warning') || JSON.stringify(parsed.document)!==JSON.stringify(document)) issue('source-document-binding','The canonical document differs from its embedded authored draft.','Re-parse the embedded draft and render its normalized document.');
105
+ if (bundle.source?.sha256!==hash(bundle.sourceText) || bundle.source?.bytes!==Buffer.byteLength(bundle.sourceText)) issue('bundle-source-hash','The prepared source bytes do not match their identity.','Freeze the exact original UTF-8 draft bytes.');
106
+ }
107
+ if (document && manifest && source) {
108
+ if (document.diagram_type!=='explainer' || document.schema_version!==1 || !Array.isArray(document.panels) || !Array.isArray(document.sources)) issue('canonical-document','The canonical explainer envelope is invalid.','Use schema_version:1, diagram_type:"explainer", panels:[] and sources:[].');
109
+ if (typeof source.text!=='string' || hash(source.text || '')!==manifest.sourceSha256) issue('source-hash','The embedded source does not match its immutable source hash.','Re-render the original draft and retain its exact SHA-256 source hash.');
110
+ try {
111
+ if (manifest.revision!==computeExplainerRevision(bundle)) issue('content-revision','The content revision does not bind the draft and frozen resources.','Recompute the revision from the canonical draft, source locations, child diagrams and contained assets.');
112
+ } catch {issue('content-revision','The content revision cannot be computed from invalid frozen resources.','Re-prepare the original draft and every declared resource.');}
113
+ if (hash(JSON.stringify(document))!==manifest.documentSha256) issue('document-hash','The canonical document does not match its manifest hash.','Re-render after changing the canonical document.');
114
+ if (manifest.artifactId!==document.meta?.id || elements.find(e=>e.tag==='html')?.attrs['data-revision']!==manifest.revision) issue('artifact-identity','Artifact ID or revision disagrees with the manifest.','Use the renderer-generated identity and revision.');
115
+ for (const panel of document.panels || []) {
116
+ for (const item of [panel,...(panel.blocks || [])]) if (!ids.has(item.id)) issue('canonical-id',`Canonical ID is absent from the rendered document: ${item.id}.`,'Render every authored panel and block with its stable ID.');
117
+ }
118
+ for (const src of document.sources || []) if (!ids.has(`source-${src.id}`)) issue('source-target',`Source target is missing: ${src.id}.`,'Render every declared source in the source list.');
119
+ }
120
+ add('explainer_payload_identity',before);
121
+ before=diagnostics.length;
122
+ const dependencies=bundle?.dependencies;
123
+ if (manifest?.dependencies && JSON.stringify(manifest.dependencies)!==JSON.stringify((dependencies || []).map(({contentBase64,...entry})=>entry))) issue('manifest-dependencies','The compact manifest disagrees with the canonical dependency identities.','Generate dependency metadata from the canonical prepared bundle.');
124
+ if (!Array.isArray(dependencies)) issue('dependency-manifest','The frozen dependency manifest is missing.','Store dependencies:[] even when no external resources are used.');
125
+ const depIds=new Set();
126
+ for (const dep of dependencies || []) {
127
+ const key=`${dep.kind}:${dep.id}`;
128
+ if (depIds.has(key)) issue('dependency-duplicate',`Duplicate dependency: ${key}.`,'Freeze each resource once.');
129
+ depIds.add(key);
130
+ const frozen=typeof dep.contentBase64==='string' ? Buffer.from(dep.contentBase64,'base64') : null;
131
+ if (!frozen || frozen.toString('base64')!==dep.contentBase64 || !Number.isSafeInteger(dep.bytes) || dep.bytes!==frozen.length || hash(frozen)!==dep.sha256) issue('dependency-hash',`Frozen dependency identity is corrupt: ${key}.`,'Regenerate the dependency bytes, byte count and SHA-256 from the source.');
132
+ }
133
+ for (const [id,asset] of Object.entries(manifest?.assets || {})) {
134
+ const dep=(dependencies || []).find(d=>d.kind==='image' && d.id===id);
135
+ if (!dep || dep.sha256!==asset.sha256 || dep.bytes!==asset.bytes) issue('asset-binding',`Image asset is not bound to its frozen dependency: ${id}.`,'Freeze the image before rendering.');
136
+ const img=elements.find(e=>e.tag==='img' && e.attrs.src===`data:${asset.mime};base64,${dep?.contentBase64}`);
137
+ if (!img) issue('asset-content',`Rendered image bytes differ from the frozen asset: ${id}.`,'Render the frozen data URL without replacing its bytes.');
138
+ }
139
+ for (const [id,src] of Object.entries(bundle?.preparedSources?.sources || {})) if (typeof src.text==='string') {
140
+ const dep=(dependencies || []).find(d=>d.kind==='repository' && d.id===id);
141
+ if (!dep || dep.sha256!==src.sha256 || dep.bytes!==src.bytes || !/^[0-9a-f]{40}$/.test(src.revision || '') || dep.revision!==src.revision) issue('source-binding',`Code excerpt is missing pinned frozen bytes: ${id}.`,'Resolve the repository source against a full Git commit before rendering.');
142
+ }
143
+ add('explainer_dependencies',before,[`${dependencies?.length || 0} frozen dependencies`]);
144
+ before=diagnostics.length;
145
+ const childReceipts=[];
146
+ const expected=(document?.panels || []).flatMap(p=>p.blocks || []).filter(b=>b.type==='diagram').map(b=>b.id);
147
+ if (!children || typeof children!=='object' || Array.isArray(children) || expected.length!==Object.keys(children).length || expected.some(id=>!children[id])) issue('native-count','Native diagram payloads do not match canonical diagram blocks.','Embed one native payload per diagram block; zero diagrams is valid.');
148
+ for (const [id,child] of Object.entries(children || {})) {
149
+ const binding=manifest?.children?.[id];
150
+ if (typeof child.html!=='string' || hash(child.html || '')!==child.sha256 || Buffer.byteLength(child.html || '')!==child.bytes || binding?.sha256!==child.sha256 || binding?.bytes!==child.bytes) { issue('native-hash',`Native diagram payload is corrupt: ${id}.`,'Re-render the native diagram and freeze its original HTML bytes.',ids.get(id),'diagram'); continue; }
151
+ if (isExplainerHtml(child.html)) {issue('native-family',`Nested explainers are not native diagrams: ${id}.`,'Use an existing Archify native diagram type.',ids.get(id),'diagram');continue;}
152
+ if (checkNative) {
153
+ const directory=fs.mkdtempSync(path.join(os.tmpdir(),'archify-explainer-check-'));
154
+ try {
155
+ const artifactPath=path.join(directory,'child.html'); fs.writeFileSync(artifactPath,child.html);
156
+ const run=spawnSync(process.execPath,runtimeArgs([fileURLToPath(new URL('../scripts/check-render-output.mjs',import.meta.url)),artifactPath]),{encoding:'utf8',maxBuffer:8*1024*1024,timeout:60000});
157
+ let receipt; try {receipt=JSON.parse(run.stdout);} catch {receipt=null;}
158
+ childReceipts.push({id,receipt});
159
+ if (run.status!==0 || !receipt?.ok || receipt.artifact?.sha256!==child.sha256 || receipt.composition?.profile!==(bundle?.quality || document?.meta?.quality_profile || 'standard')) issue('native-check',`Native diagram failed its existing composition checks: ${id}.`,'Fix the nested diagram using the native checker diagnostics.',ids.get(id),'diagram');
160
+ } finally {fs.rmSync(directory,{recursive:true,force:true});}
161
+ }
162
+ }
163
+ add('explainer_native_diagrams',before,childReceipts.map(({id,receipt})=>`${id}: ${receipt?.ok ? 'pass':'fail'}`));
164
+ const profile=bundle?.quality || document?.meta?.quality_profile || 'standard';
165
+ const errors=diagnostics.length;
166
+ return {ok:errors===0,file,family:'explainer',artifact:{sha256:hash(bytes),bytes:bytes.length},checks,composition:{schemaVersion:1,profile,status:errors ? 'fail':'pass',summary:{errors,warnings:0},issues:diagnostics,nativeDiagrams:childReceipts},diagnostics};
167
+ }
168
+ function decodeURIComponentSafe(value) { try {return decodeURIComponent(value);} catch {return value;} }