dsh-archify-projects 0.0.0-stage → 0.1.0

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 (148) hide show
  1. package/README.md +204 -2
  2. package/cordis.patch.yml +99 -0
  3. package/dist/client.js +2261 -0
  4. package/dist/client.js.map +1 -0
  5. package/docs/CONTAINER.md +197 -0
  6. package/docs/DESIGN.md +905 -0
  7. package/docs/HANDOVER.md +391 -0
  8. package/docs/PUBLISH.md +107 -0
  9. package/icon.svg +12 -0
  10. package/lib/host/archify-skill.js +116 -0
  11. package/lib/host/archify-skill.js.map +1 -0
  12. package/lib/host/config.js +95 -0
  13. package/lib/host/config.js.map +1 -0
  14. package/lib/host/context.js +28 -0
  15. package/lib/host/context.js.map +1 -0
  16. package/lib/host/git.js +185 -0
  17. package/lib/host/git.js.map +1 -0
  18. package/lib/host/http.js +184 -0
  19. package/lib/host/http.js.map +1 -0
  20. package/lib/host/index.js +90 -0
  21. package/lib/host/index.js.map +1 -0
  22. package/lib/host/policy.js +124 -0
  23. package/lib/host/policy.js.map +1 -0
  24. package/lib/host/routes.js +667 -0
  25. package/lib/host/routes.js.map +1 -0
  26. package/lib/host/runner.js +913 -0
  27. package/lib/host/runner.js.map +1 -0
  28. package/lib/host/store.js +370 -0
  29. package/lib/host/store.js.map +1 -0
  30. package/lib/host/workspace.js +65 -0
  31. package/lib/host/workspace.js.map +1 -0
  32. package/lib/shared/format.js +138 -0
  33. package/lib/shared/format.js.map +1 -0
  34. package/lib/shared/prompt.js +257 -0
  35. package/lib/shared/prompt.js.map +1 -0
  36. package/lib/shared/protocol.js +31 -0
  37. package/lib/shared/protocol.js.map +1 -0
  38. package/lib/shared/types.js +173 -0
  39. package/lib/shared/types.js.map +1 -0
  40. package/locale/en.json +6 -0
  41. package/locale/zh.json +6 -0
  42. package/package.json +98 -4
  43. package/skills/archify/LICENSE +22 -0
  44. package/skills/archify/SKILL.md +109 -0
  45. package/skills/archify/THIRD_PARTY_NOTICES.md +69 -0
  46. package/skills/archify/assets/JetBrainsMono-OFL.txt +93 -0
  47. package/skills/archify/assets/template.html +13709 -0
  48. package/skills/archify/bin/archify.mjs +6936 -0
  49. package/skills/archify/bin/delivery-update.mjs +91 -0
  50. package/skills/archify/bin/finalize.mjs +895 -0
  51. package/skills/archify/bin/open-artifact.mjs +146 -0
  52. package/skills/archify/bin/preview.mjs +969 -0
  53. package/skills/archify/bin/recover-output.mjs +41 -0
  54. package/skills/archify/bin/visual-check.mjs +2667 -0
  55. package/skills/archify/brand-marks/README.md +31 -0
  56. package/skills/archify/brand-marks/catalog.json +131 -0
  57. package/skills/archify/delta/architecture-delta.mjs +1313 -0
  58. package/skills/archify/examples/agent-run.lifecycle.json +55 -0
  59. package/skills/archify/examples/agent-tool-call.workflow.json +74 -0
  60. package/skills/archify/examples/async-job-roundtrip.sequence.json +56 -0
  61. package/skills/archify/examples/brand-aware-delivery.architecture.json +34 -0
  62. package/skills/archify/examples/cache-miss-request.sequence.json +77 -0
  63. package/skills/archify/examples/checkout-platform.base.architecture.json +32 -0
  64. package/skills/archify/examples/checkout-platform.head.architecture.json +32 -0
  65. package/skills/archify/examples/dataflow-product-analytics.html +13820 -0
  66. package/skills/archify/examples/deployment-release.lifecycle.json +48 -0
  67. package/skills/archify/examples/event-stream.dataflow.json +52 -0
  68. package/skills/archify/examples/holiday-planning.workflow.json +143 -0
  69. package/skills/archify/examples/incident-response.workflow.json +59 -0
  70. package/skills/archify/examples/lifecycle-agent-run.html +13765 -0
  71. package/skills/archify/examples/locales/de.json +425 -0
  72. package/skills/archify/examples/locales/es.json +440 -0
  73. package/skills/archify/examples/locales/fr.json +425 -0
  74. package/skills/archify/examples/locales/fr.partial.json +22 -0
  75. package/skills/archify/examples/locales/it.json +425 -0
  76. package/skills/archify/examples/locales/ja.json +425 -0
  77. package/skills/archify/examples/locales/ko.json +440 -0
  78. package/skills/archify/examples/locales/pt.json +425 -0
  79. package/skills/archify/examples/locales/ru.json +425 -0
  80. package/skills/archify/examples/product-analytics.dataflow.json +71 -0
  81. package/skills/archify/examples/production-deployment.architecture.json +51 -0
  82. package/skills/archify/examples/release-delivery.workflow.json +57 -0
  83. package/skills/archify/examples/sequence-cache-miss-request.html +13835 -0
  84. package/skills/archify/examples/web-app-rendered.html +13808 -0
  85. package/skills/archify/examples/web-app.architecture.json +41 -0
  86. package/skills/archify/examples/workflow-agent-tool-call-rendered.html +13826 -0
  87. package/skills/archify/migrations/workflow-v2.mjs +280 -0
  88. package/skills/archify/package.json +17 -0
  89. package/skills/archify/recipes/scenarios.mjs +430 -0
  90. package/skills/archify/references/architecture-layout-repair.md +18 -0
  91. package/skills/archify/references/authoring-contract.md +419 -0
  92. package/skills/archify/references/authoring-defaults.md +41 -0
  93. package/skills/archify/references/brand-marks.md +82 -0
  94. package/skills/archify/references/delivery-contract.md +588 -0
  95. package/skills/archify/references/repository-authoring.md +105 -0
  96. package/skills/archify/references/update-awareness.md +12 -0
  97. package/skills/archify/references/viewer-runtime.md +35 -0
  98. package/skills/archify/renderers/architecture/grid.mjs +62 -0
  99. package/skills/archify/renderers/architecture/labels.mjs +215 -0
  100. package/skills/archify/renderers/architecture/render-architecture.mjs +1001 -0
  101. package/skills/archify/renderers/architecture/routing.mjs +1157 -0
  102. package/skills/archify/renderers/dataflow/README.md +105 -0
  103. package/skills/archify/renderers/dataflow/render-dataflow.mjs +544 -0
  104. package/skills/archify/renderers/lifecycle/README.md +171 -0
  105. package/skills/archify/renderers/lifecycle/grid-routing.mjs +429 -0
  106. package/skills/archify/renderers/lifecycle/render-lifecycle.mjs +938 -0
  107. package/skills/archify/renderers/sequence/README.md +129 -0
  108. package/skills/archify/renderers/sequence/render-sequence.mjs +526 -0
  109. package/skills/archify/renderers/shared/atomic-output.mjs +1982 -0
  110. package/skills/archify/renderers/shared/brand-marks.mjs +670 -0
  111. package/skills/archify/renderers/shared/cli.mjs +451 -0
  112. package/skills/archify/renderers/shared/desktop-readability.mjs +132 -0
  113. package/skills/archify/renderers/shared/diagnostics.mjs +173 -0
  114. package/skills/archify/renderers/shared/engineering-profiles.mjs +157 -0
  115. package/skills/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
  116. package/skills/archify/renderers/shared/generated-validators.mjs +134 -0
  117. package/skills/archify/renderers/shared/geometry.mjs +1941 -0
  118. package/skills/archify/renderers/shared/i18n.mjs +603 -0
  119. package/skills/archify/renderers/shared/layout-report.mjs +42 -0
  120. package/skills/archify/renderers/shared/legend.mjs +217 -0
  121. package/skills/archify/renderers/shared/output-path.mjs +440 -0
  122. package/skills/archify/renderers/shared/path-semantics.mjs +1017 -0
  123. package/skills/archify/renderers/shared/portable-path.mjs +408 -0
  124. package/skills/archify/renderers/shared/repository-evidence.mjs +352 -0
  125. package/skills/archify/renderers/shared/repository-location.mjs +59 -0
  126. package/skills/archify/renderers/shared/route-quality.mjs +614 -0
  127. package/skills/archify/renderers/shared/sidecar-path.mjs +38 -0
  128. package/skills/archify/renderers/shared/spatial-grid.mjs +66 -0
  129. package/skills/archify/renderers/shared/text-fit.mjs +105 -0
  130. package/skills/archify/renderers/shared/utils.mjs +254 -0
  131. package/skills/archify/renderers/shared/validator.mjs +86 -0
  132. package/skills/archify/renderers/workflow/README.md +301 -0
  133. package/skills/archify/renderers/workflow/render-workflow.mjs +37 -0
  134. package/skills/archify/renderers/workflow/workflow-compiler.mjs +4913 -0
  135. package/skills/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  136. package/skills/archify/schemas/README.md +256 -0
  137. package/skills/archify/schemas/architecture.schema.json +152 -0
  138. package/skills/archify/schemas/common.schema.json +181 -0
  139. package/skills/archify/schemas/dataflow.schema.json +254 -0
  140. package/skills/archify/schemas/lifecycle.schema.json +280 -0
  141. package/skills/archify/schemas/sequence.schema.json +234 -0
  142. package/skills/archify/schemas/workflow.schema.json +439 -0
  143. package/skills/archify/scripts/check-render-output.mjs +1388 -0
  144. package/skills/archify/scripts/check-update.mjs +1710 -0
  145. package/skills/archify/scripts/delivery-update-child.mjs +23 -0
  146. package/skills/archify/scripts/render-examples.mjs +26 -0
  147. package/skills/archify/scripts/update-contract.mjs +182 -0
  148. package/skills/archify/skill-release.json +10 -0
