@stacksjs/defaults 0.74.40 → 0.74.42

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 (78) hide show
  1. package/ai/skills/stacks-cms/SKILL.md +4 -4
  2. package/ai/skills/stacks-composables/SKILL.md +1 -1
  3. package/ai/skills/stacks-config/SKILL.md +1 -1
  4. package/ai/skills/stacks-dashboard/SKILL.md +1 -1
  5. package/ai/skills/stacks-technical-diagrams/LICENSE +1 -1
  6. package/ai/skills/stacks-technical-diagrams/SKILL.md +257 -229
  7. package/ai/skills/stacks-technical-diagrams/THIRD_PARTY_NOTICES.md +69 -0
  8. package/ai/skills/stacks-technical-diagrams/assets/JetBrainsMono-OFL.txt +93 -0
  9. package/ai/skills/stacks-technical-diagrams/assets/template.html +14588 -929
  10. package/ai/skills/stacks-technical-diagrams/bin/diagrams +10 -0
  11. package/ai/skills/stacks-technical-diagrams/bin/open-artifact.mjs +86 -0
  12. package/ai/skills/stacks-technical-diagrams/bin/preview.mjs +654 -0
  13. package/ai/skills/stacks-technical-diagrams/bin/technical-diagrams.mjs +1898 -89
  14. package/ai/skills/stacks-technical-diagrams/bin/visual-check.mjs +829 -0
  15. package/ai/skills/stacks-technical-diagrams/brand-marks/README.md +31 -0
  16. package/ai/skills/stacks-technical-diagrams/brand-marks/catalog.json +131 -0
  17. package/ai/skills/stacks-technical-diagrams/delta/architecture-delta.mjs +1221 -0
  18. package/ai/skills/stacks-technical-diagrams/examples/agent-run.lifecycle.json +18 -22
  19. package/ai/skills/stacks-technical-diagrams/examples/agent-tool-call.workflow.json +58 -52
  20. package/ai/skills/stacks-technical-diagrams/examples/async-job-roundtrip.sequence.json +61 -0
  21. package/ai/skills/stacks-technical-diagrams/examples/brand-aware-delivery.architecture.json +47 -0
  22. package/ai/skills/stacks-technical-diagrams/examples/cache-miss-request.sequence.json +30 -23
  23. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.base.architecture.json +31 -0
  24. package/ai/skills/stacks-technical-diagrams/examples/checkout-platform.head.architecture.json +31 -0
  25. package/ai/skills/stacks-technical-diagrams/examples/deployment-release.lifecycle.json +49 -0
  26. package/ai/skills/stacks-technical-diagrams/examples/event-stream.dataflow.json +57 -0
  27. package/ai/skills/stacks-technical-diagrams/examples/incident-response.workflow.json +64 -0
  28. package/ai/skills/stacks-technical-diagrams/examples/product-analytics.dataflow.json +22 -16
  29. package/ai/skills/stacks-technical-diagrams/examples/production-deployment.architecture.json +71 -0
  30. package/ai/skills/stacks-technical-diagrams/examples/release-delivery.workflow.json +62 -0
  31. package/ai/skills/stacks-technical-diagrams/examples/web-app.architecture.json +16 -11
  32. package/ai/skills/stacks-technical-diagrams/migrations/workflow-v2.mjs +279 -0
  33. package/ai/skills/stacks-technical-diagrams/recipes/scenarios.mjs +391 -0
  34. package/ai/skills/stacks-technical-diagrams/references/authoring-contract.md +243 -0
  35. package/ai/skills/stacks-technical-diagrams/references/brand-marks.md +65 -0
  36. package/ai/skills/stacks-technical-diagrams/references/delivery-contract.md +120 -0
  37. package/ai/skills/stacks-technical-diagrams/references/viewer-runtime.md +45 -0
  38. package/ai/skills/stacks-technical-diagrams/renderers/architecture/render-architecture.mjs +780 -73
  39. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/README.md +25 -3
  40. package/ai/skills/stacks-technical-diagrams/renderers/dataflow/render-dataflow.mjs +240 -52
  41. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/README.md +31 -7
  42. package/ai/skills/stacks-technical-diagrams/renderers/lifecycle/render-lifecycle.mjs +227 -50
  43. package/ai/skills/stacks-technical-diagrams/renderers/sequence/README.md +36 -6
  44. package/ai/skills/stacks-technical-diagrams/renderers/sequence/render-sequence.mjs +270 -63
  45. package/ai/skills/stacks-technical-diagrams/renderers/shared/brand-marks.mjs +563 -0
  46. package/ai/skills/stacks-technical-diagrams/renderers/shared/bun-runtime.mjs +20 -0
  47. package/ai/skills/stacks-technical-diagrams/renderers/shared/cli.mjs +186 -10
  48. package/ai/skills/stacks-technical-diagrams/renderers/shared/desktop-readability.mjs +26 -0
  49. package/ai/skills/stacks-technical-diagrams/renderers/shared/diagnostics.mjs +127 -0
  50. package/ai/skills/stacks-technical-diagrams/renderers/shared/engineering-profiles.mjs +157 -0
  51. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-brand-marks.mjs +2003 -0
  52. package/ai/skills/stacks-technical-diagrams/renderers/shared/generated-validators.mjs +3 -3
  53. package/ai/skills/stacks-technical-diagrams/renderers/shared/geometry.mjs +1195 -2
  54. package/ai/skills/stacks-technical-diagrams/renderers/shared/i18n.mjs +595 -0
  55. package/ai/skills/stacks-technical-diagrams/renderers/shared/legend.mjs +217 -0
  56. package/ai/skills/stacks-technical-diagrams/renderers/shared/output-path.mjs +340 -0
  57. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-evidence.mjs +238 -0
  58. package/ai/skills/stacks-technical-diagrams/renderers/shared/repository-location.mjs +58 -0
  59. package/ai/skills/stacks-technical-diagrams/renderers/shared/text-fit.mjs +49 -0
  60. package/ai/skills/stacks-technical-diagrams/renderers/shared/utils.mjs +163 -19
  61. package/ai/skills/stacks-technical-diagrams/renderers/shared/validator.mjs +51 -5
  62. package/ai/skills/stacks-technical-diagrams/renderers/workflow/README.md +137 -17
  63. package/ai/skills/stacks-technical-diagrams/renderers/workflow/render-workflow.mjs +24 -470
  64. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-compiler.mjs +4400 -0
  65. package/ai/skills/stacks-technical-diagrams/renderers/workflow/workflow-migration-geometry.mjs +144 -0
  66. package/ai/skills/stacks-technical-diagrams/schemas/README.md +154 -11
  67. package/ai/skills/stacks-technical-diagrams/schemas/architecture.schema.json +61 -4
  68. package/ai/skills/stacks-technical-diagrams/schemas/common.schema.json +72 -0
  69. package/ai/skills/stacks-technical-diagrams/schemas/dataflow.schema.json +40 -18
  70. package/ai/skills/stacks-technical-diagrams/schemas/lifecycle.schema.json +43 -18
  71. package/ai/skills/stacks-technical-diagrams/schemas/sequence.schema.json +41 -4
  72. package/ai/skills/stacks-technical-diagrams/schemas/workflow.schema.json +97 -1
  73. package/ai/skills/stacks-technical-diagrams/scripts/check-render-output.mjs +551 -12
  74. package/ai/skills/stacks-technical-diagrams/scripts/render-examples.mjs +3 -4
  75. package/ide/vscode/package.json +1 -1
  76. package/package.json +2 -2
  77. package/resources/components/CookieConsent.stx +146 -0
  78. package/resources/components/CookieConsent.test.ts +79 -0
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  import { spawnSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
7
8
  import { fileURLToPath, pathToFileURL } from 'node:url';
9
+ import { runtimeArgs } from '../renderers/shared/bun-runtime.mjs';
8
10
 
9
11
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
12
  const skillRoot = path.resolve(__dirname, '..');
@@ -13,13 +15,21 @@ const TYPES = new Set(['architecture', 'workflow', 'sequence', 'dataflow', 'life
13
15
 
14
16
  function usage() {
15
17
  return `Usage:
16
- technical-diagrams render <type> <input.json> [output.html]
17
- technical-diagrams validate <type> <input.json> [--json] [--layout-json]
18
- technical-diagrams inspect <type> <input.json>
19
- technical-diagrams check <output.html>
20
- technical-diagrams examples [output-directory]
21
- technical-diagrams doctor
22
- technical-diagrams demo [output-directory]
18
+ diagrams render <type> <input.json> [output.html] [--quality standard|showcase] [--repo-root path (architecture only)]
19
+ diagrams compare architecture <base.json> <head.json> [output.html] [--receipt path] [--json] [--quality standard|showcase] [--repo-root path]
20
+ diagrams deliver <type> <input.json> [output.html] [--json] [--open] [--quality standard|showcase] [--repo-root path (architecture only)]
21
+ diagrams preview <type> <input.json> [output.html] [--no-open] [--quality standard|showcase] [--repo-root path (architecture only)]
22
+ diagrams validate <type> <input.json> [--json] [--layout-json] [--quality standard|showcase] [--repo-root path (architecture only)]
23
+ diagrams migrate workflow <old.json> <new.json> --to-schema 2 [--json]
24
+ diagrams inspect <type> <input.json>
25
+ diagrams check <output.html>
26
+ diagrams visual-check <output.html> [--json]
27
+ diagrams guide [scenario or question] [--json] [--lang en|zh]
28
+ diagrams brands [name, alias, domain, or category] [--json]
29
+ diagrams brands capture <url> [--json]
30
+ diagrams examples [output-directory]
31
+ diagrams doctor
32
+ diagrams demo [output-directory]
23
33
 
24
34
  Types:
25
35
  architecture, workflow, sequence, dataflow, lifecycle
@@ -31,62 +41,1266 @@ function fail(message, code = 2) {
31
41
  process.exit(code);
32
42
  }
33
43
 
44
+ function rejectCliArgument(message, details = {}) {
45
+ const error = new Error(message);
46
+ error.archifyArgument = {
47
+ code: details.code || 'cli/invalid-arguments',
48
+ subject: details.subject || {},
49
+ evidence: details.evidence || {},
50
+ supportedFixes: details.supportedFixes || ['correct the command arguments and retry'],
51
+ };
52
+ throw error;
53
+ }
54
+
34
55
  function rendererPath(type) {
35
56
  if (!TYPES.has(type)) {
36
- fail(`Unknown diagram type "${type}". Expected one of: ${[...TYPES].join(', ')}`);
57
+ rejectCliArgument(`Unknown diagram type "${type}". Expected one of: ${[...TYPES].join(', ')}`, {
58
+ code: 'cli/unknown-diagram-type',
59
+ subject: { type },
60
+ evidence: { supportedTypes: [...TYPES] },
61
+ supportedFixes: [`use one of: ${[...TYPES].join(', ')}`],
62
+ });
37
63
  }
38
64
  return path.join(skillRoot, 'renderers', type, `render-${type}.mjs`);
39
65
  }
40
66
 
41
- function runRuntime(args, options = {}) {
42
- return spawnSync(process.execPath, [
43
- `--config=${path.join(skillRoot, 'bunfig.toml')}`,
44
- '--no-env-file',
45
- ...args,
46
- ], {
67
+ function runNode(args, options = {}) {
68
+ return spawnSync(process.execPath, runtimeArgs(args), {
47
69
  cwd: options.cwd || process.cwd(),
48
70
  encoding: 'utf8',
49
71
  stdio: options.stdio || 'inherit',
72
+ env: options.env ? { ...process.env, ...options.env } : process.env,
73
+ });
74
+ }
75
+
76
+ function extractQualityArgs(args) {
77
+ const rest = [];
78
+ let quality;
79
+ for (let index = 0; index < args.length; index += 1) {
80
+ const arg = args[index];
81
+ if (arg === '--quality') {
82
+ quality = args[index + 1];
83
+ if (!quality || quality.startsWith('--')) rejectCliArgument('--quality requires standard or showcase.', {
84
+ code: 'cli/missing-option-value',
85
+ subject: { option: '--quality' },
86
+ supportedFixes: ['provide --quality standard or --quality showcase'],
87
+ });
88
+ index += 1;
89
+ continue;
90
+ }
91
+ if (arg.startsWith('--quality=')) {
92
+ quality = arg.slice('--quality='.length);
93
+ if (!quality) rejectCliArgument('--quality requires standard or showcase.', {
94
+ code: 'cli/missing-option-value',
95
+ subject: { option: '--quality' },
96
+ supportedFixes: ['provide --quality standard or --quality showcase'],
97
+ });
98
+ continue;
99
+ }
100
+ rest.push(arg);
101
+ }
102
+ if (quality !== undefined && !['standard', 'showcase'].includes(quality)) {
103
+ rejectCliArgument(`Unknown quality profile "${quality}". Expected standard or showcase.`, {
104
+ code: 'cli/invalid-option-value',
105
+ subject: { option: '--quality' },
106
+ evidence: { value: quality, supportedValues: ['standard', 'showcase'] },
107
+ supportedFixes: ['use --quality standard or --quality showcase'],
108
+ });
109
+ }
110
+ return { rest, quality };
111
+ }
112
+
113
+ function extractRepoRootArgs(args) {
114
+ const rest = [];
115
+ let repoRoot;
116
+ for (let index = 0; index < args.length; index += 1) {
117
+ const arg = args[index];
118
+ if (arg === '--repo-root') {
119
+ repoRoot = args[index + 1];
120
+ if (!repoRoot || repoRoot.startsWith('--')) rejectCliArgument('--repo-root requires a repository path.', {
121
+ code: 'cli/missing-option-value',
122
+ subject: { option: '--repo-root' },
123
+ supportedFixes: ['provide one repository path after --repo-root'],
124
+ });
125
+ index += 1;
126
+ continue;
127
+ }
128
+ if (arg.startsWith('--repo-root=')) {
129
+ repoRoot = arg.slice('--repo-root='.length);
130
+ if (!repoRoot) rejectCliArgument('--repo-root requires a repository path.', {
131
+ code: 'cli/missing-option-value',
132
+ subject: { option: '--repo-root' },
133
+ supportedFixes: ['provide one repository path after --repo-root'],
134
+ });
135
+ continue;
136
+ }
137
+ rest.push(arg);
138
+ }
139
+ return { rest, repoRoot: repoRoot ? path.resolve(repoRoot) : undefined };
140
+ }
141
+
142
+ function rendererEnv(quality, repoRoot, diagnosticJson = false) {
143
+ return {
144
+ ...(quality ? { ARCHIFY_QUALITY_PROFILE: quality } : {}),
145
+ ...(repoRoot ? { ARCHIFY_REPO_ROOT: repoRoot } : {}),
146
+ ...(diagnosticJson ? { ARCHIFY_DIAGNOSTIC_FORMAT: 'json' } : {}),
147
+ };
148
+ }
149
+
150
+ function diagnostic({ code, message, subject = {}, evidence = {}, supportedFixes = [], severity = 'error' }) {
151
+ return {
152
+ code,
153
+ severity,
154
+ message,
155
+ subject,
156
+ evidence,
157
+ supportedFixes,
158
+ };
159
+ }
160
+
161
+ function inputDiagnostic(error, inputPath) {
162
+ const isSyntax = error instanceof SyntaxError;
163
+ return diagnostic({
164
+ code: isSyntax ? 'input/json-parse' : 'input/read',
165
+ message: isSyntax
166
+ ? `Input JSON could not be parsed: ${error.message}`
167
+ : `Input could not be read: ${error.message}`,
168
+ subject: { input: inputPath },
169
+ evidence: {
170
+ ...(error?.code ? { systemCode: error.code } : {}),
171
+ reason: error.message,
172
+ },
173
+ supportedFixes: [isSyntax
174
+ ? 'repair the JSON syntax and run validation again'
175
+ : 'provide one readable JSON input file'],
50
176
  });
51
177
  }
52
178
 
179
+ function rendererFailure(result) {
180
+ if (result.error) {
181
+ return {
182
+ error: 'Renderer process could not start.',
183
+ diagnostics: [diagnostic({
184
+ code: 'internal/renderer-process',
185
+ message: 'Renderer process could not start.',
186
+ evidence: { reason: result.error.message },
187
+ })],
188
+ };
189
+ }
190
+ try {
191
+ const payload = JSON.parse((result.stderr || '').trim());
192
+ if (payload?.ok === false && Array.isArray(payload.diagnostics) && payload.diagnostics.length) {
193
+ return {
194
+ error: payload.error || payload.diagnostics[0].message,
195
+ diagnostics: payload.diagnostics,
196
+ };
197
+ }
198
+ } catch {
199
+ // The diagnostic boundary is intentionally fail-closed. Never copy a raw
200
+ // Node stack into a machine receipt when a renderer exits unexpectedly.
201
+ }
202
+ return {
203
+ error: 'Renderer failed before emitting a structured diagnostic.',
204
+ diagnostics: [diagnostic({
205
+ code: 'internal/unclassified',
206
+ message: 'Renderer failed before emitting a structured diagnostic.',
207
+ evidence: { exitCode: result.status ?? 1 },
208
+ })],
209
+ };
210
+ }
211
+
212
+ const COMPOSITION_CHECKS = new Set([
213
+ 'label_route_clearance',
214
+ 'relationship_crossings',
215
+ 'relationship_corridors',
216
+ 'container_border_runs',
217
+ 'route_rhythm',
218
+ ]);
219
+
220
+ const CHECK_FIXES = {
221
+ single_svg: ['remove additional SVG roots so the artifact contains exactly one diagram SVG'],
222
+ finite_svg: ['replace non-finite coordinates before rendering again'],
223
+ orthogonal_arrows: ['use renderer-supported orthogonal routing controls'],
224
+ legend_clearance: ['move the route or enlarge the viewBox so relationships do not enter the legend'],
225
+ };
226
+
227
+ const COMPOSITION_FIXES = {
228
+ 'composition/proper-crossing': ['adjust route/via or channel coordinates so unrelated relationships use separate corridors'],
229
+ 'composition/ambiguous-corridor': ['adjust route/via or channel coordinates so unrelated relationships do not visually merge'],
230
+ 'composition/container-border-run': ['route across the frame perpendicularly through a clear opening'],
231
+ 'composition/label-route-clearance': ['adjust labelAt, labelDx, labelDy, labelSegment, message y, or the other relationship route'],
232
+ 'composition/desktop-readability': ['reduce the viewBox width, shorten node copy, widen affected nodes, or split the diagram so node context remains at least 6px at a 1440px desktop viewport'],
233
+ 'composition/micro-segment': ['move the route/channel/via point so every visible segment is at least 8px'],
234
+ 'composition/short-interior-segment': ['move the route/channel/via point so every interior turn has at least 16px'],
235
+ };
236
+
237
+ function checkerDiagnostics(checker) {
238
+ const diagnostics = [];
239
+ for (const issue of checker?.composition?.issues || []) {
240
+ if (issue.severity !== 'error') continue;
241
+ const { severity, code, relationship, ...evidence } = issue;
242
+ diagnostics.push(diagnostic({
243
+ code,
244
+ severity,
245
+ message: `Final artifact failed ${code}.`,
246
+ subject: relationship ? { relationship } : { check: 'composition' },
247
+ evidence,
248
+ supportedFixes: COMPOSITION_FIXES[code] || [],
249
+ }));
250
+ }
251
+ for (const check of checker?.checks || []) {
252
+ if (check.ok || COMPOSITION_CHECKS.has(check.name)) continue;
253
+ diagnostics.push(diagnostic({
254
+ code: `artifact/${check.name.replaceAll('_', '-')}`,
255
+ message: (check.details || []).find(Boolean) || `Final artifact failed ${check.name}.`,
256
+ subject: { check: check.name },
257
+ evidence: { details: check.details || [] },
258
+ supportedFixes: CHECK_FIXES[check.name] || [],
259
+ }));
260
+ }
261
+ return diagnostics.length ? diagnostics : [diagnostic({
262
+ code: 'artifact/check-failed',
263
+ message: 'Final artifact check failed without a classified diagnostic.',
264
+ subject: { check: 'unknown' },
265
+ evidence: {},
266
+ })];
267
+ }
268
+
269
+ function formatDiagnostics(error, diagnostics = []) {
270
+ if (!diagnostics.length) return error;
271
+ return [
272
+ error,
273
+ ...diagnostics.map((entry) => {
274
+ const fix = entry.supportedFixes?.length ? ` Fix: ${entry.supportedFixes.join('; ')}.` : '';
275
+ return `[${entry.code}] ${entry.message}${fix}`;
276
+ }),
277
+ ].join('\n');
278
+ }
279
+
280
+ function assertEvidenceType(type, repoRoot) {
281
+ if (repoRoot && type !== 'architecture') {
282
+ rejectCliArgument('--repo-root is currently supported for architecture diagrams only.', {
283
+ code: 'cli/unsupported-option',
284
+ subject: { option: '--repo-root', type },
285
+ supportedFixes: ['remove --repo-root or use an architecture diagram'],
286
+ });
287
+ }
288
+ }
289
+
53
290
  function exitFrom(result) {
54
291
  if (result.error) fail(result.error.message, 1);
55
292
  process.exit(result.status ?? 1);
56
293
  }
57
294
 
295
+ function reportCompareFailure({ json, stage, error, code = 'delta/internal', details = {}, status = 1 }) {
296
+ const receipt = {
297
+ schemaVersion: 1,
298
+ ok: false,
299
+ command: 'compare',
300
+ type: 'architecture',
301
+ stage,
302
+ error,
303
+ diagnostics: [{
304
+ code,
305
+ severity: 'error',
306
+ message: error,
307
+ subject: details.side ? { side: details.side, ...(details.path ? { path: details.path } : {}) } : {},
308
+ evidence: Object.fromEntries(Object.entries(details).filter(([key]) => !['side', 'path', 'supportedFixes'].includes(key))),
309
+ supportedFixes: details.supportedFixes || [],
310
+ }],
311
+ };
312
+ if (json) console.log(JSON.stringify(receipt, null, 2));
313
+ else console.error(formatDiagnostics(error, receipt.diagnostics));
314
+ process.exitCode = status;
315
+ }
316
+
317
+ function extractCompareOptions(args) {
318
+ const positional = [];
319
+ let receipt;
320
+ let json = false;
321
+ for (let index = 0; index < args.length; index += 1) {
322
+ const arg = args[index];
323
+ if (arg === '--json') {
324
+ json = true;
325
+ continue;
326
+ }
327
+ if (arg === '--receipt') {
328
+ receipt = args[index + 1];
329
+ if (!receipt || receipt.startsWith('--')) fail('--receipt requires a JSON output path.');
330
+ index += 1;
331
+ continue;
332
+ }
333
+ if (arg.startsWith('--receipt=')) {
334
+ receipt = arg.slice('--receipt='.length);
335
+ if (!receipt) fail('--receipt requires a JSON output path.');
336
+ continue;
337
+ }
338
+ if (arg.startsWith('--')) fail(`Unknown compare option "${arg}".`);
339
+ positional.push(arg);
340
+ }
341
+ return { positional, receipt, json };
342
+ }
343
+
344
+ function compareReceiptPath(outputPath) {
345
+ const extension = path.extname(outputPath);
346
+ return extension ? `${outputPath.slice(0, -extension.length)}.receipt.json` : `${outputPath}.receipt.json`;
347
+ }
348
+
349
+ function compareCommitError(message, code, details = {}) {
350
+ const error = new Error(message);
351
+ error.compareStage = 'commit';
352
+ error.compareCode = code;
353
+ error.compareDetails = details;
354
+ return error;
355
+ }
356
+
357
+ function commitComparePair({ htmlCandidate, receiptCandidate, outputPath, receiptPath, stagingDirectory }) {
358
+ const targets = [
359
+ { label: 'HTML artifact', target: outputPath, candidate: htmlCandidate, backup: path.join(stagingDirectory, '.previous-output') },
360
+ { label: 'receipt', target: receiptPath, candidate: receiptCandidate, backup: path.join(stagingDirectory, '.previous-receipt') },
361
+ ];
362
+
363
+ // Preflight the whole pair before moving either trusted target. This avoids
364
+ // replacing the HTML and only then discovering that its receipt destination
365
+ // cannot be committed (for example, because it is a directory).
366
+ for (const item of targets) {
367
+ if (!fs.existsSync(item.target)) continue;
368
+ const existing = fs.lstatSync(item.target);
369
+ if (!existing.isFile()) {
370
+ throw compareCommitError(
371
+ `Could not commit Architecture Delta: existing ${item.label} target is not a regular file.`,
372
+ 'delta/commit-target',
373
+ {
374
+ target: path.basename(item.target),
375
+ targetType: existing.isDirectory() ? 'directory' : 'non-file',
376
+ supportedFixes: [`choose a regular-file path for the ${item.label}`],
377
+ },
378
+ );
379
+ }
380
+ }
381
+
382
+ const backedUp = [];
383
+ const committed = [];
384
+ try {
385
+ for (const item of targets) {
386
+ if (!fs.existsSync(item.target)) continue;
387
+ fs.renameSync(item.target, item.backup);
388
+ backedUp.push(item);
389
+ }
390
+ for (const item of targets) {
391
+ fs.renameSync(item.candidate, item.target);
392
+ committed.push(item);
393
+ }
394
+ } catch (cause) {
395
+ const rollbackErrors = [];
396
+ for (const item of [...committed].reverse()) {
397
+ try {
398
+ fs.rmSync(item.target, { force: true });
399
+ } catch (error) {
400
+ rollbackErrors.push(`${item.label}: remove failed (${error.message})`);
401
+ }
402
+ }
403
+ for (const item of [...backedUp].reverse()) {
404
+ try {
405
+ if (fs.existsSync(item.target)) fs.rmSync(item.target, { force: true });
406
+ fs.renameSync(item.backup, item.target);
407
+ } catch (error) {
408
+ rollbackErrors.push(`${item.label}: restore failed (${error.message})`);
409
+ }
410
+ }
411
+ throw compareCommitError(
412
+ rollbackErrors.length
413
+ ? 'Architecture Delta pair commit failed and its previous files could not be fully restored.'
414
+ : 'Architecture Delta pair commit failed; the previous files were restored.',
415
+ rollbackErrors.length ? 'delta/commit-rollback-failed' : 'delta/commit-failed',
416
+ {
417
+ reason: cause.message,
418
+ ...(rollbackErrors.length ? { rollbackErrors } : {}),
419
+ supportedFixes: ['check that both output paths are writable regular files, then retry'],
420
+ },
421
+ );
422
+ }
423
+ }
424
+
425
+ function renderValidatedArchitecture(inputPath, outputPath, quality, repoRoot) {
426
+ const render = runNode([rendererPath('architecture'), inputPath, outputPath], {
427
+ stdio: 'pipe',
428
+ env: rendererEnv(quality, repoRoot, true),
429
+ });
430
+ if (render.status !== 0) {
431
+ const failure = rendererFailure(render);
432
+ const error = new Error(failure.error);
433
+ error.compareStage = 'input';
434
+ error.compareStatus = render.status ?? 1;
435
+ error.diagnostics = failure.diagnostics;
436
+ throw error;
437
+ }
438
+ const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), outputPath], { stdio: 'pipe' });
439
+ if (check.status !== 0) {
440
+ const error = new Error('Validated snapshot failed final artifact checks.');
441
+ error.compareStage = 'check';
442
+ error.compareStatus = check.status ?? 1;
443
+ try {
444
+ error.checker = JSON.parse(check.stdout);
445
+ error.diagnostics = checkerDiagnostics(error.checker);
446
+ } catch {
447
+ error.diagnostics = [];
448
+ }
449
+ throw error;
450
+ }
451
+ const artifact = fs.readFileSync(outputPath);
452
+ return {
453
+ artifact,
454
+ html: artifact.toString('utf8'),
455
+ checks: JSON.parse(check.stdout),
456
+ sourceEvidence: sourceEvidenceFromArtifact(artifact),
457
+ };
458
+ }
459
+
460
+ async function commandCompare(args) {
461
+ const { resolveOutputPath } = await import('../renderers/shared/output-path.mjs');
462
+ const qualityArgs = extractQualityArgs(args);
463
+ const repoArgs = extractRepoRootArgs(qualityArgs.rest);
464
+ const options = extractCompareOptions(repoArgs.rest);
465
+ const [type, baseInput, headInput, requestedOutput] = options.positional;
466
+ if (type !== 'architecture' || !baseInput || !headInput || options.positional.length > 4) fail(usage());
467
+ let deltaRuntime;
468
+ try {
469
+ deltaRuntime = await import(pathToFileURL(path.join(skillRoot, 'delta/architecture-delta.mjs')).href);
470
+ } catch (error) {
471
+ reportCompareFailure({ json: options.json, stage: 'prepare', error: 'Architecture compare runtime is unavailable.', code: 'delta/runtime-missing', details: { reason: error.message, supportedFixes: ['restore the complete stacks-technical-diagrams skill directory'] } });
472
+ return;
473
+ }
474
+ const {
475
+ ArchitectureDeltaError,
476
+ annotateArchitectureSideSvg,
477
+ buildDeltaSvg,
478
+ canonicalArchitecture,
479
+ canonicalArchitectureJson,
480
+ compareArchitecture,
481
+ extractArchitectureSvg,
482
+ extractArtifactCss,
483
+ renderArchitectureDeltaHtml,
484
+ validateArchitectureDeltaHtml,
485
+ } = deltaRuntime;
486
+
487
+ const basePath = path.resolve(baseInput);
488
+ const headPath = path.resolve(headInput);
489
+ const receiptTarget = options.receipt || compareReceiptPath(path.resolve(requestedOutput || 'architecture-delta.html'));
490
+ let outputPath;
491
+ try {
492
+ ({ outputPath } = resolveOutputPath({
493
+ requestedOutput,
494
+ defaultOutput: 'architecture-delta.html',
495
+ inputPaths: [basePath, headPath],
496
+ otherOutputPaths: [path.resolve(receiptTarget)],
497
+ }));
498
+ } catch (error) {
499
+ const outputDiagnostic = error.archifyDiagnostics?.[0];
500
+ reportCompareFailure({
501
+ json: options.json,
502
+ stage: 'prepare',
503
+ error: error.message,
504
+ code: outputDiagnostic?.code || 'output/path-resolution',
505
+ details: {
506
+ ...(outputDiagnostic?.subject || {}),
507
+ ...(outputDiagnostic?.evidence || {}),
508
+ supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe output path and retry'],
509
+ },
510
+ });
511
+ return;
512
+ }
513
+ let receiptPath;
514
+ try {
515
+ ({ outputPath: receiptPath } = resolveOutputPath({
516
+ requestedOutput: options.receipt || compareReceiptPath(outputPath),
517
+ defaultOutput: compareReceiptPath(outputPath),
518
+ requiredExtension: '.json',
519
+ inputPaths: [basePath, headPath],
520
+ otherOutputPaths: [outputPath],
521
+ }));
522
+ } catch (error) {
523
+ const outputDiagnostic = error.archifyDiagnostics?.[0];
524
+ reportCompareFailure({
525
+ json: options.json,
526
+ stage: 'prepare',
527
+ error: error.message,
528
+ code: outputDiagnostic?.code || 'output/path-resolution',
529
+ details: {
530
+ ...(outputDiagnostic?.subject || {}),
531
+ ...(outputDiagnostic?.evidence || {}),
532
+ supportedFixes: outputDiagnostic?.supportedFixes || ['choose a safe receipt path and retry'],
533
+ },
534
+ });
535
+ return;
536
+ }
537
+ let baseBuffer;
538
+ let headBuffer;
539
+ let base;
540
+ let head;
541
+ try {
542
+ baseBuffer = fs.readFileSync(basePath);
543
+ base = JSON.parse(baseBuffer.toString('utf8'));
544
+ } catch (error) {
545
+ reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read base input: ${error.message}`, code: 'delta/base-input', details: { side: 'base', reason: error.message } });
546
+ return;
547
+ }
548
+ try {
549
+ headBuffer = fs.readFileSync(headPath);
550
+ head = JSON.parse(headBuffer.toString('utf8'));
551
+ } catch (error) {
552
+ reportCompareFailure({ json: options.json, stage: 'input', error: `Could not read head input: ${error.message}`, code: 'delta/head-input', details: { side: 'head', reason: error.message } });
553
+ return;
554
+ }
555
+
556
+ const outputDirectory = path.dirname(outputPath);
557
+ if (path.dirname(receiptPath) !== outputDirectory) {
558
+ reportCompareFailure({ json: options.json, stage: 'prepare', error: 'The compare receipt must be written beside the HTML artifact.', code: 'delta/receipt-directory', details: { supportedFixes: ['choose a --receipt path in the same directory as output.html'] } });
559
+ return;
560
+ }
561
+ try {
562
+ fs.mkdirSync(outputDirectory, { recursive: true });
563
+ } catch (error) {
564
+ reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare output directory: ${error.message}`, code: 'delta/output-directory', details: { reason: error.message } });
565
+ return;
566
+ }
567
+
568
+ let stagingDirectory;
569
+ try {
570
+ stagingDirectory = fs.mkdtempSync(path.join(outputDirectory, '.archify-compare-'));
571
+ } catch (error) {
572
+ reportCompareFailure({ json: options.json, stage: 'prepare', error: `Could not create compare candidate: ${error.message}`, code: 'delta/candidate-directory', details: { reason: error.message } });
573
+ return;
574
+ }
575
+
576
+ const baseCandidate = path.join(stagingDirectory, 'base.html');
577
+ const headCandidate = path.join(stagingDirectory, 'head.html');
578
+ const rawBaseCandidate = path.join(stagingDirectory, 'base.raw.html');
579
+ const rawHeadCandidate = path.join(stagingDirectory, 'head.raw.html');
580
+ const canonicalBaseInput = path.join(stagingDirectory, 'base.architecture.json');
581
+ const canonicalHeadInput = path.join(stagingDirectory, 'head.architecture.json');
582
+ const htmlCandidate = path.join(stagingDirectory, path.basename(outputPath));
583
+ const receiptCandidate = path.join(stagingDirectory, path.basename(receiptPath));
584
+
585
+ try {
586
+ let baseResult;
587
+ let headResult;
588
+ try {
589
+ renderValidatedArchitecture(basePath, rawBaseCandidate, qualityArgs.quality, repoArgs.repoRoot);
590
+ } catch (error) {
591
+ const diagnosticEntry = error.diagnostics?.[0];
592
+ reportCompareFailure({
593
+ json: options.json,
594
+ stage: error.compareStage || 'validate',
595
+ error: `Base snapshot failed validation: ${error.message}`,
596
+ code: diagnosticEntry?.code || 'delta/base-validation',
597
+ details: { side: 'base', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] },
598
+ status: error.compareStatus || 1,
599
+ });
600
+ return;
601
+ }
602
+ try {
603
+ renderValidatedArchitecture(headPath, rawHeadCandidate, qualityArgs.quality, repoArgs.repoRoot);
604
+ } catch (error) {
605
+ const diagnosticEntry = error.diagnostics?.[0];
606
+ reportCompareFailure({
607
+ json: options.json,
608
+ stage: error.compareStage || 'validate',
609
+ error: `Head snapshot failed validation: ${error.message}`,
610
+ code: diagnosticEntry?.code || 'delta/head-validation',
611
+ details: { side: 'head', ...(diagnosticEntry?.subject?.path ? { path: diagnosticEntry.subject.path } : {}), ...(diagnosticEntry?.evidence || {}), supportedFixes: diagnosticEntry?.supportedFixes || [] },
612
+ status: error.compareStatus || 1,
613
+ });
614
+ return;
615
+ }
616
+
617
+ // Validation must see the exact authored inputs. Only after both sides
618
+ // pass do we canonicalize their collection order for deterministic SVG
619
+ // geometry and stable artifact bytes.
620
+ fs.writeFileSync(canonicalBaseInput, JSON.stringify(canonicalArchitecture(base)));
621
+ fs.writeFileSync(canonicalHeadInput, JSON.stringify(canonicalArchitecture(head)));
622
+ baseResult = renderValidatedArchitecture(canonicalBaseInput, baseCandidate, qualityArgs.quality, repoArgs.repoRoot);
623
+ headResult = renderValidatedArchitecture(canonicalHeadInput, headCandidate, qualityArgs.quality, repoArgs.repoRoot);
624
+
625
+ const semanticHash = (diagram) => createHash('sha256').update(canonicalArchitectureJson(diagram)).digest('hex');
626
+ let compareIr;
627
+ try {
628
+ compareIr = compareArchitecture(base, head, {
629
+ baseRawSha256: createHash('sha256').update(baseBuffer).digest('hex'),
630
+ headRawSha256: createHash('sha256').update(headBuffer).digest('hex'),
631
+ baseSemanticSha256: semanticHash(base),
632
+ headSemanticSha256: semanticHash(head),
633
+ baseBytes: baseBuffer.byteLength,
634
+ headBytes: headBuffer.byteLength,
635
+ baseVerified: Boolean(baseResult.sourceEvidence),
636
+ headVerified: Boolean(headResult.sourceEvidence),
637
+ });
638
+ } catch (error) {
639
+ if (!(error instanceof ArchitectureDeltaError)) throw error;
640
+ reportCompareFailure({ json: options.json, stage: 'compare', error: error.message, code: error.code, details: error.details });
641
+ return;
642
+ }
643
+
644
+ const baseSourceSvg = extractArchitectureSvg(baseResult.html);
645
+ const headSourceSvg = extractArchitectureSvg(headResult.html);
646
+ const baseSvg = annotateArchitectureSideSvg(baseSourceSvg, compareIr, 'base');
647
+ const headSvg = annotateArchitectureSideSvg(headSourceSvg, compareIr, 'head');
648
+ const deltaSvg = buildDeltaSvg(baseSourceSvg, headSourceSvg, compareIr);
649
+ // Raw input hashes and byte counts belong in the sidecar receipt, not the
650
+ // artifact. Keeping them out makes formatting-only input rewrites produce
651
+ // the exact same canonical review HTML and artifact hash.
652
+ const artifactIr = {
653
+ ...compareIr,
654
+ base: Object.fromEntries(Object.entries(compareIr.base).filter(([key]) => !['rawSha256', 'bytes'].includes(key))),
655
+ head: Object.fromEntries(Object.entries(compareIr.head).filter(([key]) => !['rawSha256', 'bytes'].includes(key))),
656
+ };
657
+ const html = renderArchitectureDeltaHtml({
658
+ receipt: artifactIr,
659
+ baseSvg,
660
+ deltaSvg,
661
+ headSvg,
662
+ baseHtml: baseResult.html,
663
+ headHtml: headResult.html,
664
+ artifactCss: extractArtifactCss(headResult.html),
665
+ });
666
+ const deltaValidation = validateArchitectureDeltaHtml(html, artifactIr);
667
+ fs.writeFileSync(htmlCandidate, html);
668
+ const artifact = fs.readFileSync(htmlCandidate);
669
+ const baseChecks = baseResult.checks.checks.filter((check) => check.ok).length;
670
+ const headChecks = headResult.checks.checks.filter((check) => check.ok).length;
671
+ const finalReceipt = {
672
+ ...compareIr,
673
+ artifact: { sha256: createHash('sha256').update(artifact).digest('hex'), bytes: artifact.byteLength },
674
+ validation: {
675
+ checksPassed: baseChecks + headChecks + deltaValidation.checksPassed,
676
+ checkCount: baseResult.checks.checks.length + headResult.checks.checks.length + deltaValidation.checkCount,
677
+ baseComposition: baseResult.checks.composition.status,
678
+ headComposition: headResult.checks.composition.status,
679
+ },
680
+ };
681
+ fs.writeFileSync(receiptCandidate, `${JSON.stringify(finalReceipt, null, 2)}\n`);
682
+
683
+ try {
684
+ const currentOutput = resolveOutputPath({
685
+ requestedOutput,
686
+ defaultOutput: 'architecture-delta.html',
687
+ inputPaths: [basePath, headPath],
688
+ otherOutputPaths: [receiptPath],
689
+ }).outputPath;
690
+ resolveOutputPath({
691
+ requestedOutput: options.receipt || compareReceiptPath(currentOutput),
692
+ defaultOutput: compareReceiptPath(currentOutput),
693
+ requiredExtension: '.json',
694
+ inputPaths: [basePath, headPath],
695
+ otherOutputPaths: [currentOutput],
696
+ });
697
+ } catch (error) {
698
+ const outputDiagnostic = error.archifyDiagnostics?.[0];
699
+ reportCompareFailure({
700
+ json: options.json,
701
+ stage: 'commit',
702
+ error: error.message,
703
+ code: outputDiagnostic?.code || 'output/path-resolution',
704
+ details: {
705
+ ...(outputDiagnostic?.subject || {}),
706
+ ...(outputDiagnostic?.evidence || {}),
707
+ supportedFixes: outputDiagnostic?.supportedFixes || ['restore safe output paths and retry'],
708
+ },
709
+ });
710
+ return;
711
+ }
712
+
713
+ commitComparePair({ htmlCandidate, receiptCandidate, outputPath, receiptPath, stagingDirectory });
714
+ if (options.json) console.log(JSON.stringify(finalReceipt, null, 2));
715
+ else {
716
+ console.log(`compared architecture ${outputPath}`);
717
+ console.log(`${finalReceipt.validation.checksPassed}/${finalReceipt.validation.checkCount} checks; completeness ${finalReceipt.completeness}; ${finalReceipt.proofLevel}; sha256 ${finalReceipt.artifact.sha256.slice(0, 12)}`);
718
+ console.log(`receipt ${receiptPath}`);
719
+ }
720
+ } catch (error) {
721
+ if (error instanceof ArchitectureDeltaError) {
722
+ reportCompareFailure({ json: options.json, stage: 'artifact', error: error.message, code: error.code, details: error.details });
723
+ } else if (error.compareStage === 'commit') {
724
+ reportCompareFailure({
725
+ json: options.json,
726
+ stage: error.compareStage,
727
+ error: error.message,
728
+ code: error.compareCode,
729
+ details: error.compareDetails,
730
+ });
731
+ } else {
732
+ reportCompareFailure({ json: options.json, stage: 'internal', error: 'Architecture compare failed before commit.', code: 'delta/internal', details: { reason: error.message } });
733
+ }
734
+ } finally {
735
+ try {
736
+ fs.rmSync(stagingDirectory, { recursive: true, force: true });
737
+ } catch (error) {
738
+ console.error(`Warning: could not remove compare staging directory: ${error.message}`);
739
+ }
740
+ }
741
+ }
742
+
58
743
  function commandRender(args) {
59
- const [type, input, output] = args;
60
- if (!type || !input) fail(usage());
61
- const result = runRuntime([rendererPath(type), input, ...(output ? [output] : [])]);
744
+ const qualityArgs = extractQualityArgs(args);
745
+ const repoArgs = extractRepoRootArgs(qualityArgs.rest);
746
+ // render takes no options of its own once --quality and --repo-root are
747
+ // stripped, so anything left starting with -- is a typo. Without this a
748
+ // mistyped flag was taken as the output path: `render architecture spec.json
749
+ // --json out.html` wrote a file literally named `--json` and never wrote
750
+ // out.html, exiting 0. Every sibling subcommand already guards this.
751
+ const unknown = repoArgs.rest.filter((arg) => arg.startsWith('--'));
752
+ if (unknown.length) fail(`Unknown render option "${unknown[0]}".`);
753
+ const [type, input, output] = repoArgs.rest;
754
+ if (!type || !input || repoArgs.rest.length > 3) fail(usage());
755
+ assertEvidenceType(type, repoArgs.repoRoot);
756
+ const result = runNode([rendererPath(type), input, ...(output ? [output] : [])], {
757
+ env: rendererEnv(qualityArgs.quality, repoArgs.repoRoot),
758
+ });
62
759
  if (result.status !== 0) exitFrom(result);
63
760
  }
64
761
 
762
+ function reportArtifactFailure({ command, json, stage, type, input, output, error, diagnostics = [], status = 1, checker }) {
763
+ const receipt = {
764
+ schemaVersion: 1,
765
+ ok: false,
766
+ command,
767
+ stage,
768
+ type,
769
+ input,
770
+ ...(output === undefined ? {} : { output }),
771
+ error,
772
+ diagnostics,
773
+ ...(checker ? { checker } : {}),
774
+ };
775
+ if (json) console.log(JSON.stringify(receipt, null, 2));
776
+ else console.error(formatDiagnostics(error, diagnostics));
777
+ process.exitCode = status;
778
+ }
779
+
780
+ function reportDeliveryFailure(options) {
781
+ reportArtifactFailure({ ...options, command: 'deliver' });
782
+ }
783
+
784
+ function reportValidateFailure(options) {
785
+ reportArtifactFailure({ ...options, command: 'validate' });
786
+ }
787
+
788
+ function reportArtifactArgumentFailure(command, error) {
789
+ const details = error.archifyArgument || {};
790
+ reportArtifactFailure({
791
+ command,
792
+ json: true,
793
+ stage: 'arguments',
794
+ error: error.message,
795
+ diagnostics: [diagnostic({
796
+ code: details.code || 'cli/invalid-arguments',
797
+ message: error.message,
798
+ subject: { command, ...(details.subject || {}) },
799
+ evidence: details.evidence || {},
800
+ supportedFixes: details.supportedFixes || ['correct the command arguments and retry'],
801
+ })],
802
+ status: 2,
803
+ });
804
+ }
805
+
806
+ function sourceEvidenceFromArtifact(artifact) {
807
+ const html = artifact.toString('utf8');
808
+ const match = html.match(/<script id="archify-source-evidence-data" type="application\/json">([\s\S]*?)<\/script>/);
809
+ if (!match) return null;
810
+ const evidence = JSON.parse(match[1]);
811
+ if (evidence?.verified !== true || !evidence.repository?.url || !evidence.repository?.revision || !Number.isInteger(evidence.referenceCount)) {
812
+ throw new Error('Rendered source evidence receipt is incomplete.');
813
+ }
814
+ return evidence;
815
+ }
816
+
817
+ function engineeringProfileFromArtifact(artifact) {
818
+ const match = artifact.toString('utf8').match(/<svg[^>]*\sdata-engineering-profile="([^"]+)"/);
819
+ return match ? match[1] : null;
820
+ }
821
+
822
+ async function commandDeliver(args) {
823
+ const qualityArgs = extractQualityArgs(args);
824
+ const repoArgs = extractRepoRootArgs(qualityArgs.rest);
825
+ const json = repoArgs.rest.includes('--json');
826
+ const open = repoArgs.rest.includes('--open');
827
+ const knownOptions = new Set(['--json', '--open']);
828
+ const unknown = repoArgs.rest.filter((arg) => arg.startsWith('--') && !knownOptions.has(arg));
829
+ if (unknown.length) rejectCliArgument(`Unknown deliver option "${unknown[0]}".`, {
830
+ code: 'cli/unknown-option',
831
+ subject: { option: unknown[0] },
832
+ supportedFixes: ['remove the unknown option and retry'],
833
+ });
834
+ const positional = repoArgs.rest.filter((arg) => !knownOptions.has(arg));
835
+ const [type, input, requestedOutput] = positional;
836
+ if (!type || !input || positional.length > 3) rejectCliArgument(usage(), {
837
+ code: 'cli/usage',
838
+ supportedFixes: ['use: diagrams deliver <type> <input.json> [output.html] [options]'],
839
+ });
840
+ assertEvidenceType(type, repoArgs.repoRoot);
841
+ const renderer = rendererPath(type);
842
+ const { resolveOutputPath } = await import('../renderers/shared/output-path.mjs');
843
+ const inputPath = path.resolve(input);
844
+ let specification;
845
+ let diagram;
846
+ try {
847
+ specification = fs.readFileSync(inputPath);
848
+ diagram = JSON.parse(specification.toString('utf8'));
849
+ } catch (error) {
850
+ const repair = inputDiagnostic(error, inputPath);
851
+ reportDeliveryFailure({
852
+ json,
853
+ stage: 'input',
854
+ type,
855
+ input: inputPath,
856
+ output: path.resolve(requestedOutput || `${type}.html`),
857
+ error: `Could not read delivery input "${inputPath}": ${error.message}`,
858
+ diagnostics: [repair],
859
+ });
860
+ return;
861
+ }
862
+
863
+ const authoredOutput = typeof diagram?.meta?.output === 'string' && diagram.meta.output
864
+ ? diagram.meta.output
865
+ : undefined;
866
+ let outputPath;
867
+ try {
868
+ ({ outputPath } = resolveOutputPath({
869
+ requestedOutput,
870
+ authoredOutput,
871
+ defaultOutput: `${type}.html`,
872
+ inputPaths: [inputPath],
873
+ }));
874
+ } catch (error) {
875
+ const attemptedOutput = path.resolve(requestedOutput || authoredOutput || `${type}.html`);
876
+ reportDeliveryFailure({
877
+ json,
878
+ stage: 'prepare',
879
+ type,
880
+ input: inputPath,
881
+ output: attemptedOutput,
882
+ error: error.message,
883
+ diagnostics: error.archifyDiagnostics || [diagnostic({
884
+ code: 'output/path-resolution',
885
+ message: error.message,
886
+ subject: { output: attemptedOutput },
887
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}) },
888
+ supportedFixes: ['choose a safe output path and retry'],
889
+ })],
890
+ });
891
+ return;
892
+ }
893
+ const outputDirectory = path.dirname(outputPath);
894
+ try {
895
+ fs.mkdirSync(outputDirectory, { recursive: true });
896
+ } catch (error) {
897
+ const message = `Could not create delivery directory "${outputDirectory}": ${error.message}`;
898
+ reportDeliveryFailure({
899
+ json,
900
+ stage: 'prepare',
901
+ type,
902
+ input: inputPath,
903
+ output: outputPath,
904
+ error: message,
905
+ diagnostics: [diagnostic({
906
+ code: 'delivery/prepare-directory',
907
+ message,
908
+ subject: { outputDirectory },
909
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
910
+ supportedFixes: ['choose a writable output directory'],
911
+ })],
912
+ });
913
+ return;
914
+ }
915
+
916
+ // Keep the candidate beside the target so the final rename is one
917
+ // same-filesystem commit. A render or artifact-check failure never touches
918
+ // an existing trusted output.
919
+ let stagingDirectory;
920
+ try {
921
+ stagingDirectory = fs.mkdtempSync(path.join(outputDirectory, '.archify-delivery-'));
922
+ } catch (error) {
923
+ const message = `Could not create a delivery candidate beside "${outputPath}": ${error.message}`;
924
+ reportDeliveryFailure({
925
+ json,
926
+ stage: 'prepare',
927
+ type,
928
+ input: inputPath,
929
+ output: outputPath,
930
+ error: message,
931
+ diagnostics: [diagnostic({
932
+ code: 'delivery/prepare-candidate',
933
+ message,
934
+ subject: { output: outputPath },
935
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
936
+ supportedFixes: ['choose a writable output directory on the target filesystem'],
937
+ })],
938
+ });
939
+ return;
940
+ }
941
+ const candidatePath = path.join(stagingDirectory, path.basename(outputPath));
942
+ const specificationSnapshotPath = path.join(stagingDirectory, 'specification.snapshot.json');
943
+
944
+ try {
945
+ try {
946
+ fs.writeFileSync(specificationSnapshotPath, specification, { flag: 'wx' });
947
+ } catch (error) {
948
+ const message = `Could not freeze the delivery specification: ${error.message}`;
949
+ reportDeliveryFailure({
950
+ json,
951
+ stage: 'prepare',
952
+ type,
953
+ input: inputPath,
954
+ output: outputPath,
955
+ error: message,
956
+ diagnostics: [diagnostic({
957
+ code: 'delivery/freeze-specification',
958
+ message,
959
+ subject: { input: inputPath },
960
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
961
+ supportedFixes: ['choose a writable output directory on the target filesystem'],
962
+ })],
963
+ });
964
+ return;
965
+ }
966
+
967
+ const render = runNode([renderer, specificationSnapshotPath, candidatePath], {
968
+ stdio: 'pipe',
969
+ env: rendererEnv(qualityArgs.quality, repoArgs.repoRoot, true),
970
+ });
971
+ if (render.status !== 0) {
972
+ const failure = rendererFailure(render);
973
+ reportDeliveryFailure({
974
+ json,
975
+ stage: 'render',
976
+ type,
977
+ input: inputPath,
978
+ output: outputPath,
979
+ error: failure.error,
980
+ diagnostics: failure.diagnostics,
981
+ status: render.status ?? 1,
982
+ });
983
+ return;
984
+ }
985
+
986
+ const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), candidatePath], {
987
+ stdio: 'pipe',
988
+ });
989
+ if (check.status !== 0) {
990
+ if (check.stderr) process.stderr.write(check.stderr);
991
+ let checker;
992
+ try {
993
+ checker = JSON.parse(check.stdout);
994
+ checker.file = outputPath;
995
+ } catch {
996
+ checker = { ok: false, file: outputPath, diagnostic: check.stdout.trim() };
997
+ }
998
+ reportDeliveryFailure({
999
+ json,
1000
+ stage: 'check',
1001
+ type,
1002
+ input: inputPath,
1003
+ output: outputPath,
1004
+ error: 'Final artifact check failed; the previous artifact was preserved.',
1005
+ diagnostics: checkerDiagnostics(checker),
1006
+ status: check.status ?? 1,
1007
+ checker,
1008
+ });
1009
+ return;
1010
+ }
1011
+
1012
+ let result;
1013
+ try {
1014
+ result = JSON.parse(check.stdout);
1015
+ } catch (error) {
1016
+ const message = `Could not parse the successful artifact-check receipt: ${error.message}`;
1017
+ reportDeliveryFailure({
1018
+ json,
1019
+ stage: 'receipt',
1020
+ type,
1021
+ input: inputPath,
1022
+ output: outputPath,
1023
+ error: message,
1024
+ diagnostics: [diagnostic({
1025
+ code: 'delivery/receipt-invalid',
1026
+ message,
1027
+ subject: { output: outputPath },
1028
+ evidence: { reason: error.message },
1029
+ })],
1030
+ });
1031
+ return;
1032
+ }
1033
+ let artifact;
1034
+ try {
1035
+ artifact = fs.readFileSync(candidatePath);
1036
+ } catch (error) {
1037
+ const message = `Could not read the verified delivery candidate: ${error.message}`;
1038
+ reportDeliveryFailure({
1039
+ json,
1040
+ stage: 'receipt',
1041
+ type,
1042
+ input: inputPath,
1043
+ output: outputPath,
1044
+ error: message,
1045
+ diagnostics: [diagnostic({
1046
+ code: 'delivery/candidate-unreadable',
1047
+ message,
1048
+ subject: { output: outputPath },
1049
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
1050
+ })],
1051
+ });
1052
+ return;
1053
+ }
1054
+ let sourceEvidence;
1055
+ try {
1056
+ sourceEvidence = sourceEvidenceFromArtifact(artifact);
1057
+ } catch (error) {
1058
+ const message = `Could not read the repository evidence receipt: ${error.message}`;
1059
+ reportDeliveryFailure({
1060
+ json,
1061
+ stage: 'receipt',
1062
+ type,
1063
+ input: inputPath,
1064
+ output: outputPath,
1065
+ error: message,
1066
+ diagnostics: [diagnostic({
1067
+ code: 'delivery/evidence-receipt-invalid',
1068
+ message,
1069
+ subject: { output: outputPath },
1070
+ evidence: { reason: error.message },
1071
+ })],
1072
+ });
1073
+ return;
1074
+ }
1075
+ const engineeringProfile = engineeringProfileFromArtifact(artifact);
1076
+ const receipt = {
1077
+ schemaVersion: 1,
1078
+ ok: true,
1079
+ command: 'deliver',
1080
+ type,
1081
+ input: inputPath,
1082
+ output: outputPath,
1083
+ specification: {
1084
+ sha256: createHash('sha256').update(specification).digest('hex'),
1085
+ bytes: specification.byteLength,
1086
+ },
1087
+ artifact: {
1088
+ sha256: createHash('sha256').update(artifact).digest('hex'),
1089
+ bytes: artifact.byteLength,
1090
+ },
1091
+ validation: {
1092
+ checksPassed: result.checks.filter((checkItem) => checkItem.ok).length,
1093
+ checkCount: result.checks.length,
1094
+ compositionProfile: result.composition.profile,
1095
+ compositionStatus: result.composition.status,
1096
+ ...(engineeringProfile ? { engineeringProfile } : {}),
1097
+ errors: result.composition.summary.errors,
1098
+ warnings: result.composition.summary.warnings,
1099
+ },
1100
+ ...(sourceEvidence ? {
1101
+ evidence: {
1102
+ verified: true,
1103
+ repository: sourceEvidence.repository.url,
1104
+ revision: sourceEvidence.repository.revision,
1105
+ references: sourceEvidence.referenceCount,
1106
+ ...(sourceEvidence.repository.linkMode ? { linkMode: sourceEvidence.repository.linkMode } : {}),
1107
+ },
1108
+ } : {}),
1109
+ };
1110
+
1111
+ try {
1112
+ resolveOutputPath({
1113
+ requestedOutput,
1114
+ authoredOutput,
1115
+ defaultOutput: `${type}.html`,
1116
+ inputPaths: [inputPath],
1117
+ });
1118
+ } catch (error) {
1119
+ reportDeliveryFailure({
1120
+ json,
1121
+ stage: 'commit',
1122
+ type,
1123
+ input: inputPath,
1124
+ output: outputPath,
1125
+ error: error.message,
1126
+ diagnostics: error.archifyDiagnostics || [diagnostic({
1127
+ code: 'output/path-resolution',
1128
+ message: error.message,
1129
+ subject: { output: outputPath },
1130
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}) },
1131
+ supportedFixes: ['restore a safe output path and retry'],
1132
+ })],
1133
+ });
1134
+ return;
1135
+ }
1136
+
1137
+ try {
1138
+ fs.renameSync(candidatePath, outputPath);
1139
+ } catch (error) {
1140
+ const message = `Could not commit verified delivery "${outputPath}": ${error.message}`;
1141
+ reportDeliveryFailure({
1142
+ json,
1143
+ stage: 'commit',
1144
+ type,
1145
+ input: inputPath,
1146
+ output: outputPath,
1147
+ error: message,
1148
+ diagnostics: [diagnostic({
1149
+ code: 'delivery/commit',
1150
+ message,
1151
+ subject: { output: outputPath },
1152
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
1153
+ supportedFixes: ['choose a replaceable file target on the same writable filesystem'],
1154
+ })],
1155
+ });
1156
+ return;
1157
+ }
1158
+
1159
+ if (open) {
1160
+ try {
1161
+ const { openArtifact } = await import('./open-artifact.mjs');
1162
+ receipt.open = openArtifact(outputPath);
1163
+ } catch {
1164
+ receipt.open = {
1165
+ requested: true,
1166
+ status: 'unsupported',
1167
+ target: outputPath,
1168
+ method: null,
1169
+ };
1170
+ }
1171
+ if (receipt.open.status !== 'opened') {
1172
+ console.error(`Could not open the verified artifact (${receipt.open.status}). Open it manually: ${outputPath}`);
1173
+ }
1174
+ }
1175
+
1176
+ if (json) {
1177
+ console.log(JSON.stringify(receipt, null, 2));
1178
+ } else {
1179
+ console.log(`delivered ${type} ${outputPath}`);
1180
+ const engineering = receipt.validation.engineeringProfile
1181
+ ? `; engineering ${receipt.validation.engineeringProfile}: pass`
1182
+ : '';
1183
+ console.log(`${receipt.validation.checksPassed}/${receipt.validation.checkCount} artifact checks; composition ${receipt.validation.compositionProfile}: ${receipt.validation.compositionStatus}${engineering}; sha256 ${receipt.artifact.sha256.slice(0, 12)}`);
1184
+ if (receipt.open?.status === 'opened') console.log(`opened ${outputPath}`);
1185
+ }
1186
+ } finally {
1187
+ try {
1188
+ fs.rmSync(stagingDirectory, { recursive: true, force: true });
1189
+ } catch (error) {
1190
+ console.error(`Warning: could not remove delivery staging directory "${stagingDirectory}": ${error.message}`);
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ async function commandPreview(args) {
1196
+ const qualityArgs = extractQualityArgs(args);
1197
+ const repoArgs = extractRepoRootArgs(qualityArgs.rest);
1198
+ const noOpen = repoArgs.rest.includes('--no-open');
1199
+ const knownOptions = new Set(['--no-open']);
1200
+ const unknown = repoArgs.rest.filter((arg) => arg.startsWith('--') && !knownOptions.has(arg));
1201
+ if (unknown.length) fail(`Unknown preview option "${unknown[0]}".`);
1202
+ const positional = repoArgs.rest.filter((arg) => !knownOptions.has(arg));
1203
+ const [type, input, output] = positional;
1204
+ if (!type || !input || positional.length > 3) fail(usage());
1205
+ assertEvidenceType(type, repoArgs.repoRoot);
1206
+ rendererPath(type);
1207
+
1208
+ let runPreview;
1209
+ try {
1210
+ ({ runPreview } = await import('./preview.mjs'));
1211
+ } catch (error) {
1212
+ fail(`Could not load live preview: ${error.message}`, 1);
1213
+ }
1214
+ try {
1215
+ await runPreview({
1216
+ type,
1217
+ input,
1218
+ output,
1219
+ quality: qualityArgs.quality,
1220
+ repoRoot: repoArgs.repoRoot,
1221
+ open: !noOpen,
1222
+ });
1223
+ } catch (error) {
1224
+ fail(`Could not start live preview: ${error.message}`, 1);
1225
+ }
1226
+ }
1227
+
65
1228
  function commandCheck(args) {
66
1229
  const [html] = args;
67
1230
  if (!html) fail(usage());
68
- const result = runRuntime([path.join(skillRoot, 'scripts/check-render-output.mjs'), html]);
1231
+ const result = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), html]);
69
1232
  if (result.status !== 0) exitFrom(result);
