@stacksjs/defaults 0.75.361 → 0.75.366

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 (210) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +5 -5
  3. package/ai/skills/stacks-payments/SKILL.md +10 -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/ai/skills/stacks-testing/SKILL.md +8 -1
  209. package/ide/vscode/package.json +1 -1
  210. package/package.json +2 -2
@@ -1,31 +1,90 @@
1
+ import { softwareNoticesHtml } from './software-notices.mjs';
2
+ import { createHash } from 'node:crypto';
1
3
  import fs from 'node:fs';
2
4
  import path from 'node:path';
3
5
  import { applyTemplate, renderCards, esc } from './utils.mjs';
4
6
  import { validateSchema } from './validator.mjs';
5
7
  import { verifyRepositoryEvidence } from './repository-evidence.mjs';
6
- import { installRendererDiagnosticBoundary, throwDiagnosticProblems } from './diagnostics.mjs';
8
+ import { installRendererDiagnosticBoundary, throwDiagnosticError, throwDiagnosticProblems, recordDiagnostic } from './diagnostics.mjs';
7
9
  import { validateEngineeringProfile } from './engineering-profiles.mjs';
8
- import { resolveOutputPath } from './output-path.mjs';
10
+ import {
11
+ resolveOutputPath,
12
+ validateAuthoredOutputPath,
13
+ } from './output-path.mjs';
14
+ import {
15
+ captureAtomicOutput,
16
+ captureRegularFileBinding,
17
+ publishRegularFileBinding,
18
+ releaseRegularFileBinding,
19
+ removeOwnedRegularFile,
20
+ verifyAtomicOutput,
21
+ } from './atomic-output.mjs';
22
+ import { resolveLocale, translateMessage, registerLocale, localeDiagnostics } from './i18n.mjs';
9
23
  import { prepareDiagramBrandMarks } from './brand-marks.mjs';
10
- import { resolveLocale, translateMessage } from './i18n.mjs';
11
-
12
- installRendererDiagnosticBoundary();
13
24
 
14
25
  const outputPathGuards = new Map();
26
+ let renderCandidateSequence = 0;
27
+
28
+ // meta.locale is renderer-owned Viewer UI, not authored content. Each message
29
+ // resolves as: valid meta.translations value → bundled catalog enrolled in
30
+ // locales/manifest.json → English (see i18n.mjs). Every fallback is disclosed
31
+ // by human renderers on stderr. Diagnostic JSON mode records warnings without
32
+ // mixing text into failure receipts; validate/deliver disclose them publicly.
33
+ function applyLocaleTranslations(diagramType, diagram) {
34
+ const locale = diagram.meta?.locale;
35
+ if (!locale) return;
36
+ registerLocale(locale, diagram.meta.translations);
37
+ for (const diagnostic of localeDiagnostics(diagramType, diagram.meta)) {
38
+ recordDiagnostic(diagnostic);
39
+ if (process.env.ARCHIFY_DIAGNOSTIC_FORMAT !== 'json') {
40
+ console.warn(`archify: ${diagnostic.message}`);
41
+ }
42
+ }
43
+ }
15
44
 
16
45
  // Common CLI head: node render-<type>.mjs [input.json] [output.html]
17
46
  // Keep this synchronous because callers also use it to establish the guarded
18
47
  // output path before testing a last-moment filesystem alias change.