@@ -0,0 +1,440 @@
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';
10
+
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
+ );
19
+ }
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
+ });
37
+ }
38
+
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);
46
+ throw new OutputPathError(`Output path contains a symbolic-link cycle: "${output}".`, {
47
+ code: 'output/symlink-cycle',
48
+ message: 'Output path could not be resolved because it contains a symbolic-link cycle.',
49
+ subject: { output },
50
+ evidence: { relation: relation.reason },
51
+ supportedFixes: ['remove the symbolic-link cycle or choose an output path outside it'],
52
+ });
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
+ });
61
+ }
62
+
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
+ });
74
+ }
75
+
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
+ };
96
+ }
97
+
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');
119
+ }
120
+ return authoredComponents.filter(Boolean);
121
+ }
122
+
123
+ function rejectWindowsIpcShare(rawOutput, share) {
124
+ if (isWindowsIpcShare(share)) {
125
+ throwNativeOutputDiagnostic(rawOutput, 'windows-ipc-namespace', { share });
126
+ }
127
+ }
128
+
129
+ function validateWindowsNativeComponent(
130
+ rawOutput,
131
+ component,
132
+ componentIndex,
133
+ { allowReservedName = false } = {},
134
+ ) {
135
+ try {
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
+ });
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
+ });
165
+ }
166
+ }
167
+
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
+ });
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
+ }
181
+
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');
190
+ }
191
+
192
+ const drive = rawOutput.match(/^\\\\\?\\[A-Za-z]:\\/u);
193
+ if (drive) {
194
+ return windowsExtendedTailComponents(rawOutput, rawOutput.slice(drive[0].length));
195
+ }
196
+
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
+ }
210
+
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');
218
+ }
219
+ validateWindowsUncRootComponents(rawOutput, unc[2], unc[3]);
220
+ }
221
+
222
+ function windowsPathComponents(rawOutput, normalized) {
223
+ const authoredUncPrefix = /^[\\/]{2}/u.test(rawOutput);
224
+ if (/^\\\\\.\\/u.test(normalized)) {
225
+ throwNativeOutputDiagnostic(rawOutput, 'windows-device-namespace');
226
+ }
227
+ if (/^\\\\\?\\/u.test(normalized)) {
228
+ throwNativeOutputDiagnostic(rawOutput, 'windows-extended-root');
229
+ }
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 !== '..');
246
+ }
247
+
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
+ }
309
+ }
310
+ return rawOutput;
311
+ }
312
+
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;
357
+ }
358
+
359
+ export class OutputPathError extends Error {
360
+ constructor(message, diagnostic) {
361
+ super(message);
362
+ this.name = 'OutputPathError';
363
+ this.archifyDiagnostics = [{
364
+ severity: 'error',
365
+ subject: {},
366
+ evidence: {},
367
+ supportedFixes: [],
368
+ ...diagnostic,
369
+ }];
370
+ }
371
+ }
372
+
373
+ export function resolveOutputPath({
374
+ requestedOutput,
375
+ authoredOutput,
376
+ defaultOutput,
377
+ inputPaths = [],
378
+ inputDescription = 'an input',
379
+ otherOutputPaths = [],
380
+ cwd = process.cwd(),
381
+ requiredExtension = '.html',
382
+ platform = process.platform,
383
+ }) {
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' });
392
+ const outputPath = path.resolve(cwd, rawOutput);
393
+ for (const inputPath of inputPaths) {
394
+ if (!pathsAlias(outputPath, inputPath)) continue;
395
+ throw new OutputPathError(`Output must not replace ${inputDescription}.`, {
396
+ code: 'output/input-alias',
397
+ message: `Output must not replace ${inputDescription}, including through a symbolic-link or future-path alias.`,
398
+ subject: { output: outputPath, input: path.resolve(inputPath) },
399
+ supportedFixes: ['choose an output path that is distinct from every input path'],
400
+ });
401
+ }
402
+ for (const otherOutputPath of otherOutputPaths) {
403
+ if (!pathsAlias(outputPath, otherOutputPath)) continue;
404
+ throw new OutputPathError('Output targets must use distinct paths.', {
405
+ code: 'output/target-alias',
406
+ message: 'Output targets must use distinct paths, including symbolic-link and future-path aliases.',
407
+ subject: { output: outputPath, conflictingOutput: path.resolve(otherOutputPath) },
408
+ supportedFixes: ['choose distinct paths for every generated output'],
409
+ });
410
+ }
411
+
412
+ // Keep explicit CLI directories unrestricted, but reject mistaken file types.
413
+ // Alias checks above retain priority when a target would overwrite an input.
414
+ if (source === 'cli') {
415
+ const resolvedOutput = canonicalFuturePath(outputPath);
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;
423
+ const resolvedMatches = path.extname(resolvedOutput).toLowerCase() === requiredExtension;
424
+ if (!authoredMatches || !resolvedMatches) {
425
+ const message = `CLI output must ${authoredMatches ? 'resolve to' : 'target'} a ${requiredExtension} file.`;
426
+ throw new OutputPathError(message, {
427
+ code: authoredMatches ? 'output/cli-resolved-extension' : 'output/cli-extension',
428
+ message,
429
+ subject: { output: rawOutput },
430
+ evidence: { resolvedOutput, requiredExtension },
431
+ supportedFixes: [`choose a path ending in ${requiredExtension} whose symbolic-link target also ends in ${requiredExtension}`],
432
+ });
433
+ }
434
+ }
435
+
436
+ return {
437
+ outputPath,
438
+ source,
439
+ };
440
+ }