70
1233
  }
71
1234
 
1235
+ async function commandVisualCheck(args) {
1236
+ const json = args.includes('--json');
1237
+ const knownOptions = new Set(['--json']);
1238
+ const unknown = args.filter((arg) => arg.startsWith('--') && !knownOptions.has(arg));
1239
+ if (unknown.length) fail(`Unknown visual-check option "${unknown[0]}".`, 1);
1240
+ const positional = args.filter((arg) => !knownOptions.has(arg));
1241
+ if (positional.length !== 1) fail(usage(), 1);
1242
+
1243
+ let runVisualCheck;
1244
+ try {
1245
+ ({ runVisualCheck } = await import('./visual-check.mjs'));
1246
+ } catch (error) {
1247
+ fail(`Could not load visual-check: ${error.message}`, 1);
1248
+ }
1249
+
1250
+ let result;
1251
+ try {
1252
+ result = await runVisualCheck({ artifactPath: positional[0] });
1253
+ } catch (error) {
1254
+ if (json) {
1255
+ console.log(JSON.stringify({
1256
+ schemaVersion: 1,
1257
+ ok: false,
1258
+ command: 'visual-check',
1259
+ evidenceKind: 'automated-browser',
1260
+ status: 'fail',
1261
+ visualReview: 'pending',
1262
+ artifact: { path: path.resolve(positional[0]) },
1263
+ error: error.message,
1264
+ }, null, 2));
1265
+ } else {
1266
+ console.error(`automated browser evidence failed: ${error.message}`);
1267
+ console.error('perceptual visual review pending');
1268
+ }
1269
+ process.exitCode = 1;
1270
+ return;
1271
+ }
1272
+
1273
+ if (json) {
1274
+ console.log(JSON.stringify(result.receipt, null, 2));
1275
+ } else {
1276
+ console.log(`automated browser evidence ${result.receipt.status}: ${result.receipt.artifact.path}`);
1277
+ console.log(`visual-check containment ${result.receipt.containment.status}; captures ${result.receipt.captures.status}; perceptual visual review pending`);
1278
+ console.log(`receipt ${path.join(path.dirname(result.receipt.artifact.path), result.receipt.sidecars.receipt)}`);
1279
+ if (result.receipt.captures.contactSheet) {
1280
+ console.log(`contact sheet ${path.join(path.dirname(result.receipt.artifact.path), result.receipt.captures.contactSheet)}`);
1281
+ }
1282
+ if (result.receipt.error) console.error(result.receipt.error);
1283
+ }
1284
+ process.exitCode = result.exitCode;
1285
+ }
1286
+
72
1287
  function commandExamples(args) {
73
- if (args.length > 1) fail(usage());
74
- const outputDirectory = path.resolve(args[0] || process.cwd());
1288
+ // Stacks port: the skill directory is tracked in git, so rendering the bundled
1289
+ // examples in place would drop several megabytes of generated HTML into the
1290
+ // working tree. Default to a temp directory and say where the files landed.
1291
+ const outputDirectory = path.resolve(args[0] || path.join(os.tmpdir(), 'technical-diagrams-examples'));
75
1292
  fs.mkdirSync(outputDirectory, { recursive: true });
76
- const result = runRuntime([
77
- path.join(skillRoot, 'scripts/render-examples.mjs'),
78
- outputDirectory,
79
- ]);
1293
+ const result = runNode([path.join(skillRoot, 'scripts/render-examples.mjs'), outputDirectory], { cwd: skillRoot });
80
1294
  if (result.status !== 0) exitFrom(result);
1295
+ console.log(`\nRendered examples: ${outputDirectory}`);
81
1296
  }