19
48
  export function loadDiagram({ rendererDir, diagramType, defaultExample, argv = process.argv }) {
49
+ // Compilers also import this module for SVG helpers. Only CLI execution
50
+ // should install a process-level handler, before reading or validating input.
51
+ installRendererDiagnosticBoundary();
20
52
  const skillRoot = path.resolve(rendererDir, '../..');
21
53
  const inputPath = path.resolve(argv[2] || path.join(skillRoot, 'examples', defaultExample));
22
- const diagram = JSON.parse(fs.readFileSync(inputPath, 'utf8'));
54
+ let input;
55
+ try {
56
+ input = fs.readFileSync(inputPath, 'utf8');
57
+ } catch (error) {
58
+ if (!isFilesystemError(error)) throw error;
59
+ const message = `Input could not be read: ${error.message}`;
60
+ throwDiagnosticError(message, [{
61
+ code: 'input/read', message,
62
+ subject: { input: inputPath },
63
+ evidence: { systemCode: error.code, reason: error.message },
64
+ supportedFixes: ['provide one readable JSON input file'],
65
+ }]);
66
+ }
67
+ let diagram;
68
+ try {
69
+ diagram = JSON.parse(input);
70
+ } catch (error) {
71
+ if (!(error instanceof SyntaxError)) throw error;
72
+ const message = `Input JSON could not be parsed: ${error.message}`;
73
+ throwDiagnosticError(message, [{
74
+ code: 'input/json-parse', message,
75
+ subject: { input: inputPath },
76
+ evidence: { reason: error.message },
77
+ supportedFixes: ['repair the JSON syntax and run validation again'],
78
+ }]);
79
+ }
80
+ const authoredOutput = diagram?.meta?.output;
81
+ if (authoredOutput !== undefined) validateAuthoredOutputPath(authoredOutput);
23
82
  validateSchema(diagramType, diagram);
24
- validateGuidedViews(diagramType, diagram);
25
- validateRelationshipIds(diagramType, diagram);
83
+ applyLocaleTranslations(diagramType, diagram);
84
+ validateCrossCollectionContracts(diagramType, diagram);
26
85
  validateEngineeringProfile(diagramType, diagram);
27
86
  const sourceEvidence = verifyRepositoryEvidence(diagramType, diagram, process.env.ARCHIFY_REPO_ROOT);
28
- const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8');
87
+ const template = fs.readFileSync(path.join(skillRoot, 'assets/template.html'), 'utf8').replace('</head>', `${softwareNoticesHtml}\n</head>`);
29
88
  const outputRequest = {
30
89
  requestedOutput: argv[3],
31
90
  authoredOutput: diagram.meta?.output,
@@ -33,7 +92,12 @@ export function loadDiagram({ rendererDir, diagramType, defaultExample, argv = p
33
92
  inputPaths: [inputPath],
34
93
  cwd: process.cwd(),
35
94
  };
36
- const { outputPath: outPath } = resolveOutputPath(outputRequest);
95
+ let outPath;
96
+ try {
97
+ ({ outputPath: outPath } = resolveOutputPath(outputRequest));
98
+ } catch (error) {
99
+ throwOutputError(error, path.resolve(outputRequest.requestedOutput || outputRequest.authoredOutput || outputRequest.defaultOutput));
100
+ }
37
101
  outputPathGuards.set(outPath, outputRequest);
38
102
  return { diagram, template, outPath, sourceEvidence };
39
103
  }
@@ -47,25 +111,194 @@ export async function loadDiagramWithBrandMarks(options) {
47
111
  return loaded;
48
112
  }
49
113
 
50
- const START_TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle']);
114
+ const START_TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'lifecycle', 'erd', 'tree', 'class', 'timeline', 'waterfall']);
115
+
116
+ function isFilesystemError(error) {
117
+ return typeof error?.code === 'string'
118
+ && typeof error?.syscall === 'string'
119
+ && typeof error?.errno === 'number';
120
+ }
121
+
122
+ function throwOutputError(error, output) {
123
+ if (error?.archifyDiagnostics || !isFilesystemError(error)) throw error;
124
+ const message = `Output could not be written: ${error.message}`;
125
+ throwDiagnosticError(message, [{
126
+ code: 'output/write', message,
127
+ subject: { output },
128
+ evidence: { systemCode: error.code, reason: error.message },
129
+ supportedFixes: ['choose a writable HTML file path and ensure its parent directories can be created'],
130
+ }]);
131
+ }
132
+
133
+ function throwAtomicOutputFailure(result, output) {
134
+ const reason = result.reason || { code: 'unclassified' };
135
+ const changed = result.status === 'different';
136
+ const candidate = reason.code.startsWith('candidate-');
137
+ const nonRegular = ['target-not-regular-file', 'candidate-not-regular-file'].includes(reason.code);
138
+ const hardlinked = ['target-hardlinked', 'candidate-hardlinked'].includes(reason.code);
139
+ const message = changed
140
+ ? 'Output target changed while the rendered artifact was being prepared.'
141
+ : nonRegular
142
+ ? candidate
143
+ ? 'Temporary output candidate is no longer a regular file.'
144
+ : 'Output already exists and is not a regular file.'
145
+ : hardlinked
146
+ ? candidate
147
+ ? 'Temporary output candidate has multiple hard-link names.'
148
+ : 'Output already exists through multiple hard-link names.'
149
+ : 'Output target stability could not be determined safely before commit.';
150
+ throwDiagnosticError(message, [{
151
+ code: changed
152
+ ? 'output/target-changed'
153
+ : nonRegular
154
+ ? 'output/target-not-regular-file'
155
+ : hardlinked
156
+ ? 'output/target-hardlinked'
157
+ : 'output/target-indeterminate',
158
+ message,
159
+ subject: { output },
160
+ evidence: { relation: reason },
161
+ supportedFixes: [hardlinked
162
+ ? 'choose a non-hardlinked output path; atomic replacement cannot update every hard-link name'
163
+ : 'retry after other processes stop replacing or redirecting the output path'],
164
+ }]);
165
+ }
166
+
167
+ function stageRenderedHtml(outputPath, html, mode) {
168
+ for (let attempt = 0; attempt < 100; attempt += 1) {
169
+ renderCandidateSequence += 1;
170
+ const candidatePath = path.join(
171
+ path.dirname(outputPath),
172
+ `.archify-render-${process.pid}-${Date.now().toString(36)}-${renderCandidateSequence}.tmp`,
173
+ );
174
+ let descriptor;
175
+ let identity;
176
+ try {
177
+ const noFollow = process.platform === 'win32' ? 0 : (fs.constants.O_NOFOLLOW || 0);
178
+ descriptor = fs.openSync(
179
+ candidatePath,
180
+ fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow,
181
+ mode ?? 0o666,
182
+ );
183
+ let metadata;
184
+ try {
185
+ metadata = fs.fstatSync(descriptor, { bigint: true });
186
+ } catch (error) {
187
+ // A transient first inspection failure must not strand the exclusive
188
+ // candidate. A successful retry binds cleanup to the still-open file;
189
+ // if both inspections fail, preserving the unknown entry is safer.
190
+ try {
191
+ const retry = fs.fstatSync(descriptor, { bigint: true });
192
+ if (retry.isFile() && retry.ino !== 0n) {
193
+ identity = { device: retry.dev, inode: retry.ino };
194
+ }
195
+ } catch {}
196
+ throw error;
197
+ }
198
+ if (!metadata.isFile() || metadata.ino === 0n) {
199
+ throw new Error('Temporary render candidate identity could not be verified safely.');
200
+ }
201
+ identity = { device: metadata.dev, inode: metadata.ino };
202
+ fs.writeFileSync(descriptor, html);
203
+ // Creation modes are filtered through the process umask. An atomic
204
+ // replacement must retain the exact permissions of an existing target,
205
+ // while a brand-new target should keep normal umask behavior.
206
+ if (mode !== null) fs.fchmodSync(descriptor, mode);
207
+ fs.closeSync(descriptor);
208
+ descriptor = undefined;
209
+ return { candidatePath, identity };
210
+ } catch (error) {
211
+ if (descriptor !== undefined) {
212
+ try { fs.closeSync(descriptor); } catch {}
213
+ }
214
+ if (error.code === 'EEXIST') continue;
215
+ if (identity) {
216
+ const cleanup = removeOwnedRegularFile(candidatePath, identity);
217
+ if (!['removed', 'absent', 'preserved'].includes(cleanup.status)) {
218
+ const cleanupError = new Error(`${error.message}; temporary render candidate cleanup also failed.`);
219
+ cleanupError.cause = error;
220
+ throw cleanupError;
221
+ }
222
+ }
223
+ throw error;
224
+ }
225
+ }
226
+ const error = new Error(`Could not reserve a temporary render candidate beside "${outputPath}".`);
227
+ error.code = 'EEXIST';
228
+ error.errno = -17;
229
+ error.syscall = 'open';
230
+ throw error;
231
+ }
51
232
 
