@stacksjs/defaults 0.75.361 → 0.75.362

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/ai/skills/stacks-ai/SKILL.md +22 -0
  2. package/ai/skills/stacks-native/CATALOG.md +2 -2
  3. package/ai/skills/stacks-technical-diagrams/LICENSE +202 -22
  4. package/ai/skills/stacks-technical-diagrams/LICENSE-MIT +22 -0
  5. package/ai/skills/stacks-technical-diagrams/NOTICE +26 -0
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +106 -266
  7. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +11 -2
  8. package/ai/skills/stacks-technical-diagrams/UPSTREAM.json +12 -0
  9. package/ai/skills/stacks-technical-diagrams/agents/openai.yaml +2 -2
  10. package/ai/skills/stacks-technical-diagrams/assets/template.html +3655 -3798
  11. package/ai/skills/stacks-technical-diagrams/bin/archify.mjs +7290 -0
  12. package/ai/skills/stacks-technical-diagrams/bin/delivery-update.mjs +7 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/diagrams +1 -0
  14. package/ai/skills/stacks-technical-diagrams/bin/finalize.mjs +1056 -0
  15. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +72 -12
  16. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +348 -31
  17. package/ai/skills/stacks-technical-diagrams/bin/recover-output.mjs +41 -0
  18. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +3 -2097
  19. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +2142 -157
  20. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +3 -3
  21. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +130 -33
  22. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +25 -30
  23. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +0 -20
  24. package/ai/skills/stacks-technical-diagrams/examples/archify-dev-activity.timeline.json +176 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/archify-repository.tree.json +197 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +1 -6
  27. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +3 -16
  28. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +2 -7
  29. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +2 -1
  30. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +2 -1
  31. package/ai/skills/stacks-technical-diagrams/examples/checkout-request.waterfall.json +20 -0
  32. package/ai/skills/stacks-technical-diagrams/examples/class-payment-processors-rendered.html +14987 -0
  33. package/ai/skills/stacks-technical-diagrams/examples/class-payments-rendered.html +14787 -0
  34. package/ai/skills/stacks-technical-diagrams/examples/dataflow-product-analytics.html +14880 -0
  35. package/ai/skills/stacks-technical-diagrams/examples/delivery.explainer.json +155 -0
  36. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +57 -34
  37. package/ai/skills/stacks-technical-diagrams/examples/erd-orders-rendered.html +15112 -0
  38. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +1 -6
  39. package/ai/skills/stacks-technical-diagrams/examples/example-rebuild.waterfall.json +212 -0
  40. package/ai/skills/stacks-technical-diagrams/examples/holiday-planning.workflow.json +143 -0
  41. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +1 -6
  42. package/ai/skills/stacks-technical-diagrams/examples/lifecycle-agent-run.html +14892 -0
  43. package/ai/skills/stacks-technical-diagrams/examples/locales/de.json +463 -0
  44. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.json +463 -0
  45. package/ai/skills/stacks-technical-diagrams/examples/locales/fr.partial.json +22 -0
  46. package/ai/skills/stacks-technical-diagrams/examples/locales/it.json +463 -0
  47. package/ai/skills/stacks-technical-diagrams/examples/locales/ja.json +463 -0
  48. package/ai/skills/stacks-technical-diagrams/examples/locales/pt.json +463 -0
  49. package/ai/skills/stacks-technical-diagrams/examples/locales/ru.json +463 -0
  50. package/ai/skills/stacks-technical-diagrams/examples/orders.erd.json +337 -0
  51. package/ai/skills/stacks-technical-diagrams/examples/payment-incident.timeline.json +24 -0
  52. package/ai/skills/stacks-technical-diagrams/examples/payment-platform.tree.json +22 -0
  53. package/ai/skills/stacks-technical-diagrams/examples/payment-processors.class.json +177 -0
  54. package/ai/skills/stacks-technical-diagrams/examples/payments.class.json +85 -0
  55. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +1 -6
  56. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +3 -23
  57. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +1 -6
  58. package/ai/skills/stacks-technical-diagrams/examples/sequence-cache-miss-request.html +14895 -0
  59. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing-rendered.html +15369 -0
  60. package/ai/skills/stacks-technical-diagrams/examples/subscription-billing.erd.json +548 -0
  61. package/ai/skills/stacks-technical-diagrams/examples/timeline-archify-dev-activity-rendered.html +15002 -0
  62. package/ai/skills/stacks-technical-diagrams/examples/timeline-payment-incident-rendered.html +14772 -0
  63. package/ai/skills/stacks-technical-diagrams/examples/tree-archify-repository-rendered.html +15071 -0
  64. package/ai/skills/stacks-technical-diagrams/examples/tree-payment-platform-rendered.html +14810 -0
  65. package/ai/skills/stacks-technical-diagrams/examples/waterfall-checkout-request-rendered.html +14754 -0
  66. package/ai/skills/stacks-technical-diagrams/examples/waterfall-example-rebuild-rendered.html +14940 -0
  67. package/ai/skills/stacks-technical-diagrams/examples/web-app-rendered.html +14889 -0
  68. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +1 -6
  69. package/ai/skills/stacks-technical-diagrams/examples/workflow-agent-tool-call-rendered.html +14907 -0
  70. package/ai/skills/stacks-technical-diagrams/explainer/browser-check.mjs +208 -0
  71. package/ai/skills/stacks-technical-diagrams/explainer/charts.mjs +70 -0
  72. package/ai/skills/stacks-technical-diagrams/explainer/check.mjs +168 -0
  73. package/ai/skills/stacks-technical-diagrams/explainer/diagrams.mjs +518 -0
  74. package/ai/skills/stacks-technical-diagrams/explainer/diff.mjs +124 -0
  75. package/ai/skills/stacks-technical-diagrams/explainer/inline.mjs +155 -0
  76. package/ai/skills/stacks-technical-diagrams/explainer/layout.mjs +35 -0
  77. package/ai/skills/stacks-technical-diagrams/explainer/page-bundle.mjs +16 -0
  78. package/ai/skills/stacks-technical-diagrams/explainer/parse.mjs +300 -0
  79. package/ai/skills/stacks-technical-diagrams/explainer/patch.mjs +117 -0
  80. package/ai/skills/stacks-technical-diagrams/explainer/prepare.mjs +131 -0
  81. package/ai/skills/stacks-technical-diagrams/explainer/reader.css +472 -0
  82. package/ai/skills/stacks-technical-diagrams/explainer/reader.js +667 -0
  83. package/ai/skills/stacks-technical-diagrams/explainer/render.mjs +224 -0
  84. package/ai/skills/stacks-technical-diagrams/explainer/revision.mjs +20 -0
  85. package/ai/skills/stacks-technical-diagrams/explainer/sources.mjs +318 -0
  86. package/ai/skills/stacks-technical-diagrams/explainer/validate.mjs +138 -0
  87. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/LICENSE +40 -0
  88. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/README.md +5 -0
  89. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/3DKXF73K.js +4 -0
  90. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/42UTT654.js +3 -0
  91. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/DKTLEL5E.js +1 -0
  92. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EEIX5ZYH.js +2 -0
  93. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/EJ5KOXYD.js +25 -0
  94. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/GUJTEFZK.js +1 -0
  95. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/IZOKQ2GT.js +4 -0
  96. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/L4LY553T.js +3 -0
  97. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/MSKIJHB2.js +2 -0
  98. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/RVARI57O.js +1 -0
  99. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UKSIWDDN.js +2 -0
  100. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/UPTXNAXN.js +2 -0
  101. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/XTDUIGUI.js +9 -0
  102. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/chunks/YAYTP6L5.js +9 -0
  103. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/core.js +1 -0
  104. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/pie.js +1 -0
  105. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/quadrantChart.js +1 -0
  106. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/radar.js +1 -0
  107. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/sankey.js +1 -0
  108. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/venn.js +1 -0
  109. package/ai/skills/stacks-technical-diagrams/explainer/vendor/pele/diagrams/xychart.js +1 -0
  110. package/ai/skills/stacks-technical-diagrams/explainer/writing.mjs +139 -0
  111. package/ai/skills/stacks-technical-diagrams/locales/README.md +40 -0
  112. package/ai/skills/stacks-technical-diagrams/locales/ar.json +488 -0
  113. package/ai/skills/stacks-technical-diagrams/locales/en.json +488 -0
  114. package/ai/skills/stacks-technical-diagrams/locales/es.json +488 -0
  115. package/ai/skills/stacks-technical-diagrams/locales/ko.json +488 -0
  116. package/ai/skills/stacks-technical-diagrams/locales/manifest.json +10 -0
  117. package/ai/skills/stacks-technical-diagrams/locales/zh-CN.json +488 -0
  118. package/ai/skills/stacks-technical-diagrams/locales/zh-TW.json +488 -0
  119. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +1 -0
  120. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +115 -51
  121. package/ai/skills/stacks-technical-diagrams/references/architecture-layout-repair.md +18 -0
  122. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +453 -51
  123. package/ai/skills/stacks-technical-diagrams/references/authoring-defaults.md +57 -0
  124. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +57 -3
  125. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +603 -58
  126. package/ai/skills/stacks-technical-diagrams/references/explainer-authoring.md +274 -0
  127. package/ai/skills/stacks-technical-diagrams/references/repository-authoring.md +126 -0
  128. package/ai/skills/stacks-technical-diagrams/references/update-awareness.md +13 -0
  129. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +18 -11
  130. package/ai/skills/stacks-technical-diagrams/renderers/architecture/grid.mjs +1 -1
  131. package/ai/skills/stacks-technical-diagrams/renderers/architecture/labels.mjs +2 -0
  132. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +550 -424
  133. package/ai/skills/stacks-technical-diagrams/renderers/architecture/routing.mjs +1469 -0
  134. package/ai/skills/stacks-technical-diagrams/renderers/class/README.md +129 -0
  135. package/ai/skills/stacks-technical-diagrams/renderers/class/render-class.mjs +1095 -0
  136. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +89 -17
  137. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/dataflow-compiler.mjs +1605 -0
  138. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/joint-label-routing.mjs +165 -0
  139. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/label-capacity.mjs +81 -0
  140. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +14 -474
  141. package/ai/skills/stacks-technical-diagrams/renderers/erd/README.md +246 -0
  142. package/ai/skills/stacks-technical-diagrams/renderers/erd/grid.mjs +150 -0
  143. package/ai/skills/stacks-technical-diagrams/renderers/erd/render-erd.mjs +1588 -0
  144. package/ai/skills/stacks-technical-diagrams/renderers/explainer/render-explainer.mjs +42 -0
  145. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +83 -94
  146. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +990 -417
  147. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +49 -17
  148. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +735 -94
  149. package/ai/skills/stacks-technical-diagrams/renderers/shared/atomic-output.mjs +1982 -0
  150. package/ai/skills/stacks-technical-diagrams/renderers/shared/automatic-labels.mjs +290 -0
  151. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +169 -26
  152. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +266 -54
  153. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +106 -0
  154. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +0 -0
  155. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +268 -2
  156. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +774 -124
  157. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +289 -556
  158. package/ai/skills/stacks-technical-diagrams/renderers/shared/layout-report.mjs +18 -1
  159. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +16 -6
  160. package/ai/skills/stacks-technical-diagrams/renderers/shared/node-card-style.mjs +32 -0
  161. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +329 -229
  162. package/ai/skills/stacks-technical-diagrams/renderers/shared/owned-temp-directory.mjs +82 -0
  163. package/ai/skills/stacks-technical-diagrams/renderers/shared/path-semantics.mjs +1017 -0
  164. package/ai/skills/stacks-technical-diagrams/renderers/shared/portable-path.mjs +408 -0
  165. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +235 -79
  166. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +17 -4
  167. package/ai/skills/stacks-technical-diagrams/renderers/shared/route-quality.mjs +670 -0
  168. package/ai/skills/stacks-technical-diagrams/renderers/shared/sidecar-path.mjs +38 -0
  169. package/ai/skills/stacks-technical-diagrams/renderers/shared/software-notices.mjs +6 -0
  170. package/ai/skills/stacks-technical-diagrams/renderers/shared/spatial-grid.mjs +66 -0
  171. package/ai/skills/stacks-technical-diagrams/renderers/shared/svg-path-analysis.mjs +135 -0
  172. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +59 -3
  173. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +63 -35
  174. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +2 -5
  175. package/ai/skills/stacks-technical-diagrams/renderers/timeline/README.md +76 -0
  176. package/ai/skills/stacks-technical-diagrams/renderers/timeline/render-timeline.mjs +576 -0
  177. package/ai/skills/stacks-technical-diagrams/renderers/tree/README.md +96 -0
  178. package/ai/skills/stacks-technical-diagrams/renderers/tree/render-tree.mjs +573 -0
  179. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/README.md +73 -0
  180. package/ai/skills/stacks-technical-diagrams/renderers/waterfall/render-waterfall.mjs +413 -0
  181. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -23
  182. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +18 -6
  183. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +2230 -710
  184. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +17 -0
  185. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-placement-repair.mjs +94 -0
  186. package/ai/skills/stacks-technical-diagrams/schemas/README.md +87 -40
  187. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +7 -33
  188. package/ai/skills/stacks-technical-diagrams/schemas/class.schema.json +345 -0
  189. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +68 -2
  190. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +14 -3
  191. package/ai/skills/stacks-technical-diagrams/schemas/erd.schema.json +381 -0
  192. package/ai/skills/stacks-technical-diagrams/schemas/explainer.schema.json +927 -0
  193. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +23 -110
  194. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +15 -4
  195. package/ai/skills/stacks-technical-diagrams/schemas/timeline.schema.json +160 -0
  196. package/ai/skills/stacks-technical-diagrams/schemas/tree.schema.json +141 -0
  197. package/ai/skills/stacks-technical-diagrams/schemas/waterfall.schema.json +147 -0
  198. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +14 -3
  199. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +876 -243
  200. package/ai/skills/stacks-technical-diagrams/scripts/check-update.mjs +1717 -0
  201. package/ai/skills/stacks-technical-diagrams/scripts/delivery-update-child.mjs +26 -0
  202. package/ai/skills/stacks-technical-diagrams/scripts/generate-brand-marks.mjs +141 -0
  203. package/ai/skills/stacks-technical-diagrams/scripts/generate-validators.mjs +105 -0
  204. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +11 -1
  205. package/ai/skills/stacks-technical-diagrams/scripts/update-contract.mjs +187 -0
  206. package/ai/skills/stacks-technical-diagrams/skill-release.json +10 -0
  207. package/ide/vscode/package.json +1 -1
  208. package/package.json +2 -2