82
1297
 
83
1298
  async function commandDoctor() {
84
1299
  const checks = [];
85
- const bunVersion = process.versions.bun || '0.0.0';
86
- const [bunMajor, bunMinor] = bunVersion.split('.').map(Number);
1300
+ const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
87
1301
  checks.push({
88
- label: `Bun v${bunVersion} (requires >=1.3)`,
89
- ok: bunMajor > 1 || (bunMajor === 1 && bunMinor >= 3),
1302
+ label: `Node.js v${process.versions.node} (requires >=18)`,
1303
+ ok: nodeMajor >= 18,
90
1304
  missing: 0,
91
1305
  failureLabel: 'unsupported',
92
1306
  });
@@ -105,11 +1319,56 @@ async function commandDoctor() {
105
1319
  missing: fs.existsSync(examplesRenderer) ? 0 : 1,
106
1320
  });
107
1321
 
108
- const isolatedConfig = path.join(skillRoot, 'bunfig.toml');
1322
+ const previewRuntime = path.join(skillRoot, 'bin/preview.mjs');
1323
+ checks.push({
1324
+ label: 'Live preview runtime',
1325
+ ok: fs.existsSync(previewRuntime),
1326
+ missing: fs.existsSync(previewRuntime) ? 0 : 1,
1327
+ });
1328
+
1329
+ const visualCheckRuntime = path.join(skillRoot, 'bin/visual-check.mjs');
1330
+ checks.push({
1331
+ label: 'Visual-check runtime',
1332
+ ok: fs.existsSync(visualCheckRuntime),
1333
+ missing: fs.existsSync(visualCheckRuntime) ? 0 : 1,
1334
+ });
1335
+
1336
+ const outputPathRuntime = path.join(skillRoot, 'renderers/shared/output-path.mjs');
1337
+ checks.push({
1338
+ label: 'Output path safety runtime',
1339
+ ok: fs.existsSync(outputPathRuntime),
1340
+ missing: fs.existsSync(outputPathRuntime) ? 0 : 1,
1341
+ });
1342
+
1343
+ const scenarioGuide = path.join(skillRoot, 'recipes/scenarios.mjs');
1344
+ checks.push({
1345
+ label: 'Scenario recipe guide',
1346
+ ok: fs.existsSync(scenarioGuide),
1347
+ missing: fs.existsSync(scenarioGuide) ? 0 : 1,
1348
+ });
1349
+
1350
+ const authoringReferences = [
1351
+ path.join(skillRoot, 'references', 'authoring-contract.md'),
1352
+ path.join(skillRoot, 'references', 'viewer-runtime.md'),
1353
+ path.join(skillRoot, 'references', 'delivery-contract.md'),
1354
+ ];
1355
+ const authoringReferencesMissing = authoringReferences.filter((file) => !fs.existsSync(file)).length;
1356
+ checks.push({
1357
+ label: 'Progressive authoring references',
1358
+ ok: authoringReferencesMissing === 0,
1359
+ missing: authoringReferencesMissing,
1360
+ });
1361
+
1362
+ const compareRuntime = path.join(skillRoot, 'delta/architecture-delta.mjs');
1363
+ const compareFixtures = [
1364
+ path.join(skillRoot, 'examples/checkout-platform.base.architecture.json'),
1365
+ path.join(skillRoot, 'examples/checkout-platform.head.architecture.json'),
1366
+ ];
1367
+ const compareMissing = [compareRuntime, ...compareFixtures].filter((file) => !fs.existsSync(file)).length;
109
1368
  checks.push({
110
- label: 'Isolated Bun configuration',
111
- ok: fs.existsSync(isolatedConfig),
112
- missing: fs.existsSync(isolatedConfig) ? 0 : 1,
1369
+ label: 'Architecture compare runtime and proof fixtures',
1370
+ ok: compareMissing === 0,
1371
+ missing: compareMissing,
113
1372
  });