52
233
  // Common CLI tail: fill the template and write the standalone HTML file.
53
234
  export function writeDiagram({ outPath, template, diagramType, meta, svg, cards, sourceEvidence = null }) {
54
235
  if (!START_TYPES.has(diagramType)) throw new Error(`writeDiagram: unknown diagram type ${JSON.stringify(diagramType)}`);
55
236
  const outputGuard = outputPathGuards.get(outPath);
56
- if (outputGuard) resolveOutputPath(outputGuard);
57
- fs.mkdirSync(path.dirname(outPath), { recursive: true });
58
- fs.writeFileSync(outPath, applyTemplate(template, {
237
+ const html = applyTemplate(template, {
59
238
  title: meta.title,
60
239
  subtitle: meta.subtitle,
61
240
  svg,
62
241
  cards: renderCards(cards),
63
242
  locale: meta.locale,
64
243
  visualPreset: meta.visual_preset || 'classic',
65
- guidedViews: meta.views || [],
66
244
  sourceEvidence,
67
- }));
68
- outputPathGuards.delete(outPath);
245
+ });
246
+ let candidatePath;
247
+ let candidateIdentity;
248
+ let candidateBinding;
249
+ try {
250
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
251
+ const outputCapture = captureAtomicOutput(outPath);
252
+ if (outputCapture.status !== 'captured') throwAtomicOutputFailure(outputCapture, outPath);
253
+ const beforeStage = verifyAtomicOutput(outputCapture.snapshot);
254
+ if (beforeStage.status !== 'match') throwAtomicOutputFailure(beforeStage, outPath);
255
+ ({ candidatePath, identity: candidateIdentity } = stageRenderedHtml(
256
+ outputCapture.commitPath,
257
+ html,
258
+ outputCapture.mode,
259
+ ));
260
+
261
+ // The renderer may spend substantial time building HTML after loadDiagram
262
+ // establishes the guard. Re-run it after staging so a last-moment alias
263
+ // cannot redirect the commit onto an input file.
264
+ if (outputGuard) resolveOutputPath(outputGuard);
265
+ const candidateCapture = captureRegularFileBinding(candidatePath, {
266
+ subject: 'candidate',
267
+ expectedSha256: createHash('sha256').update(html).digest('hex'),
268
+ expectedBytes: Buffer.byteLength(html),
269
+ expectedIdentity: candidateIdentity,
270
+ ...(outputCapture.mode === null ? {} : { expectedMode: outputCapture.mode }),
271
+ });
272
+ if (candidateCapture.status !== 'captured') throwAtomicOutputFailure(candidateCapture, outPath);
273
+ candidateBinding = candidateCapture.binding;
274
+ const beforeCommit = verifyAtomicOutput(outputCapture.snapshot);
275
+ if (beforeCommit.status !== 'match') throwAtomicOutputFailure(beforeCommit, outPath);
276
+ const publication = publishRegularFileBinding(
277
+ candidateBinding,
278
+ candidatePath,
279
+ outputCapture.snapshot,
280
+ { subject: 'candidate' },
281
+ );
282
+ if (!['committed', 'committed-with-warning'].includes(publication.status)) {
283
+ throwAtomicOutputFailure(publication, outPath);
284
+ }
285
+ const releasedCandidate = releaseRegularFileBinding(candidateBinding);
286
+ candidateBinding = undefined;
287
+ if (releasedCandidate.status !== 'released') throwAtomicOutputFailure(releasedCandidate, outPath);
288
+ candidatePath = undefined;
289
+ candidateIdentity = undefined;
290
+ } catch (error) {
291
+ throwOutputError(error, outPath);
292
+ } finally {
293
+ outputPathGuards.delete(outPath);
294
+ if (candidateBinding) releaseRegularFileBinding(candidateBinding);
295
+ if (candidatePath && candidateIdentity) {
296
+ const cleanup = removeOwnedRegularFile(candidatePath, candidateIdentity);
297
+ if (!['removed', 'absent', 'preserved'].includes(cleanup.status)) {
298
+ throwAtomicOutputFailure(cleanup, outPath);
299
+ }
300
+ }
301
+ }
69
302
  console.log(outPath);
70
303
  }