@@ -1,236 +1,359 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
2
+ import {
3
+ containedBy,
4
+ isValidWindowsSmbShareName,
5
+ isWindowsIpcShare,
6
+ resolvePhysicalLocation,
7
+ sameLocation,
8
+ } from './path-semantics.mjs';
9
+ import { PortablePathError, validatePortablePath } from './portable-path.mjs';
3
10
 
4
- const MAX_SYMLINK_DEPTH = 64;
5
- const directorySemanticsCache = new Map();
6
- let semanticsProbeSequence = 0;
7
-
8
- function splitAbsolute(absolutePath) {
9
- const root = path.parse(absolutePath).root;
10
- return {
11
- root,
12
- segments: absolutePath.slice(root.length).split(path.sep).filter(Boolean),
13
- };
14
- }
15
-
16
- function canonicalize(targetPath, depth) {
17
- const absolutePath = path.resolve(targetPath);
18
- const { root, segments } = splitAbsolute(absolutePath);
19
- let current = root;
20
-
21
- for (let index = 0; index < segments.length; index += 1) {
22
- const candidate = path.join(current, segments[index]);
23
- let stat;
24
- try {
25
- stat = fs.lstatSync(candidate);
26
- } catch (error) {
27
- if (error.code === 'ENOENT' || error.code === 'ENOTDIR') {
28
- return path.resolve(current, ...segments.slice(index));
29
- }
30
- throw error;
31
- }
32
-
33
- if (stat.isSymbolicLink()) {
34
- if (depth >= MAX_SYMLINK_DEPTH) {
35
- const error = new Error(`Could not resolve path because a symbolic-link cycle includes "${candidate}".`);
36
- error.code = 'ELOOP';
37
- error.path = candidate;
38
- throw error;
39
- }
40
- const link = fs.readlinkSync(candidate);
41
- const linkTarget = path.isAbsolute(link) ? link : path.resolve(path.dirname(candidate), link);
42
- return canonicalize(path.join(linkTarget, ...segments.slice(index + 1)), depth + 1);
43
- }
44
-
45
- current = fs.realpathSync.native(candidate);
11
+ export function canonicalFuturePath(targetPath) {
12
+ const resolution = resolvePhysicalLocation(targetPath);
13
+ if (resolution.status === 'resolved') {
14
+ if (resolution.location.kind === 'existing') return resolution.location.path;
15
+ return path.join(
16
+ resolution.location.ancestorPath,
17
+ ...resolution.location.unresolved,
18
+ );
46
19
  }
47
-
48
- return path.normalize(current);
20
+ const output = path.resolve(targetPath);
21
+ if (resolution.reason.code === 'symlink-cycle') {
22
+ throw new OutputPathError(`Output path contains a symbolic-link cycle: "${output}".`, {
23
+ code: 'output/symlink-cycle',
24
+ message: 'Output path could not be resolved because it contains a symbolic-link cycle.',
25
+ subject: { output },
26
+ evidence: { relation: resolution.reason },
27
+ supportedFixes: ['remove the symbolic-link cycle or choose an output path outside it'],
28
+ });
29
+ }
30
+ throw new OutputPathError('Output path could not be resolved safely.', {
31
+ code: 'output/path-resolution-indeterminate',
32
+ message: 'Output path could not be resolved safely for the requested filesystem location.',
33
+ subject: { output },
34
+ evidence: { relation: resolution.reason },
35
+ supportedFixes: ['use an ordinary local filesystem path that can be resolved safely, then retry'],
36
+ });
49
37
  }
50
38
 
51
- export function canonicalFuturePath(targetPath) {
52
- try {
53
- return canonicalize(targetPath, 0);
54
- } catch (error) {
55
- if (error?.code !== 'ELOOP') throw error;
56
- const output = path.resolve(targetPath);
39
+ export function pathsAlias(leftPath, rightPath) {
40
+ const relation = sameLocation(leftPath, rightPath);
41
+ if (relation.status === 'match') return true;
42
+ if (relation.status === 'different') return false;
43
+ if (relation.reason.code === 'ancestor-not-directory') return false;
44
+ if (relation.reason.code === 'symlink-cycle') {
45
+ const output = path.resolve(relation.reason.side === 'right' ? rightPath : leftPath);
57
46
  throw new OutputPathError(`Output path contains a symbolic-link cycle: "${output}".`, {
58
47
  code: 'output/symlink-cycle',
59
48
  message: 'Output path could not be resolved because it contains a symbolic-link cycle.',
60
49
  subject: { output },
61
- evidence: {
62
- systemCode: 'ELOOP',
63
- ...(error.path ? { cycleAt: path.resolve(error.path) } : {}),
64
- },
50
+ evidence: { relation: relation.reason },
65
51
  supportedFixes: ['remove the symbolic-link cycle or choose an output path outside it'],
66
52
  });
67
53
  }
54
+ throw new OutputPathError('Path identity could not be determined safely.', {
55
+ code: 'output/path-identity-indeterminate',
56
+ message: 'Path identity could not be determined safely for the requested filesystem location.',
57
+ subject: { left: path.resolve(leftPath), right: path.resolve(rightPath) },
58
+ evidence: { relation: relation.reason },
59
+ supportedFixes: ['use ordinary local filesystem paths whose identity can be verified, then retry'],
60
+ });
68
61
  }
69
62
 
70
- function hasFileIdentity(stat) {
71
- return stat.ino !== 0 && stat.ino !== 0n;
63
+ function pathIsInside(directoryPath, targetPath) {
64
+ const relation = containedBy(directoryPath, targetPath);
65
+ if (relation.status === 'match') return true;
66
+ if (relation.status === 'different') return false;
67
+ throw new OutputPathError('Output containment could not be determined safely.', {
68
+ code: 'output/containment-indeterminate',
69
+ message: 'Output containment could not be determined safely for the requested filesystem location.',
70
+ subject: { output: path.resolve(targetPath), cwd: path.resolve(directoryPath) },
71
+ evidence: { relation: relation.reason },
72
+ supportedFixes: ['use an output beneath an ordinary local directory whose identity can be verified'],
73
+ });
72
74
  }
73
75
 
74
- function sameFileIdentity(left, right) {
75
- return hasFileIdentity(left)
76
- && hasFileIdentity(right)
77
- && left.dev === right.dev
78
- && left.ino === right.ino;
76
+ function authoredOutputDiagnostic(error, rawOutput) {
77
+ const absolute = error?.reason === 'absolute';
78
+ const code = absolute ? 'output/meta-absolute' : 'output/meta-path-syntax';
79
+ const message = absolute
80
+ ? 'meta.output must be a relative path resolved from the current working directory.'
81
+ : 'meta.output must be a portable POSIX-relative path.';
82
+ return {
83
+ code,
84
+ message,
85
+ subject: { output: rawOutput, path: '/meta/output' },
86
+ evidence: {
87
+ reason: error?.reason || 'invalid',
88
+ ...(error?.segmentIndex !== undefined ? { segmentIndex: error.segmentIndex } : {}),
89
+ ...(error?.segment !== undefined ? { segment: error.segment } : {}),
90
+ ...(error?.utf8Bytes !== undefined ? { utf8Bytes: error.utf8Bytes } : {}),
91
+ ...(error?.utf16CodeUnits !== undefined ? { utf16CodeUnits: error.utf16CodeUnits } : {}),
92
+ ...(error?.limit !== undefined ? { limit: error.limit } : {}),
93
+ },
94
+ supportedFixes: ['set meta.output to a portable relative .html path such as reports/diagram.html'],
95
+ };
79
96
  }
80
97
 
81
- function nearestExistingDirectory(targetPath) {
82
- let directory = path.dirname(targetPath);
83
- while (true) {
84
- try {
85
- const stat = fs.statSync(directory);
86
- if (stat.isDirectory()) {
87
- return {
88
- path: fs.realpathSync.native(directory),
89
- stat,
90
- };
91
- }
92
- } catch (error) {
93
- if (error.code !== 'ENOENT' && error.code !== 'ENOTDIR') return null;
94
- }
95
- const parent = path.dirname(directory);
96
- if (parent === directory) return null;
97
- directory = parent;
98
+ function nativeOutputDiagnostic(rawOutput, reason, details = {}) {
99
+ return {
100
+ code: 'output/native-path-syntax',
101
+ message: 'The output path is not a valid native filesystem path on this host.',
102
+ subject: { output: rawOutput },
103
+ evidence: { reason, ...details },
104
+ supportedFixes: ['choose an ordinary filesystem path without device names, alternate data streams, trailing dots or spaces, or overlong components'],
105
+ };
106
+ }
107
+
108
+ function throwNativeOutputDiagnostic(rawOutput, reason, details = {}) {
109
+ const diagnostic = nativeOutputDiagnostic(rawOutput, reason, details);
110
+ throw new OutputPathError(diagnostic.message, diagnostic);
111
+ }
112
+
113
+ function windowsExtendedTailComponents(rawOutput, tail) {
114
+ const authoredComponents = tail.split('\\');
115
+ // Preserve one trailing separator for directory arguments, but never repair
116
+ // an empty component inside the raw extended namespace.
117
+ if (authoredComponents.slice(0, -1).some((component) => component.length === 0)) {
118
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root');
98
119
  }
120
+ return authoredComponents.filter(Boolean);
99
121
  }
100
122
 
101
- function directoryIdentityKey(directory) {
102
- if (!hasFileIdentity(directory.stat)) return null;
103
- return `${directory.stat.dev}:${directory.stat.ino}`;
123
+ function rejectWindowsIpcShare(rawOutput, share) {
124
+ if (isWindowsIpcShare(share)) {
125
+ throwNativeOutputDiagnostic(rawOutput, 'windows-ipc-namespace', { share });
126
+ }
104
127
  }
105
128
 
106
- function probeNamesAlias(directoryPath, authoredName, lookupName) {
107
- let fileDescriptor;
108
- let created = false;
109
- let result = null;
110
- let cleaned = true;
111
- const authoredPath = path.join(directoryPath, authoredName);
112
- const lookupPath = path.join(directoryPath, lookupName);
129
+ function validateWindowsNativeComponent(
130
+ rawOutput,
131
+ component,
132
+ componentIndex,
133
+ { allowReservedName = false } = {},
134
+ ) {
113
135
  try {
114
- fileDescriptor = fs.openSync(authoredPath, 'wx', 0o600);
115
- created = true;
116
- fs.closeSync(fileDescriptor);
117
- fileDescriptor = undefined;
118
-
119
- let authored;
120
- let lookup;
121
- try {
122
- authored = fs.statSync(authoredPath);
123
- lookup = fs.statSync(lookupPath);
124
- } catch (error) {
125
- if (error.code === 'ENOENT') result = false;
126
- }
127
- if (authored && lookup) {
128
- if (sameFileIdentity(authored, lookup)) {
129
- result = true;
130
- } else {
131
- try {
132
- result = fs.realpathSync.native(authoredPath) === fs.realpathSync.native(lookupPath);
133
- } catch {
134
- result = null;
135
- }
136
- }
137
- }
138
- } catch {
139
- result = null;
140
- } finally {
141
- if (fileDescriptor !== undefined) {
142
- try {
143
- fs.closeSync(fileDescriptor);
144
- } catch {
145
- cleaned = false;
146
- }
147
- }
148
- if (created) {
149
- try {
150
- fs.unlinkSync(authoredPath);
151
- } catch {
152
- cleaned = false;
153
- }
136
+ // Prefix the component so a colon is classified as an ADS separator,
137
+ // rather than allowing the generic URI detector to claim it first.
138
+ validatePortablePath(`native/${component}`, { profile: 'output' });
139
+ } catch (error) {
140
+ if (!(error instanceof PortablePathError)) throw error;
141
+ if (allowReservedName && error.reason === 'windows-reserved-name') return;
142
+ // Native Windows arguments may intentionally name an existing 8.3 alias.
143
+ // Portable authored/archive paths reject that ambiguous spelling, while
144
+ // native resolution lets the filesystem prove the existing target.
145
+ if (error.reason === 'windows-short-name') {
146
+ if (component.length <= 255) return;
147
+ throwNativeOutputDiagnostic(rawOutput, 'component-too-long', {
148
+ component,
149
+ componentIndex,
150
+ utf16CodeUnits: component.length,
151
+ limit: 255,
152
+ });
154
153
  }
154
+ // Native Windows filesystems bound components in UTF-16 code units. The
155
+ // stricter UTF-8 bound belongs to portable authored/archive names only.
156
+ if (error.reason === 'component-too-long' && error.utf16CodeUnits <= 255) return;
157
+ throwNativeOutputDiagnostic(rawOutput, error.reason, {
158
+ component,
159
+ componentIndex,
160
+ ...(error.character !== undefined ? { character: error.character } : {}),
161
+ ...(error.utf8Bytes !== undefined ? { utf8Bytes: error.utf8Bytes } : {}),
162
+ ...(error.utf16CodeUnits !== undefined ? { utf16CodeUnits: error.utf16CodeUnits } : {}),
163
+ ...(error.limit !== undefined ? { limit: error.limit } : {}),
164
+ });
155
165
  }
156
- return cleaned ? result : null;
157
166
  }
158
167
 
159
- function probeDirectorySemantics(directory) {
160
- const cacheKey = directoryIdentityKey(directory);
161
- if (cacheKey && directorySemanticsCache.has(cacheKey)) {
162
- return directorySemanticsCache.get(cacheKey);
168
+ function validateWindowsUncRootComponents(rawOutput, server, share) {
169
+ rejectWindowsIpcShare(rawOutput, share);
170
+ if (!isValidWindowsSmbShareName(share)) {
171
+ throwNativeOutputDiagnostic(rawOutput, 'windows-unc-share-name', {
172
+ share,
173
+ utf16CodeUnits: share.length,
174
+ limit: 80,
175
+ });
163
176
  }
177
+ // UNC servers and shares are root components, not DOS file names. Keep the
178
+ // server's ordinary native syntax checks while allowing names such as CON.
179
+ validateWindowsNativeComponent(rawOutput, server, 0, { allowReservedName: true });
180
+ }
164
181
 
165
- semanticsProbeSequence += 1;
166
- const suffix = `${process.pid}-${Date.now().toString(36)}-${semanticsProbeSequence}`;
167
- const caseAuthored = `.archify-Case-Probe-${suffix}`;
168
- const normalizationAuthored = `.archify-norm-\u00e9-probe-${suffix}`;
169
- const semantics = {
170
- caseInsensitive: probeNamesAlias(
171
- directory.path,
172
- caseAuthored,
173
- caseAuthored.toLowerCase(),
174
- ),
175
- normalizationInsensitive: probeNamesAlias(
176
- directory.path,
177
- normalizationAuthored,
178
- normalizationAuthored.normalize('NFD'),
179
- ),
180
- };
181
- if (
182
- cacheKey
183
- && semantics.caseInsensitive !== null
184
- && semantics.normalizationInsensitive !== null
185
- ) {
186
- directorySemanticsCache.set(cacheKey, semantics);
182
+ function windowsExtendedPathComponents(rawOutput) {
183
+ // The extended-length namespace deliberately bypasses Win32 normalization.
184
+ // Inspect its original spelling so a dot segment cannot retarget a UNC share.
185
+ if (rawOutput.includes('/')) {
186
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-separator', { character: '/' });
187
+ }
188
+ if (/^\\\\\?\\(?:GLOBALROOT|Device)(?:\\|$)/iu.test(rawOutput)) {
189
+ throwNativeOutputDiagnostic(rawOutput, 'windows-device-namespace');
187
190
  }
188
- return semantics;
189
- }
190
191
 
191
- function sameDirectory(left, right) {
192
- return left.path === right.path || sameFileIdentity(left.stat, right.stat);
193
- }
192
+ const drive = rawOutput.match(/^\\\\\?\\[A-Za-z]:\\/u);
193
+ if (drive) {
194
+ return windowsExtendedTailComponents(rawOutput, rawOutput.slice(drive[0].length));
195
+ }
194
196
 
195
- function futurePathsAlias(leftPath, rightPath) {
196
- const left = canonicalFuturePath(leftPath);
197
- const right = canonicalFuturePath(rightPath);
198
- if (left === right) return true;
197
+ const uncPrefix = rawOutput.match(/^\\\\\?\\UNC\\/iu);
198
+ if (uncPrefix) {
199
+ const authoredTail = rawOutput.slice(uncPrefix[0].length);
200
+ const components = authoredTail.split('\\');
201
+ if (components.length < 2 || components[0].length === 0 || components[1].length === 0) {
202
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root');
203
+ }
204
+ validateWindowsUncRootComponents(rawOutput, components[0], components[1]);
205
+ return windowsExtendedTailComponents(rawOutput, components.slice(2).join('\\'));
206
+ }
207
+
208
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root');
209
+ }
199
210
 
200
- const leftDirectory = nearestExistingDirectory(left);
201
- const rightDirectory = nearestExistingDirectory(right);
202
- if (!leftDirectory || !rightDirectory || !sameDirectory(leftDirectory, rightDirectory)) {
203
- return false;
211
+ function validateWindowsRawUncRoot(rawOutput) {
212
+ if (!/^[\\/]{2}/u.test(rawOutput) || /^[\\/]{2}[.?][\\/]/u.test(rawOutput)) return;
213
+ // Validate the raw server/share boundary before win32.normalize can collapse
214
+ // an empty share or mix the two UNC separator spellings.
215
+ const unc = rawOutput.match(/^([\\/])\1([^\\/]+)\1([^\\/]+)(?:[\\/]|$)/u);
216
+ if (!unc) {
217
+ throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root');
204
218
  }
219
+ validateWindowsUncRootComponents(rawOutput, unc[2], unc[3]);
220
+ }
205
221
 
206
- const semantics = probeDirectorySemantics(leftDirectory);
207
- let comparableLeft = path.relative(leftDirectory.path, left);
208
- let comparableRight = path.relative(rightDirectory.path, right);
209
- if (semantics.normalizationInsensitive !== false) {
210
- comparableLeft = comparableLeft.normalize('NFC');
211
- comparableRight = comparableRight.normalize('NFC');
222
+ function windowsPathComponents(rawOutput, normalized) {
223
+ const authoredUncPrefix = /^[\\/]{2}/u.test(rawOutput);
224
+ if (/^\\\\\.\\/u.test(normalized)) {
225
+ throwNativeOutputDiagnostic(rawOutput, 'windows-device-namespace');
212
226
  }
213
- if (semantics.caseInsensitive !== false) {
214
- comparableLeft = comparableLeft.toLowerCase();
215
- comparableRight = comparableRight.toLowerCase();
227
+ if (/^\\\\\?\\/u.test(normalized)) {
228
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root');
216
229
  }
217
- return comparableLeft === comparableRight;
230
+ if (normalized.startsWith('\\\\')) {
231
+ const unc = normalized.match(/^\\\\([^\\]+)\\([^\\]+)(?:\\|$)/u);
232
+ if (!unc) throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root');
233
+ return normalized.slice(unc[0].length).split('\\').filter(Boolean);
234
+ }
235
+ if (authoredUncPrefix) throwNativeOutputDiagnostic(rawOutput, 'windows-unc-root');
236
+ if (normalized.startsWith('\\')) {
237
+ throwNativeOutputDiagnostic(rawOutput, 'current-drive-rooted');
238
+ }
239
+ const root = path.win32.parse(normalized).root;
240
+ return normalized
241
+ .slice(root.length)
242
+ .split(/[\\/]+/u)
243
+ // normalize() retains leading navigation for a relative path. Those dot
244
+ // segments are path syntax, not filename components subject to name rules.
245
+ .filter((component) => component && component !== '.' && component !== '..');
218
246
  }
219
247
 
220
- export function pathsAlias(leftPath, rightPath) {
221
- if (futurePathsAlias(leftPath, rightPath)) return true;
222
- try {
223
- const left = fs.statSync(leftPath);
224
- const right = fs.statSync(rightPath);
225
- return sameFileIdentity(left, right);
226
- } catch {
227
- return false;
248
+ /**
249
+ * Validate command-line/default output paths using the active host's native
250
+ * syntax. Unlike authored portable paths, absolute paths and native separators
251
+ * remain supported. File outputs reject a trailing separator before native
252
+ * resolution can erase it; directory callers must opt in explicitly. The
253
+ * component bound also protects derived sidecars from failing after an
254
+ * operation has already started mutating the destination.
255
+ */
256
+ export function validateNativeOutputPath(
257
+ rawOutput,
258
+ { platform = process.platform, kind = 'file' } = {},
259
+ ) {
260
+ if (kind !== 'file' && kind !== 'directory') {
261
+ throw new TypeError(`Unsupported native output path kind: ${JSON.stringify(kind)}`);
262
+ }
263
+ if (typeof rawOutput !== 'string' || rawOutput.length === 0) {
264
+ throwNativeOutputDiagnostic(rawOutput, 'empty');
265
+ }
266
+ if (rawOutput.includes('\0')) throwNativeOutputDiagnostic(rawOutput, 'nul-character');
267
+ if (/[\uD800-\uDFFF]/u.test(rawOutput)) {
268
+ throwNativeOutputDiagnostic(rawOutput, 'unpaired-surrogate');
269
+ }
270
+ const hasTrailingSeparator = platform === 'win32'
271
+ ? /[\\/]$/u.test(rawOutput)
272
+ : rawOutput.endsWith(path.posix.sep);
273
+ if (kind === 'file' && hasTrailingSeparator) {
274
+ throwNativeOutputDiagnostic(rawOutput, 'trailing-separator', { kind });
275
+ }
276
+
277
+ let normalized;
278
+ let components;
279
+ if (platform === 'win32') {
280
+ if (/^[A-Za-z]:(?:$|[^\\/])/u.test(rawOutput)) {
281
+ throwNativeOutputDiagnostic(rawOutput, 'drive-relative');
282
+ }
283
+ const extendedPrefix = rawOutput.startsWith('\\\\?\\');
284
+ if (extendedPrefix) {
285
+ components = windowsExtendedPathComponents(rawOutput);
286
+ } else {
287
+ validateWindowsRawUncRoot(rawOutput);
288
+ normalized = path.win32.normalize(rawOutput);
289
+ components = windowsPathComponents(rawOutput, normalized);
290
+ }
291
+ for (const [componentIndex, component] of components.entries()) {
292
+ validateWindowsNativeComponent(rawOutput, component, componentIndex);
293
+ }
294
+ } else {
295
+ normalized = path.resolve(rawOutput);
296
+ const root = path.parse(normalized).root;
297
+ components = normalized.slice(root.length).split(path.sep).filter(Boolean);
298
+ for (const [componentIndex, component] of components.entries()) {
299
+ const utf8Bytes = Buffer.byteLength(component, 'utf8');
300
+ if (utf8Bytes > 255) {
301
+ throwNativeOutputDiagnostic(rawOutput, 'component-too-long', {
302
+ component,
303
+ componentIndex,
304
+ utf8Bytes,
305
+ limit: 255,
306
+ });
307
+ }
308
+ }
228
309
  }
310
+ return rawOutput;
229
311
  }
230
312
 
231
- function pathIsInside(directoryPath, targetPath) {
232
- const relative = path.relative(canonicalFuturePath(directoryPath), canonicalFuturePath(targetPath));
233
- return relative === '' || (!path.isAbsolute(relative) && relative !== '..' && !relative.startsWith(`..${path.sep}`));
313
+ /** Validate a CLI directory argument before native resolution can normalize away its raw syntax. */
314
+ export function resolveNativeOutputDirectory(
315
+ rawDirectory,
316
+ { platform = process.platform, cwd = process.cwd() } = {},
317
+ ) {
318
+ validateNativeOutputPath(rawDirectory, { platform, kind: 'directory' });
319
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
320
+ return pathApi.resolve(cwd, rawDirectory);
321
+ }
322
+
323
+ export function validateAuthoredOutputPath(rawOutput, { cwd = process.cwd() } = {}) {
324
+ try {
325
+ validatePortablePath(rawOutput, { profile: 'output' });
326
+ } catch (error) {
327
+ if (!(error instanceof PortablePathError)) throw error;
328
+ const diagnostic = authoredOutputDiagnostic(error, rawOutput);
329
+ throw new OutputPathError(diagnostic.message, diagnostic);
330
+ }
331
+ if (path.posix.extname(rawOutput).toLowerCase() !== '.html') {
332
+ throw new OutputPathError('meta.output must target an .html file.', {
333
+ code: 'output/meta-extension',
334
+ message: 'meta.output must target an .html file.',
335
+ subject: { output: rawOutput, path: '/meta/output' },
336
+ supportedFixes: ['change meta.output to a portable path ending in .html'],
337
+ });
338
+ }
339
+ const outputPath = path.resolve(cwd, rawOutput);
340
+ if (path.extname(canonicalFuturePath(outputPath)).toLowerCase() !== '.html') {
341
+ throw new OutputPathError('meta.output must resolve to an .html file.', {
342
+ code: 'output/meta-resolved-extension',
343
+ message: 'meta.output must resolve to an .html file after symbolic links are followed.',
344
+ subject: { output: rawOutput },
345
+ supportedFixes: ['remove the symbolic-link alias or point it to an .html target inside the current working directory'],
346
+ });
347
+ }
348
+ if (!pathIsInside(cwd, outputPath)) {
349
+ throw new OutputPathError('meta.output must stay inside the current working directory.', {
350
+ code: 'output/meta-outside-cwd',
351
+ message: 'meta.output must stay inside the current working directory after symbolic links are resolved.',
352
+ subject: { output: rawOutput, cwd: path.resolve(cwd) },
353
+ supportedFixes: ['set meta.output to a relative .html path inside the current working directory'],
354
+ });
355
+ }
356
+ return rawOutput;
234
357
  }
235
358
 
236
359
  export class OutputPathError extends Error {
@@ -256,46 +379,17 @@ export function resolveOutputPath({
256
379
  otherOutputPaths = [],
257
380
  cwd = process.cwd(),
258
381
  requiredExtension = '.html',
382
+ platform = process.platform,
259
383
  }) {
260
- const rawOutput = requestedOutput || authoredOutput || defaultOutput;
261
- const source = requestedOutput ? 'cli' : (authoredOutput ? 'meta' : 'default');
262
- if (
263
- source === 'meta'
264
- && (path.isAbsolute(rawOutput) || path.posix.isAbsolute(rawOutput) || path.win32.isAbsolute(rawOutput))
265
- ) {
266
- throw new OutputPathError('meta.output must be a relative path.', {
267
- code: 'output/meta-absolute',
268
- message: 'meta.output must be a relative path resolved from the current working directory.',
269
- subject: { output: rawOutput },
270
- supportedFixes: ['set meta.output to a relative .html path inside the current working directory'],
271
- });
272
- }
273
- if (source === 'meta' && path.extname(rawOutput).toLowerCase() !== '.html') {
274
- throw new OutputPathError('meta.output must target an .html file.', {
275
- code: 'output/meta-extension',
276
- message: 'meta.output must target an .html file.',
277
- subject: { output: rawOutput },
278
- supportedFixes: ['change meta.output to a path ending in .html'],
279
- });
280
- }
384
+ if (authoredOutput !== undefined) validateAuthoredOutputPath(authoredOutput, { cwd });
385
+ const source = requestedOutput !== undefined
386
+ ? 'cli'
387
+ : (authoredOutput !== undefined ? 'meta' : 'default');
388
+ const rawOutput = source === 'cli'
389
+ ? requestedOutput
390
+ : (source === 'meta' ? authoredOutput : defaultOutput);
391
+ if (source !== 'meta') validateNativeOutputPath(rawOutput, { platform, kind: 'file' });
281
392
  const outputPath = path.resolve(cwd, rawOutput);
282
- if (source === 'meta' && path.extname(canonicalFuturePath(outputPath)).toLowerCase() !== '.html') {
283
- throw new OutputPathError('meta.output must resolve to an .html file.', {
284
- code: 'output/meta-resolved-extension',
285
- message: 'meta.output must resolve to an .html file after symbolic links are followed.',
286
- subject: { output: rawOutput },
287
- supportedFixes: ['remove the symbolic-link alias or point it to an .html target inside the current working directory'],
288
- });
289
- }
290
- if (source === 'meta' && !pathIsInside(cwd, outputPath)) {
291
- throw new OutputPathError('meta.output must stay inside the current working directory.', {
292
- code: 'output/meta-outside-cwd',
293
- message: 'meta.output must stay inside the current working directory after symbolic links are resolved.',
294
- subject: { output: rawOutput, cwd: path.resolve(cwd) },
295
- supportedFixes: ['set meta.output to a relative .html path inside the current working directory'],
296
- });
297
- }
298
-
299
393
  for (const inputPath of inputPaths) {
300
394
  if (!pathsAlias(outputPath, inputPath)) continue;
301
395
  throw new OutputPathError(`Output must not replace ${inputDescription}.`, {
@@ -319,7 +413,13 @@ export function resolveOutputPath({
319
413
  // Alias checks above retain priority when a target would overwrite an input.
320
414
  if (source === 'cli') {
321
415
  const resolvedOutput = canonicalFuturePath(outputPath);
322
- const authoredMatches = path.extname(rawOutput).toLowerCase() === requiredExtension;
416
+ const authoredExtension = path.extname(rawOutput).toLowerCase();
417
+ const existingWindowsHtmlAlias = platform === 'win32'
418
+ && requiredExtension === '.html'
419
+ && authoredExtension === '.htm'
420
+ && path.extname(resolvedOutput).toLowerCase() === requiredExtension
421
+ && pathsAlias(outputPath, resolvedOutput);
422
+ const authoredMatches = authoredExtension === requiredExtension || existingWindowsHtmlAlias;
323
423
  const resolvedMatches = path.extname(resolvedOutput).toLowerCase() === requiredExtension;
324
424
  if (!authoredMatches || !resolvedMatches) {
325
425
  const message = `CLI output must ${authoredMatches ? 'resolve to' : 'target'} a ${requiredExtension} file.`;