114
1373
 
115
1374
  const validators = path.join(skillRoot, 'renderers/shared/generated-validators.mjs');
@@ -153,32 +1412,141 @@ async function commandDoctor() {
153
1412
  });
154
1413
  }
155
1414
 
156
- console.log('Technical diagram renderer doctor\n');
1415
+ console.log('Technical Diagrams doctor\n');
157
1416
  for (const check of checks) {
158
1417
  console.log(`[${check.ok ? 'ok' : (check.failureLabel || 'missing')}] ${check.label}`);
159
1418
  }
160
1419
 
161
- const runtimeFailed = checks[0].ok ? 0 : 1;
1420
+ const nodeFailed = checks[0].ok ? 0 : 1;
162
1421
  const missingFiles = checks.reduce((count, check) => count + check.missing, 0);
163
1422
  const invalidRuntime = checks.reduce((count, check) => count + (check.invalid || 0), 0);
164
- if (runtimeFailed === 0 && missingFiles === 0 && invalidRuntime === 0) {
165
- console.log('\nTechnical diagram renderer is ready.');
1423
+ if (nodeFailed === 0 && missingFiles === 0 && invalidRuntime === 0) {
1424
+ console.log('\nThe technical diagrams skill is ready.');
166
1425
  return;
167
1426
  }
168
1427
 
169
1428
  const problems = [];
170
- if (runtimeFailed) problems.push('Bun 1.3 or newer is required');
1429
+ if (nodeFailed) problems.push('Node.js 18 or newer is required');
171
1430
  if (missingFiles) problems.push(`${missingFiles} required file${missingFiles === 1 ? '' : 's'} missing`);
172
1431
  if (invalidRuntime) problems.push(`${invalidRuntime} runtime check${invalidRuntime === 1 ? '' : 's'} failed`);
173
- console.error(`\nTechnical diagram renderer is not ready: ${problems.join('; ')}.`);
1432
+ console.error(`\nThe technical diagrams skill is not ready: ${problems.join('; ')}.`);
174
1433
  process.exitCode = 1;
175
1434
  }