71
304
 
@@ -75,6 +308,11 @@ const SEMANTIC_COLLECTIONS = {
75
308
  sequence: 'participants',
76
309
  dataflow: 'nodes',
77
310
  lifecycle: 'states',
311
+ erd: 'entities',
312
+ tree: 'nodes',
313
+ class: 'types',
314
+ timeline: 'events',
315
+ waterfall: 'spans',
78
316
  };
79
317
 
80
318
  const RELATIONSHIP_COLLECTIONS = {
@@ -83,6 +321,8 @@ const RELATIONSHIP_COLLECTIONS = {
83
321
  sequence: 'messages',
84
322
  dataflow: 'flows',
85
323
  lifecycle: 'transitions',
324
+ erd: 'relationships',
325
+ class: 'relationships',
86
326
  };
87
327
 
88
328
  // Relationship IDs are optional for backwards compatibility, but once an
@@ -111,48 +351,21 @@ export function validateRelationshipIds(diagramType, diagram) {
111
351
  }
112
352
  }
113
353
 
114
- // JSON Schema keeps the view object bounded; this pass checks facts that span
115
- // collections. Keeping it here makes the same contract apply to all five
116
- // renderers, including the zero-install standalone-validator path.
117
- export function validateGuidedViews(diagramType, diagram) {
118
- const views = diagram.meta?.views;
119
- if (!Array.isArray(views) || views.length === 0) return;
120
- const collection = SEMANTIC_COLLECTIONS[diagramType];
121
- const semanticIds = new Set((diagram[collection] || []).map((item) => item.id));
122
- const seen = new Set();
123
- const problems = [];
124
-
125
- views.forEach((view, index) => {
126
- if (seen.has(view.id)) problems.push(`/meta/views/${index}/id duplicates view id ${JSON.stringify(view.id)}`);
127
- seen.add(view.id);
128
- const seenFocus = new Set();
129
- (view.focus || []).forEach((id, focusIndex) => {
130
- if (seenFocus.has(id)) {
131
- problems.push(`/meta/views/${index}/focus/${focusIndex} duplicates semantic id ${JSON.stringify(id)}`);
132
- }
133
- seenFocus.add(id);
134
- if (!semanticIds.has(id)) {
135
- problems.push(`/meta/views/${index}/focus/${focusIndex} references unknown semantic id ${JSON.stringify(id)}`);
136
- }
137
- });
138
- });
139
-
140
- if (problems.length) {
141
- throwDiagnosticProblems('Guided view validation failed', problems, {
142
- code: 'guided-view/invalid',
143
- subject: { diagramType, collection: 'meta.views' },
144
- });
145
- }
354
+ // Share relationship-ID semantic checks between the loader
355
+ // and workflow compiler without performing filesystem operations (see #429).
356
+ export function validateCrossCollectionContracts(diagramType, diagram) {
357
+ validateRelationshipIds(diagramType, diagram);
146
358
  }
147
359
 
148
360
  // Accessible name for the generated diagram SVG.
149
- export function svgRootAttrs(meta) {
150
- const animation = meta.animation === 'trace' ? ' data-animation="trace"' : '';
361
+ export function svgRootAttrs(meta, explicitQualityProfile) {
362
+ // Motion is a Viewer capability for every diagram; Live/Still controls playback.
363
+ const animation = ' data-animation="trace"';
151
364
  const preset = ` data-preset="${esc(meta.visual_preset || 'classic')}"`;
152
365
  const engineeringProfile = meta.engineering_profile
153
366
  ? ` data-engineering-profile="${esc(meta.engineering_profile)}"`
154
367
  : '';
155
- const requestedProfile = process.env.ARCHIFY_QUALITY_PROFILE || meta.quality_profile;
368
+ const requestedProfile = explicitQualityProfile || process.env.ARCHIFY_QUALITY_PROFILE || meta.quality_profile;
156
369
  const qualityProfile = requestedProfile === 'showcase' ? 'showcase' : 'standard';
157
370
  const advisory = requestedProfile ? '' : ' data-quality-gates="advisory"';
158
371
  return `role="img" lang="${esc(resolveLocale(meta.locale))}" aria-labelledby="archify-diagram-title archify-diagram-description"${animation}${preset}${engineeringProfile} data-quality-profile="${esc(qualityProfile)}"${advisory}`;
@@ -167,10 +380,9 @@ export function svgAccessibleText(meta, kind) {
167
380
  }
168
381
 
169
382
  export function animateAttr(meta, kind, step) {
170
- if (meta.animation !== 'trace') return '';
171
383
  // Ambient trace must finish inside the fixed six-second WebM capture. The
172
384
  // cap affects visual delay only; authored order and semantic identity stay
173
- // untouched in the JSON, DOM, Story, and relationship contracts.
385
+ // untouched in the JSON, DOM, and relationship contracts.
174
386
  const safeStep = Number.isFinite(step) && step >= 0 ? Math.min(12, Math.floor(step)) : 0;
175
387
  return ` data-animate="${kind}" style="--step:${safeStep}"`;
176
388
  }
@@ -3,6 +3,11 @@ export const DESKTOP_READER_MIN_WIDTH = 960;
3
3
  export const DESKTOP_READER_HORIZONTAL_CHROME = 30;
4
4
  export const DESKTOP_READER_DIAGRAM_WIDTH = DESKTOP_READER_MIN_WIDTH - DESKTOP_READER_HORIZONTAL_CHROME;
5
5
  export const MIN_PROJECTED_NODE_TEXT_PX = 6;
6
+ export const DECLARED_WIDE_READER_CONTRACT = 'declared-wide-v1';
7
+ export const DECLARED_WIDE_READER_RATIO = 1.55;
8
+ export const DECLARED_WIDE_READER_MAX_WIDTH = 1920;
9
+ export const DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX = 64;
10
+ export const DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX = 30;
6
11
 
7
12
  export function projectedNodeTextPx(sourceFontPx, viewBoxWidth, diagramWidth = DESKTOP_READER_DIAGRAM_WIDTH) {
8
13
  if (![sourceFontPx, viewBoxWidth, diagramWidth].every(Number.isFinite) || viewBoxWidth <= 0 || diagramWidth <= 0) {
@@ -24,3 +29,104 @@ export function minimumReadableSourceTextPx(
24
29
  }
25
30
  return minimumProjectedPx / Math.min(1, diagramWidth / viewBoxWidth);
26
31
  }
32
+
33
+ // This is deliberately separate from the legacy 930px projection. Architecture
34
+ // boundary convergence depends on that legacy default, while only a recognized
35
+ // v2 wide Reader may use this declared-width proof.
36
+ export function declaredWideReadabilityBudget({
37
+ viewBoxWidth,
38
+ viewBoxHeight,
39
+ minimumSourceTextPx,
40
+ requestedMinimumTextPx,
41
+ viewportWidth = DESKTOP_READABILITY_VIEWPORT.width,
42
+ bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX,
43
+ diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX,
44
+ minimumReaderWidth = DESKTOP_READER_MIN_WIDTH,
45
+ maximumReaderWidth = DECLARED_WIDE_READER_MAX_WIDTH,
46
+ } = {}) {
47
+ const values = [
48
+ viewBoxWidth, viewBoxHeight, minimumSourceTextPx, requestedMinimumTextPx,
49
+ viewportWidth, bodyHorizontalPx, diagramHorizontalPx, minimumReaderWidth, maximumReaderWidth,
50
+ ];
51
+ if (!values.every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0
52
+ || minimumSourceTextPx <= 0 || requestedMinimumTextPx <= 0 || viewportWidth <= 0
53
+ || bodyHorizontalPx < 0 || diagramHorizontalPx < 0 || minimumReaderWidth <= 0
54
+ || maximumReaderWidth < minimumReaderWidth || viewBoxWidth / viewBoxHeight < DECLARED_WIDE_READER_RATIO) {
55
+ return null;
56
+ }
57
+ const requestedTargetPx = Math.max(MIN_PROJECTED_NODE_TEXT_PX, requestedMinimumTextPx);
58
+ const requestedScale = Math.min(1, requestedTargetPx / minimumSourceTextPx);
59
+ const desiredReaderWidth = Math.max(minimumReaderWidth, viewBoxWidth * requestedScale + diagramHorizontalPx);
60
+ const viewportCap = Math.max(0, viewportWidth - bodyHorizontalPx);
61
+ const cap = Math.min(maximumReaderWidth, viewportCap);
62
+ const actualReaderWidth = Math.min(desiredReaderWidth, cap);
63
+ const guaranteedSvgWidth = Math.max(0, actualReaderWidth - diagramHorizontalPx);
64
+ const projectedMinimumTextPx = projectedNodeTextPx(minimumSourceTextPx, viewBoxWidth, guaranteedSvgWidth);
65
+ const limit = actualReaderWidth < desiredReaderWidth
66
+ ? (viewportCap <= maximumReaderWidth ? 'viewport-cap' : 'reader-cap')
67
+ : 'source-size';
68
+ return {
69
+ requestedTargetPx,
70
+ requestedScale,
71
+ desiredReaderWidth,
72
+ viewportCap,
73
+ maximumReaderWidth,
74
+ actualReaderWidth,
75
+ guaranteedSvgWidth,
76
+ projectedMinimumTextPx,
77
+ hardFloorPx: MIN_PROJECTED_NODE_TEXT_PX,
78
+ hardFloorMet: projectedMinimumTextPx >= MIN_PROJECTED_NODE_TEXT_PX,
79
+ requestedTargetMet: projectedMinimumTextPx >= requestedMinimumTextPx,
80
+ limit,
81
+ };
82
+ }
83
+
84
+ // Vertical chrome that always stacks with the SVG at the 1440x900 desktop
85
+ // viewport, measured from the delivered Viewer with the shortest one-line
86
+ // header and no cards: body padding 12, header 39, diagram padding/border 75.
87
+ // Cards are excluded so the prediction stays a lower bound.
88
+ export const DESKTOP_FIXED_VERTICAL_CHROME_PX = Object.freeze({ body: 12, header: 39, diagram: 75 });
89
+
90
+ // A canvas the Reader can neither narrow (viewBox ratio below the wide
91
+ // threshold) nor scroll readably (no automatic Reader fit) renders at the full
92
+ // reader width, so its page height is a function of the viewBox alone. Returns
93
+ // null when the Reader has a way to fit the page; otherwise the certain
94
+ // overflow at 1440x900 before any cards are counted.
95
+ export function predictedFixedWidthOverflow({
96
+ viewBoxWidth,
97
+ viewBoxHeight,
98
+ readerFit,
99
+ diagramType,
100
+ viewport = DESKTOP_READABILITY_VIEWPORT,
101
+ bodyHorizontalPx = DECLARED_WIDE_REFERENCE_BODY_HORIZONTAL_PX,
102
+ diagramHorizontalPx = DECLARED_WIDE_REFERENCE_DIAGRAM_HORIZONTAL_PX,
103
+ chrome = DESKTOP_FIXED_VERTICAL_CHROME_PX,
104
+ } = {}) {
105
+ if (![viewBoxWidth, viewBoxHeight].every(Number.isFinite) || viewBoxWidth <= 0 || viewBoxHeight <= 0) return null;
106
+ const ratio = viewBoxWidth / viewBoxHeight;
107
+ if (readerFit === 'intrinsic-height' || readerFit === 'width-first'
108
+ || (readerFit === 'authored-height' && diagramType === 'architecture')
109
+ || ratio >= DECLARED_WIDE_READER_RATIO) return null;
110
+ const svgWidthPx = viewport.width - bodyHorizontalPx - diagramHorizontalPx;
111
+ const svgHeightPx = Math.round(svgWidthPx * viewBoxHeight / viewBoxWidth);
112
+ const fixedChromePx = chrome.body + chrome.header + chrome.diagram;
113
+ const pageHeightPx = svgHeightPx + fixedChromePx;
114
+ if (pageHeightPx <= viewport.height) return null;
115
+ return {
116
+ viewportWidth: viewport.width,
117
+ viewportHeight: viewport.height,
118
+ ratio: Math.round(ratio * 100) / 100,
119
+ wideRatio: DECLARED_WIDE_READER_RATIO,
120
+ svgWidthPx,
121
+ svgHeightPx,
122
+ fixedChromePx,
123
+ pageHeightPx,
124
+ overflowPx: pageHeightPx - viewport.height,
125
+ };
126
+ }
127
+
128
+ export function describeFixedWidthOverflow(issue) {
129
+ const maximumViewBoxHeight = Math.floor(issue.viewBoxWidth / issue.wideRatio);
130
+ const wideViewBoxWidth = Math.ceil(issue.viewBoxHeight * issue.wideRatio);
131
+ return `Preserve every node, relationship, and label. This ${issue.viewBoxWidth}x${issue.viewBoxHeight} canvas (ratio ${issue.ratio}) declares no intrinsic-height fit and is below the ${issue.wideRatio} wide ratio, so the desktop Reader can neither narrow it nor accept vertical scroll: it renders ${issue.svgHeightPx}px tall at the full ${issue.svgWidthPx}px width and the page reaches ${issue.pageHeightPx}px before cards against ${issue.viewportHeight}px, a certain visual-check failure. Either compact vertical spacing so meta.viewBox height is at most ${maximumViewBoxHeight} at this width, or spread content sideways so the width is at least ${wideViewBoxWidth} at this height; for architecture and readable-v2 workflow, omitting meta.viewBox lets the renderer size the canvas and declare the fit.`;
132
+ }