176
1435
 
1436
+ async function commandGuide(args) {
1437
+ let lang;
1438
+ let json = false;
1439
+ const queryParts = [];
1440
+
1441
+ for (let index = 0; index < args.length; index += 1) {
1442
+ const arg = args[index];
1443
+ if (arg === '--json') {
1444
+ json = true;
1445
+ } else if (arg === '--lang') {
1446
+ const value = args[index + 1];
1447
+ if (value !== 'en' && value !== 'zh') fail('--lang must be "en" or "zh".');
1448
+ lang = value;
1449
+ index += 1;
1450
+ } else if (arg.startsWith('--lang=')) {
1451
+ const value = arg.slice('--lang='.length);
1452
+ if (value !== 'en' && value !== 'zh') fail('--lang must be "en" or "zh".');
1453
+ lang = value;
1454
+ } else if (arg.startsWith('--')) {
1455
+ fail(`Unknown guide option "${arg}".`);
1456
+ } else {
1457
+ queryParts.push(arg);
1458
+ }
1459
+ }
1460
+
1461
+ const guidePath = path.join(skillRoot, 'recipes/scenarios.mjs');
1462
+ let guide;
1463
+ try {
1464
+ guide = await import(pathToFileURL(guidePath).href);
1465
+ } catch (error) {
1466
+ fail(`Could not load the scenario recipe guide: ${error.message}`, 1);
1467
+ }
1468
+
1469
+ const query = queryParts.join(' ').trim();
1470
+ if (!query) {
1471
+ const selectedLang = lang || 'en';
1472
+ if (json) {
1473
+ console.log(JSON.stringify({
1474
+ ok: true,
1475
+ mode: 'list',
1476
+ lang: selectedLang,
1477
+ recipes: guide.listScenarioRecipes(selectedLang),
1478
+ }, null, 2));
1479
+ } else {
1480
+ console.log(guide.formatScenarioList(selectedLang));
1481
+ }
1482
+ return;
1483
+ }
1484
+
1485
+ const result = guide.recommendScenario(query, lang ? { lang } : {});
1486
+ console.log(json ? JSON.stringify(result, null, 2) : guide.formatScenarioRecommendation(result));
1487
+ }
1488
+
1489
+ async function commandBrands(args) {
1490
+ const json = args.includes('--json');
1491
+ const unknown = args.filter((arg) => arg.startsWith('--') && arg !== '--json');
1492
+ if (unknown.length) fail(`Unknown brands option "${unknown[0]}".`);
1493
+ const positional = args.filter((arg) => arg !== '--json');
1494
+ if (positional[0] === 'capture') {
1495
+ if (positional.length !== 2) fail('Usage: diagrams brands capture <url> [--json]');
1496
+ const { captureBrandReference } = await import('../renderers/shared/brand-marks.mjs');
1497
+ let capture;
1498
+ try {
1499
+ capture = await captureBrandReference(positional[1]);
1500
+ } catch (error) {
1501
+ fail(error.message);
1502
+ }
1503
+ const result = {
1504
+ schemaVersion: 1,
1505
+ ok: true,
1506
+ command: 'brands capture',
1507
+ brand: capture.brand,
1508
+ evidence: {
1509
+ status: capture.resolved.status,
1510
+ source: capture.resolved.sourceUrl,
1511
+ ...(capture.resolved.sha256 ? { sha256: capture.resolved.sha256 } : {}),
1512
+ ...(capture.resolved.contentType ? { contentType: capture.resolved.contentType } : {}),
1513
+ },
1514
+ };
1515
+ console.log(json ? JSON.stringify(result, null, 2) : JSON.stringify(result.brand));
1516
+ return;
1517
+ }
1518
+ const query = positional.join(' ').trim();
1519
+ const { listBrandMarks } = await import('../renderers/shared/brand-marks.mjs');
1520
+ const marks = listBrandMarks(query);
1521
+ if (json) {
1522
+ console.log(JSON.stringify({
1523
+ schemaVersion: 1,
1524
+ ok: true,
1525
+ command: 'brands',
1526
+ query,
1527
+ count: marks.length,
1528
+ marks,
1529
+ fallback: 'Run "diagrams brands capture <url> --json", then use the returned digest-pinned brand value.',
1530
+ }, null, 2));
1531
+ return;
1532
+ }
1533
+ if (!marks.length) {
1534
+ console.log(`No built-in brand matched "${query}". Run "diagrams brands capture <url> --json", then use the returned digest-pinned brand value.`);
1535
+ return;
1536
+ }
1537
+ const grouped = Map.groupBy
1538
+ ? Map.groupBy(marks, (mark) => mark.category)
1539
+ : marks.reduce((map, mark) => map.set(mark.category, [...(map.get(mark.category) || []), mark]), new Map());
1540
+ for (const [category, entries] of grouped) {
1541
+ console.log(`${category}: ${entries.map((mark) => mark.id).join(', ')}`);
1542
+ }
1543
+ }
1544
+
177
1545
  function commandDemo(args) {
178
1546
  if (args.length > 1) fail(usage());
179
1547
 
180
1548
  const outputDirectory = path.resolve(args[0] || process.cwd());
181
- const output = path.join(outputDirectory, 'technical-diagram-demo.html');
1549
+ const output = path.join(outputDirectory, 'technical-diagrams-demo.html');
182
1550
  const input = path.join(skillRoot, 'examples/web-app.architecture.json');
183
1551
 
184
1552
  try {
@@ -187,63 +1555,471 @@ function commandDemo(args) {
187
1555
  fail(`Could not create demo directory "${outputDirectory}": ${error.message}`, 1);
188
1556
  }
189
1557
 
190
- const result = runRuntime([rendererPath('architecture'), input, output]);
1558
+ const result = runNode([rendererPath('architecture'), input, output]);
191
1559
  if (result.status !== 0) exitFrom(result);
192
1560
 
193
1561
  console.log(`\nDemo ready: ${output}`);
194
1562
  console.log('Next: open the HTML in your browser, then render your own diagram:');
195
- console.log(' technical-diagrams render architecture <input.json> <output.html>');
1563
+ console.log(' diagrams render architecture <input.json> <output.html>');
1564
+ }
1565
+
1566
+ function migrationPathDiagnostics(error, sourcePath, destinationPath) {
1567
+ if (Array.isArray(error?.archifyDiagnostics) && error.archifyDiagnostics.length) {
1568
+ return error.archifyDiagnostics.map((entry) => ({
1569
+ ...entry,
1570
+ subject: { ...(entry.subject || {}) },
1571
+ evidence: { ...(entry.evidence || {}) },
1572
+ supportedFixes: [...(entry.supportedFixes || [])],
1573
+ }));
1574
+ }
1575
+ return [diagnostic({
1576
+ code: 'migration/path-preflight',
1577
+ message: 'Could not verify that the workflow migration paths are distinct.',
1578
+ subject: { source: sourcePath, destination: destinationPath },
1579
+ evidence: {
1580
+ ...(error?.code ? { systemCode: error.code } : {}),
1581
+ reason: error?.message || String(error),
1582
+ },
1583
+ supportedFixes: ['remove unsafe path aliases or choose a different destination path'],
1584
+ })];
1585
+ }
1586
+
1587
+ function migrationReport({
1588
+ ok,
1589
+ sourcePath,
1590
+ destinationPath,
1591
+ sourceBytes,
1592
+ destinationBytes,
1593
+ fromSchemaVersion,
1594
+ preExistingDiagnostics = [],
1595
+ migrationDiagnostics = [],
1596
+ newSchemaDiagnostics = [],
1597
+ changedCoordinates = [],
1598
+ oldRequiredViewBox = null,
1599
+ newRequiredViewBox = null,
1600
+ }) {
1601
+ const report = {
1602
+ ok,
1603
+ command: 'migrate',
1604
+ type: 'workflow',
1605
+ source: {
1606
+ path: sourcePath,
1607
+ ...(sourceBytes ? {
1608
+ sha256: createHash('sha256').update(sourceBytes).digest('hex'),
1609
+ bytes: sourceBytes.length,
1610
+ } : {}),
1611
+ },
1612
+ destination: {
1613
+ path: destinationPath,
1614
+ ...(destinationBytes ? {
1615
+ sha256: createHash('sha256').update(destinationBytes).digest('hex'),
1616
+ bytes: destinationBytes.length,
1617
+ } : {}),
1618
+ },
1619
+ fromSchemaVersion: fromSchemaVersion ?? null,
1620
+ toSchemaVersion: 2,
1621
+ preExistingDiagnostics,
1622
+ migrationDiagnostics,
1623
+ newSchemaDiagnostics,
1624
+ changedCoordinates,
1625
+ oldRequiredViewBox,
1626
+ newRequiredViewBox,
1627
+ };
1628
+ if (!ok) {
1629
+ report.diagnostics = [
1630
+ ...migrationDiagnostics,
1631
+ ...newSchemaDiagnostics,
1632
+ ...preExistingDiagnostics,
1633
+ ];
1634
+ if (!report.diagnostics.length) {
1635
+ report.diagnostics.push(diagnostic({
1636
+ code: 'migration/internal',
1637
+ message: 'Workflow migration failed without a classified diagnostic.',
1638
+ }));
1639
+ }
1640
+ report.error = report.diagnostics[0].message;
1641
+ }
1642
+ return report;
1643
+ }
1644
+
1645
+ function extractMigrationOptions(args) {
1646
+ const positional = [];
1647
+ let json = false;
1648
+ let toSchema;
1649
+ for (let index = 0; index < args.length; index += 1) {
1650
+ const arg = args[index];
1651
+ if (arg === '--json') {
1652
+ json = true;
1653
+ continue;
1654
+ }
1655
+ if (arg === '--to-schema') {
1656
+ toSchema = args[index + 1];
1657
+ if (!toSchema || toSchema.startsWith('--')) fail('--to-schema requires a schema version.');
1658
+ index += 1;
1659
+ continue;
1660
+ }
1661
+ if (arg.startsWith('--to-schema=')) {
1662
+ toSchema = arg.slice('--to-schema='.length);
1663
+ if (!toSchema) fail('--to-schema requires a schema version.');
1664
+ continue;
1665
+ }
1666
+ if (arg.startsWith('--')) fail(`Unknown migrate option "${arg}".`);
1667
+ positional.push(arg);
1668
+ }
1669
+ return { positional, json, toSchema };
1670
+ }
1671
+
1672
+ async function commandMigrate(args) {
1673
+ const options = extractMigrationOptions(args);
1674
+ const [type, sourceArgument, destinationArgument] = options.positional;
1675
+ if (
1676
+ type !== 'workflow'
1677
+ || !sourceArgument
1678
+ || !destinationArgument
1679
+ || options.positional.length !== 3
1680
+ || options.toSchema !== '2'
1681
+ ) {
1682
+ fail('Usage: diagrams migrate workflow <old.json> <new.json> --to-schema 2 [--json]');
1683
+ }
1684
+
1685
+ const sourcePath = path.resolve(sourceArgument);
1686
+ const destinationPath = path.resolve(destinationArgument);
1687
+ let sourceBytes;
1688
+ let sourceDocument;
1689
+ const reportMigrationFailure = ({ status = 1, ...details }) => {
1690
+ const report = migrationReport({
1691
+ ...details,
1692
+ ok: false,
1693
+ sourcePath,
1694
+ destinationPath,
1695
+ sourceBytes,
1696
+ fromSchemaVersion: sourceDocument?.schema_version,
1697
+ });
1698
+ if (options.json) console.log(JSON.stringify(report, null, 2));
1699
+ else console.error(formatDiagnostics(report.error, report.diagnostics));
1700
+ process.exitCode = status;
1701
+ };
1702
+ try {
1703
+ sourceBytes = fs.readFileSync(sourcePath);
1704
+ sourceDocument = JSON.parse(sourceBytes.toString('utf8'));
1705
+ } catch (error) {
1706
+ reportMigrationFailure({
1707
+ preExistingDiagnostics: [inputDiagnostic(error, sourcePath)],
1708
+ });
1709
+ return;
1710
+ }
1711
+ // Unlike render/validate, migrate has no --quality override. Pin every stage
1712
+ // to the document's durable policy and scrub any ambient profile from the
1713
+ // staged renderer by passing this value explicitly.
1714
+ const activeQualityProfile = sourceDocument?.meta?.quality_profile || 'standard';
1715
+
1716
+ const { pathsAlias } = await import('../renderers/shared/output-path.mjs');
1717
+ let sourceDestinationAlias;
1718
+ try {
1719
+ sourceDestinationAlias = pathsAlias(sourcePath, destinationPath);
1720
+ } catch (error) {
1721
+ reportMigrationFailure({
1722
+ migrationDiagnostics: migrationPathDiagnostics(error, sourcePath, destinationPath),
1723
+ });
1724
+ return;
1725
+ }
1726
+ if (sourceDestinationAlias) {
1727
+ reportMigrationFailure({
1728
+ migrationDiagnostics: [diagnostic({
1729
+ code: 'migration/source-destination',
1730
+ message: 'Workflow migration source and destination must be different files.',
1731
+ subject: { source: sourcePath, destination: destinationPath },
1732
+ supportedFixes: ['choose a different destination path and keep the source unchanged'],
1733
+ })],
1734
+ });
1735
+ return;
1736
+ }
1737
+
1738
+ const { migrateWorkflowDocument, serializeMigratedWorkflow } = await import('../migrations/workflow-v2.mjs');
1739
+ let migration;
1740
+ try {
1741
+ migration = migrateWorkflowDocument(sourceDocument);
1742
+ } catch (error) {
1743
+ migration = {
1744
+ ok: false,
1745
+ migrationDiagnostics: [diagnostic({
1746
+ code: 'migration/internal',
1747
+ message: 'Workflow migration failed unexpectedly.',
1748
+ evidence: { reason: error.message },
1749
+ supportedFixes: ['report the source workflow and this diagnostic to the Archify maintainers'],
1750
+ })],
1751
+ };
1752
+ }
1753
+
1754
+ if (!migration.ok) {
1755
+ reportMigrationFailure(migration);
1756
+ return;
1757
+ }
1758
+
1759
+ if (fs.existsSync(destinationPath) && !fs.lstatSync(destinationPath).isFile()) {
1760
+ reportMigrationFailure({
1761
+ ...migration,
1762
+ migrationDiagnostics: [...migration.migrationDiagnostics, diagnostic({
1763
+ code: 'migration/destination-type',
1764
+ message: 'Workflow migration destination must be a regular file path.',
1765
+ subject: { destination: destinationPath },
1766
+ supportedFixes: ['choose a destination path that is absent or names a regular file'],
1767
+ })],
1768
+ });
1769
+ return;
1770
+ }
1771
+
1772
+ const destinationDirectory = path.dirname(destinationPath);
1773
+ let stagingDirectory;
1774
+ try {
1775
+ fs.mkdirSync(destinationDirectory, { recursive: true });
1776
+ stagingDirectory = fs.mkdtempSync(path.join(destinationDirectory, '.archify-migration-'));
1777
+ } catch (error) {
1778
+ reportMigrationFailure({
1779
+ ...migration,
1780
+ migrationDiagnostics: [...migration.migrationDiagnostics, diagnostic({
1781
+ code: 'migration/prepare-destination',
1782
+ message: 'Could not prepare the workflow migration destination.',
1783
+ subject: { destination: destinationPath },
1784
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
1785
+ supportedFixes: ['choose a writable destination directory'],
1786
+ })],
1787
+ });
1788
+ return;
1789
+ }
1790
+
1791
+ const candidatePath = path.join(stagingDirectory, 'candidate.workflow.json');
1792
+ const artifactPath = path.join(stagingDirectory, 'migration-check.html');
1793
+ const destinationBytes = Buffer.from(serializeMigratedWorkflow(migration.document));
1794
+ try {
1795
+ fs.writeFileSync(candidatePath, destinationBytes, { flag: 'wx' });
1796
+ const render = runNode([rendererPath('workflow'), candidatePath, artifactPath], {
1797
+ stdio: 'pipe',
1798
+ env: rendererEnv(activeQualityProfile, undefined, true),
1799
+ });
1800
+ if (render.status !== 0) {
1801
+ const failure = rendererFailure(render);
1802
+ reportMigrationFailure({
1803
+ ...migration,
1804
+ newSchemaDiagnostics: [...migration.newSchemaDiagnostics, ...failure.diagnostics],
1805
+ status: render.status ?? 1,
1806
+ });
1807
+ return;
1808
+ }
1809
+
1810
+ const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), artifactPath], {
1811
+ stdio: 'pipe',
1812
+ });
1813
+ if (check.status !== 0) {
1814
+ let checker;
1815
+ try {
1816
+ checker = JSON.parse(check.stdout);
1817
+ } catch {
1818
+ checker = null;
1819
+ }
1820
+ reportMigrationFailure({
1821
+ ...migration,
1822
+ newSchemaDiagnostics: [
1823
+ ...migration.newSchemaDiagnostics,
1824
+ ...checkerDiagnostics(checker),
1825
+ ],
1826
+ status: check.status ?? 1,
1827
+ });
1828
+ return;
1829
+ }
1830
+
1831
+ if (pathsAlias(sourcePath, destinationPath)) {
1832
+ reportMigrationFailure({
1833
+ ...migration,
1834
+ migrationDiagnostics: [...migration.migrationDiagnostics, diagnostic({
1835
+ code: 'migration/source-destination',
1836
+ message: 'Workflow migration source and destination resolved to the same file before commit.',
1837
+ subject: { source: sourcePath, destination: destinationPath },
1838
+ supportedFixes: ['choose a different destination path and retry'],
1839
+ })],
1840
+ });
1841
+ return;
1842
+ }
1843
+ const currentSourceBytes = fs.readFileSync(sourcePath);
1844
+ if (!currentSourceBytes.equals(sourceBytes)) {
1845
+ reportMigrationFailure({
1846
+ ...migration,
1847
+ migrationDiagnostics: [...migration.migrationDiagnostics, diagnostic({
1848
+ code: 'migration/source-changed',
1849
+ message: 'Workflow migration source changed while the destination was being verified.',
1850
+ subject: { source: sourcePath },
1851
+ supportedFixes: ['retry the migration from a stable workflow source file'],
1852
+ })],
1853
+ });
1854
+ return;
1855
+ }
1856
+
1857
+ fs.renameSync(candidatePath, destinationPath);
1858
+ const report = migrationReport({
1859
+ ...migration,
1860
+ sourcePath,
1861
+ destinationPath,
1862
+ sourceBytes,
1863
+ destinationBytes,
1864
+ fromSchemaVersion: sourceDocument.schema_version,
1865
+ });
1866
+ if (options.json) console.log(JSON.stringify(report, null, 2));
1867
+ else if (sourceDocument.schema_version === 1) {
1868
+ console.log(`migrated workflow schema v1→v2: ${sourcePath} → ${destinationPath}`);
1869
+ } else {
1870
+ console.log(`verified workflow schema v2 migration: ${sourcePath} → ${destinationPath}`);
1871
+ }
1872
+ } catch (error) {
1873
+ const migrationDiagnostics = Array.isArray(error?.archifyDiagnostics)
1874
+ ? migrationPathDiagnostics(error, sourcePath, destinationPath)
1875
+ : [diagnostic({
1876
+ code: 'migration/commit',
1877
+ message: 'Could not commit the verified workflow migration.',
1878
+ subject: { destination: destinationPath },
1879
+ evidence: { ...(error?.code ? { systemCode: error.code } : {}), reason: error.message },
1880
+ supportedFixes: ['choose a writable regular-file destination and retry'],
1881
+ })];
1882
+ reportMigrationFailure({
1883
+ ...migration,
1884
+ migrationDiagnostics: [...migration.migrationDiagnostics, ...migrationDiagnostics],
1885
+ });
1886
+ } finally {
1887
+ try {
1888
+ fs.rmSync(stagingDirectory, { recursive: true, force: true });
1889
+ } catch (error) {
1890
+ console.error(`Warning: could not remove workflow migration staging directory "${stagingDirectory}": ${error.message}`);
1891
+ }
1892
+ }
196
1893
  }
197
1894
 
198
1895
  function commandValidate(args) {
1896
+ const qualityArgs = extractQualityArgs(args);
1897
+ const repoArgs = extractRepoRootArgs(qualityArgs.rest);
1898
+ args = repoArgs.rest;
1899
+ const quality = qualityArgs.quality;
1900
+ const repoRoot = repoArgs.repoRoot;
1901
+ const knownOptions = new Set(['--json', '--layout-json']);
1902
+ const unknown = args.filter((arg) => arg.startsWith('--') && !knownOptions.has(arg));
1903
+ if (unknown.length) rejectCliArgument(`Unknown validate option "${unknown[0]}".`, {
1904
+ code: 'cli/unknown-option',
1905
+ subject: { option: unknown[0] },
1906
+ supportedFixes: ['remove the unknown option and retry'],
1907
+ });
199
1908
  const json = args.includes('--json');
200
1909
  const layoutJson = args.includes('--layout-json');
201
- const rest = args.filter((arg) => arg !== '--json' && arg !== '--layout-json');
1910
+ const rest = args.filter((arg) => !knownOptions.has(arg));
202
1911
  const [type, input] = rest;
203
- if (!type || !input) fail(usage());
1912
+ if (!type || !input || rest.length !== 2) rejectCliArgument(usage(), {
1913
+ code: 'cli/usage',
1914
+ supportedFixes: ['use: diagrams validate <type> <input.json> [options]'],
1915
+ });
1916
+ assertEvidenceType(type, repoRoot);
204
1917
  const renderer = rendererPath(type);
205
1918
 
1919
+ if (layoutJson && !['architecture', 'workflow'].includes(type)) {
1920
+ rejectCliArgument('--layout-json is currently supported for architecture and workflow diagrams only.', {
1921
+ code: 'cli/unsupported-option',
1922
+ subject: { option: '--layout-json', type },
1923
+ supportedFixes: ['remove --layout-json or use an architecture or workflow diagram'],
1924
+ });
1925
+ }
1926
+
206
1927
  if (layoutJson) {
207
- if (type !== 'architecture') {
208
- fail('--layout-json is currently supported for architecture diagrams only.');
209
- }
210
- const result = runRuntime([renderer, input, '/dev/null', '--layout-json'], { stdio: 'pipe' });
1928
+ // Layout mode emits JSON without writing HTML; keep its unused target typed.
1929
+ const layoutOutput = path.join(os.tmpdir(), `archify-layout-${process.pid}-${type}.html`);
1930
+ const result = runNode([renderer, input, layoutOutput, '--layout-json'], {
1931
+ stdio: 'pipe',
1932
+ env: rendererEnv(quality, repoRoot, true),
1933
+ });
211
1934
  if (result.status !== 0) {
212
- if (result.stderr) process.stderr.write(result.stderr);
213
- if (result.stdout) process.stdout.write(result.stdout);
214
- process.exit(result.status ?? 1);
1935
+ try {
1936
+ const receipt = JSON.parse(result.stdout);
1937
+ if (receipt?.contract && Array.isArray(receipt.diagnostics)) {
1938
+ process.stdout.write(`${JSON.stringify(receipt, null, 2)}\n`);
1939
+ process.exitCode = result.status ?? 1;
1940
+ return;
1941
+ }
1942
+ } catch {
1943
+ // Fall through to the renderer failure contract when no compiler
1944
+ // receipt was produced (for example, input JSON could not be read).
1945
+ }
1946
+ const failure = rendererFailure(result);
1947
+ reportValidateFailure({
1948
+ json,
1949
+ stage: failure.diagnostics.some((entry) => entry.code.startsWith('input/')) ? 'input' : 'render',
1950
+ type,
1951
+ input: path.resolve(input),
1952
+ error: failure.error,
1953
+ diagnostics: failure.diagnostics,
1954
+ status: result.status ?? 1,
1955
+ });
1956
+ return;
215
1957
  }
216
1958
  process.stdout.write(result.stdout);
217
1959
  return;
218
1960
  }
219
1961
 
220
- const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'technical-diagrams-validate-'));
1962
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'archify-validate-'));
221
1963
  const out = path.join(tmp, `${type}.html`);
222
1964
  let exitCode = 0;
223
1965
 
224
1966
  try {
225
- const render = runRuntime([renderer, input, out], { stdio: 'pipe' });
1967
+ const render = runNode([renderer, input, out], {
1968
+ stdio: 'pipe',
1969
+ env: rendererEnv(quality, repoRoot, true),
1970
+ });
226
1971
  if (render.status !== 0) {
227
- if (render.stderr) process.stderr.write(render.stderr);
228
- if (render.stdout) process.stdout.write(render.stdout);
1972
+ const failure = rendererFailure(render);
1973
+ reportValidateFailure({
1974
+ json,
1975
+ stage: failure.diagnostics.some((entry) => entry.code.startsWith('input/')) ? 'input' : 'render',
1976
+ type,
1977
+ input: path.resolve(input),
1978
+ error: failure.error,
1979
+ diagnostics: failure.diagnostics,
1980
+ status: render.status ?? 1,
1981
+ });
229
1982
  exitCode = render.status ?? 1;
230
1983
  } else {
231
- const check = runRuntime([path.join(skillRoot, 'scripts/check-render-output.mjs'), out], { stdio: 'pipe' });
1984
+ const check = runNode([path.join(skillRoot, 'scripts/check-render-output.mjs'), out], { stdio: 'pipe' });
232
1985
  if (check.status !== 0) {
233
- if (check.stdout) process.stdout.write(check.stdout);
234
- if (check.stderr) process.stderr.write(check.stderr);
1986
+ let checker;
1987
+ try {
1988
+ checker = JSON.parse(check.stdout);
1989
+ checker.file = path.resolve(input);
1990
+ } catch {
1991
+ checker = { ok: false, diagnostic: 'Artifact checker failed without a parseable receipt.' };
1992
+ }
1993
+ reportValidateFailure({
1994
+ json,
1995
+ stage: 'check',
1996
+ type,
1997
+ input: path.resolve(input),
1998
+ error: 'Final artifact check failed.',
1999
+ diagnostics: checkerDiagnostics(checker),
2000
+ checker,
2001
+ status: check.status ?? 1,
2002
+ });
235
2003
  exitCode = check.status ?? 1;
236
2004
  } else {
237
2005
  const result = JSON.parse(check.stdout);
2006
+ const engineeringProfile = engineeringProfileFromArtifact(fs.readFileSync(out));
238
2007
  if (json) {
239
2008
  console.log(JSON.stringify({
2009
+ schemaVersion: 1,
240
2010
  ok: true,
2011
+ command: 'validate',
241
2012
  type,
242
2013
  input: path.resolve(input),
243
2014
  checks: result.checks,
2015
+ composition: result.composition,
2016
+ ...(engineeringProfile ? { engineeringProfile } : {}),
244
2017
  }, null, 2));
245
2018
  } else {
246
- console.log(`ok ${type} ${path.resolve(input)} (${result.checks.length} checks)`);
2019
+ const engineering = engineeringProfile
2020
+ ? `; engineering ${engineeringProfile}: pass`
2021
+ : '';
2022
+ console.log(`ok ${type} ${path.resolve(input)} (${result.checks.length} artifact checks; composition ${result.composition.profile}: ${result.composition.summary.errors} errors, ${result.composition.summary.warnings} warnings${engineering})`);
247
2023
  }
248
2024
  }
249
2025
  }
@@ -251,39 +2027,72 @@ function commandValidate(args) {
251
2027
  fs.rmSync(tmp, { recursive: true, force: true });
252
2028
  }
253
2029
 
254
- if (exitCode !== 0) process.exit(exitCode);
2030
+ if (exitCode !== 0) process.exitCode = exitCode;
255
2031
  }
256
2032
 
257
2033
  const [command, ...args] = process.argv.slice(2);
258
2034
 
259
- switch (command) {
260
- case undefined:
261
- case '-h':
262
- case '--help':
263
- case 'help':
264
- console.log(usage());
265
- break;
266
- case 'render':
267
- commandRender(args);
268
- break;
269
- case 'validate':
270
- commandValidate(args);
271
- break;
272
- case 'inspect':
273
- commandValidate([...args, '--layout-json']);
274
- break;
275
- case 'check':
276
- commandCheck(args);
277
- break;
278
- case 'examples':
279
- commandExamples(args);
280
- break;
281
- case 'doctor':
282
- await commandDoctor();
283
- break;
284
- case 'demo':
285
- commandDemo(args);
286
- break;
287
- default:
288
- fail(`Unknown command "${command}".\n\n${usage()}`);
2035
+ try {
2036
+ switch (command) {
2037
+ case undefined:
2038
+ case '-h':
2039
+ case '--help':
2040
+ case 'help':
2041
+ console.log(usage());
2042
+ break;
2043
+ case 'render':
2044
+ commandRender(args);
2045
+ break;
2046
+ case 'compare':
2047
+ await commandCompare(args);
2048
+ break;
2049
+ case 'deliver':
2050
+ await commandDeliver(args);
2051
+ break;
2052
+ case 'preview':
2053
+ await commandPreview(args);
2054
+ break;
2055
+ case 'validate':
2056
+ commandValidate(args);
2057
+ break;
2058
+ case 'migrate':
2059
+ await commandMigrate(args);
2060
+ break;
2061
+ case 'inspect':
2062
+ if (args[0] !== 'architecture') {
2063
+ fail('inspect is currently supported for architecture diagrams only.');
2064
+ }
2065
+ commandValidate([...args, '--layout-json']);
2066
+ break;
2067
+ case 'check':
2068
+ commandCheck(args);
2069
+ break;
2070
+ case 'visual-check':
2071
+ await commandVisualCheck(args);
2072
+ break;
2073
+ case 'guide':
2074
+ await commandGuide(args);
2075
+ break;
2076
+ case 'brands':
2077
+ await commandBrands(args);
2078
+ break;
2079
+ case 'examples':
2080
+ commandExamples(args);
2081
+ break;
2082
+ case 'doctor':
2083
+ await commandDoctor();
2084
+ break;
2085
+ case 'demo':
2086
+ commandDemo(args);
2087
+ break;
2088
+ default:
2089
+ fail(`Unknown command "${command}".\n\n${usage()}`);
2090
+ }
2091
+ } catch (error) {
2092
+ if (!error.archifyArgument) throw error;
2093
+ if (['validate', 'deliver'].includes(command) && args.includes('--json')) {
2094
+ reportArtifactArgumentFailure(command, error);
2095
+ } else {
2096
+ fail(error.message);
2097
+ }
289
2098
  }