@brainwav/diagram 1.0.7 → 1.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 (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +180 -1760
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
@@ -0,0 +1,522 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const chalk = require('chalk');
5
+ const {
6
+ probeCapabilities,
7
+ buildConfidenceReport,
8
+ writeConfidenceReport,
9
+ shouldFailStrictConfidence,
10
+ } = require('../confidence/pipeline');
11
+ const { generateDiagramArtifact } = require('../core/analysis-generation');
12
+ const {
13
+ ALLOWED_THEMES,
14
+ applyDiagramRcDefaults,
15
+ createMermaidUrl,
16
+ findClosestMatch,
17
+ formatSuggestion,
18
+ getDiagramRcFromProgram,
19
+ maybeWriteArchitectureIR,
20
+ normalizeThemeOption,
21
+ openPreviewUrl,
22
+ resolveRootPathOrExit,
23
+ runAnalysisPipeline,
24
+ runMermaidCli,
25
+ validateOutputPath,
26
+ } = require('./shared');
27
+ const { buildMachineEnvelope } = require('./output');
28
+
29
+ function shellArg(value) {
30
+ const text = String(value);
31
+ if (/^[A-Za-z0-9_./:=,-]+$/.test(text)) return text;
32
+ return `'${text.replace(/'/g, "'\\''")}'`;
33
+ }
34
+
35
+ function buildGenerateHintArgs(options, targetPath = '.', extraArgs = []) {
36
+ const args = ['archscope', 'generate', targetPath || '.', '--type', options.type || 'architecture'];
37
+ if (options.focus) args.push('--focus', options.focus);
38
+ if (options.patterns) args.push('--patterns', options.patterns);
39
+ if (options.exclude) args.push('--exclude', options.exclude);
40
+ if (options.maxFiles) args.push('--max-files', options.maxFiles);
41
+ if (options.analyzer && options.analyzer !== 'default') args.push('--analyzer', options.analyzer);
42
+ args.push(...extraArgs);
43
+ return args;
44
+ }
45
+
46
+ function buildGenerateHint(options, targetPath = '.', extraArgs = []) {
47
+ const args = buildGenerateHintArgs(options, targetPath, extraArgs);
48
+ return args.map(shellArg).join(' ');
49
+ }
50
+
51
+ function buildSaveHint(options, targetPath = '.') {
52
+ const args = buildGenerateHintArgs(options, targetPath);
53
+ args.push('--output', 'diagram.svg');
54
+ return args.map(shellArg).join(' ');
55
+ }
56
+
57
+ function cleanupTempDirectory(tempDir) {
58
+ if (!tempDir || !fs.existsSync(tempDir)) return;
59
+ try {
60
+ fs.rmSync(tempDir, { recursive: true, force: true });
61
+ } catch (cleanupError) {
62
+ if (process.env.DEBUG) {
63
+ console.error(chalk.gray(`Temp cleanup failed for "${tempDir}": ${cleanupError.message}`));
64
+ }
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Validate Mermaid diagram text for common syntax issues and, when possible, via the Mermaid CLI.
70
+ *
71
+ * Performs line-level basic checks (unbalanced double quotes and empty `[]` node labels),
72
+ * verifies that the first non-comment line declares a recognised diagram type, and — if available —
73
+ * attempts full rendering validation using the Mermaid CLI (`mmdc`).
74
+ *
75
+ * @param {string} mermaid - The Mermaid diagram source to validate.
76
+ * @param {string} [theme='default'] - Theme to inject when invoking the Mermaid CLI for validation.
77
+ * @param {boolean} [allowAutoInstall=false] - If `true`, allow the helper that runs `mmdc` to attempt automatic installation of required CLI packages.
78
+ * @returns {{ valid: boolean, errors: Array<{line:number,message:string}>, meta: { mode: string, fallbackUsed: boolean, fallbackReasons: string[], cliValidation: { attempted: boolean, success: boolean, error: string|null } } }}
79
+ * An object describing validation outcome:
80
+ * - `valid`: `true` when no validation errors were found.
81
+ * - `errors`: list of detected issues with `line` and `message`.
82
+ * - `meta.mode`: validation mode used (`'basic'` or `'mmdc'`).
83
+ * - `meta.fallbackUsed`: `true` when CLI validation was attempted and failed, causing a fallback to basic checks.
84
+ * - `meta.fallbackReasons`: list of fallback reason identifiers.
85
+ * - `meta.cliValidation`: details about the CLI validation attempt.
86
+ */
87
+ function validateMermaidSyntax(mermaid, theme = 'default', allowAutoInstall = false) {
88
+ const result = {
89
+ valid: true,
90
+ errors: [],
91
+ meta: {
92
+ mode: 'basic',
93
+ fallbackUsed: false,
94
+ fallbackReasons: [],
95
+ cliValidation: {
96
+ attempted: false,
97
+ success: false,
98
+ error: null,
99
+ },
100
+ },
101
+ };
102
+
103
+ const lines = mermaid.split('\n');
104
+
105
+ for (let i = 0; i < lines.length; i += 1) {
106
+ const line = lines[i];
107
+ const lineNum = i + 1;
108
+ const quoteCount = (line.match(/"/g) || []).length;
109
+ if (quoteCount % 2 !== 0) {
110
+ result.errors.push({ line: lineNum, message: 'Unbalanced quotes in label' });
111
+ result.valid = false;
112
+ }
113
+ if (/\[\s*\]/.test(line) && !line.includes('%%')) {
114
+ result.errors.push({ line: lineNum, message: 'Empty node label []' });
115
+ result.valid = false;
116
+ }
117
+ }
118
+
119
+ const firstNonCommentLine = lines.find((line) => !line.trim().startsWith('%%'));
120
+ const validDiagramTypes = [
121
+ 'graph',
122
+ 'flowchart',
123
+ 'sequenceDiagram',
124
+ 'classDiagram',
125
+ 'erDiagram',
126
+ 'gantt',
127
+ 'pie',
128
+ 'journey',
129
+ 'gitGraph',
130
+ 'mindmap',
131
+ 'timeline',
132
+ 'architecture-beta',
133
+ 'C4Context',
134
+ ];
135
+ const hasValidType = validDiagramTypes.some((type) => firstNonCommentLine?.trim().startsWith(type));
136
+
137
+ if (!hasValidType && firstNonCommentLine) {
138
+ result.errors.push({ line: 1, message: 'Missing or invalid diagram type declaration' });
139
+ result.valid = false;
140
+ }
141
+
142
+ let tempDir;
143
+ try {
144
+ result.meta.cliValidation.attempted = true;
145
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diagram-validate-'));
146
+ const tempFile = path.join(tempDir, 'validate.mmd');
147
+ const tempOutput = path.join(tempDir, 'validate.svg');
148
+ fs.writeFileSync(tempFile, `%%{init: {'theme': '${theme}'}}%%\n${mermaid}`);
149
+ runMermaidCli(
150
+ ['@mermaid-js/mermaid-cli', 'mmdc', '-i', tempFile, '-o', tempOutput, '-b', 'transparent'],
151
+ { allowAutoInstall }
152
+ );
153
+ result.meta.mode = 'mmdc';
154
+ result.meta.cliValidation.success = true;
155
+ } catch (error) {
156
+ result.meta.fallbackUsed = true;
157
+ result.meta.fallbackReasons.push('mmdc_unavailable_or_failed');
158
+ result.meta.cliValidation.error = error.message || String(error);
159
+ if (process.env.DEBUG) {
160
+ console.error(chalk.gray('Mermaid CLI not available for validation, using basic checks only'));
161
+ }
162
+ } finally {
163
+ cleanupTempDirectory(tempDir);
164
+ }
165
+
166
+ return result;
167
+ }
168
+
169
+ /**
170
+ * Register the `generate [path]` CLI command and its options on the provided Commander program.
171
+ *
172
+ * The command handles analysis, Mermaid generation, optional validation, confidence/capability
173
+ * checks, rendering to files (or printing as Mermaid source), and related output behaviours.
174
+ *
175
+ * @param {import('commander').Command} program - Commander `Command` instance to extend with the `generate` command; the function mutates this instance.
176
+ */
177
+ function registerGenerateCommand(program) {
178
+ program
179
+ .command('generate [path]')
180
+ .description('Generate a diagram')
181
+ .option('-t, --type <type>', 'Diagram type: architecture, sequence, dependency, class, flow, database, erd, user, events, auth, security, agent, c4context, rag', 'architecture')
182
+ .option('-f, --focus <module>', 'Focus on specific module')
183
+ .option('-o, --output <file>', 'Output file (SVG/PNG)')
184
+ .option('--format <type>', 'Output format (text, json)', 'text')
185
+ .option('--force', 'Overwrite output file if it exists', false)
186
+ .option('-q, --quiet', 'Suppress non-essential logging', false)
187
+ .option('-m, --max-files <n>', 'Max files to analyze')
188
+ .option('-p, --patterns <list>', 'File patterns (comma-separated)')
189
+ .option('-e, --exclude <list>', 'Exclude patterns')
190
+ .option('--analyzer <name>', 'Analyzer plugin to use', 'default')
191
+ .option('--emit-ir', 'Write typed architecture IR artifact', false)
192
+ .option('--incremental', 'Use incremental cache when available', false)
193
+ .option('--theme <theme>', 'Theme: default, dark, forest, neutral, light')
194
+ .option('--validate', 'Validate Mermaid syntax', false)
195
+ .option('--fail-on-validation-error', 'Exit with error if validation fails (requires --validate)', false)
196
+ .option('--allow-auto-install', 'Allow npx to auto-install missing Mermaid CLI dependencies', false)
197
+ .option('--confidence-report', 'Write confidence report artifact', false)
198
+ .option('--strict-confidence', 'Fail with exit code 1 when confidence checks degrade', false)
199
+ .option('--capability-check-only', 'Run only capability checks and confidence evaluation', false)
200
+ .option('--deterministic', 'Use deterministic machine output', false)
201
+ .option('--open', 'Open in browser')
202
+ .action(async (targetPath, rawOptions) => {
203
+ const options = applyDiagramRcDefaults(rawOptions, getDiagramRcFromProgram(program), ['patterns', 'exclude', 'maxFiles', 'theme']);
204
+ const formatStr = (options.format || 'text').toLowerCase();
205
+ const isJson = formatStr === 'json';
206
+ const quietOutput = Boolean(options.quiet || isJson);
207
+ if (options.failOnValidationError && !options.validate) {
208
+ console.warn(chalk.yellow('⚠️ --fail-on-validation-error has no effect unless --validate is also provided.'));
209
+ }
210
+ const root = resolveRootPathOrExit(targetPath);
211
+ const requestedTheme = String(options.theme || 'default').toLowerCase();
212
+ const safeTheme = normalizeThemeOption(options.theme, 'default');
213
+ if (requestedTheme !== safeTheme) {
214
+ const suggestion = findClosestMatch(options.theme, ALLOWED_THEMES);
215
+ console.warn(chalk.yellow(`⚠️ Unknown theme "${options.theme}", using "${safeTheme}"`));
216
+ if (suggestion) console.warn(formatSuggestion(suggestion));
217
+ }
218
+ const outputExt = options.output ? path.extname(options.output).toLowerCase() : '';
219
+ const needsMermaidCli = Boolean(
220
+ options.validate || (options.output && outputExt !== '.md' && outputExt !== '.mmd')
221
+ );
222
+ const confidenceEnabled = Boolean(
223
+ options.confidenceReport || options.strictConfidence || options.capabilityCheckOnly
224
+ );
225
+
226
+ let capabilities = null;
227
+ if (confidenceEnabled) {
228
+ capabilities = probeCapabilities('generate', { requiresMermaidCli: needsMermaidCli });
229
+ }
230
+
231
+ if (options.capabilityCheckOnly) {
232
+ const quickReport = buildConfidenceReport({
233
+ command: 'generate',
234
+ rootPath: root,
235
+ capabilities,
236
+ validation: { enabled: false, valid: true, errors: [] },
237
+ fallback: { used: false, reasons: [] },
238
+ notes: ['capability_check_only'],
239
+ });
240
+ let confidencePath = null;
241
+ if (options.confidenceReport || options.strictConfidence) {
242
+ confidencePath = writeConfidenceReport(root, quickReport);
243
+ if (!quietOutput) {
244
+ console.error(chalk.gray('Confidence report:'), confidencePath);
245
+ }
246
+ }
247
+ const strictConfidenceFailed = options.strictConfidence && shouldFailStrictConfidence(quickReport);
248
+ if (isJson) {
249
+ const machinePayload = buildMachineEnvelope({
250
+ schemaVersion: '1.0',
251
+ command: 'generate',
252
+ rootPath: root,
253
+ deterministic: Boolean(options.deterministic),
254
+ status: strictConfidenceFailed ? 'failure' : 'success',
255
+ data: {
256
+ capabilityCheckOnly: true,
257
+ confidence: quickReport,
258
+ artifacts: {
259
+ confidenceReportPath: confidencePath,
260
+ },
261
+ },
262
+ errors: strictConfidenceFailed
263
+ ? [{ message: 'Strict confidence check failed' }]
264
+ : [],
265
+ agentSummary: {
266
+ changedComponents: 0,
267
+ riskReasons: quickReport?.fallback?.reasons || [],
268
+ suggestedReviewerChecks: [
269
+ 'Install missing capabilities listed in the confidence report before generation.',
270
+ ],
271
+ },
272
+ });
273
+ console.log(JSON.stringify(machinePayload, null, 2));
274
+ } else if (strictConfidenceFailed) {
275
+ if (!quietOutput) {
276
+ console.error(chalk.red('❌ Strict confidence check failed'));
277
+ }
278
+ } else if (!quietOutput) {
279
+ console.log(chalk.green('✅ Capability check complete'));
280
+ }
281
+ process.exit(strictConfidenceFailed ? 1 : 0);
282
+ }
283
+ if (!quietOutput) {
284
+ console.error(chalk.blue('Generating'), options.type, 'diagram for', root);
285
+ }
286
+
287
+ const pipeline = await runAnalysisPipeline(root, options, 'generate');
288
+ const data = pipeline.analysis;
289
+ const irPath = options.emitIr
290
+ ? maybeWriteArchitectureIR(root, data, pipeline.analyzer, true)
291
+ : null;
292
+ const diagramArtifact = generateDiagramArtifact(data, options.type, options.focus);
293
+ const mermaid = diagramArtifact.mermaid;
294
+ let validationResult = {
295
+ enabled: Boolean(options.validate),
296
+ valid: true,
297
+ errors: [],
298
+ meta: { fallbackUsed: false, fallbackReasons: [], mode: 'not_requested' },
299
+ };
300
+
301
+ let failOnValidationErrorTriggered = false;
302
+ if (options.validate) {
303
+ if (!quietOutput) console.error(chalk.blue('\n🔍 Validating Mermaid syntax...'));
304
+ validationResult = validateMermaidSyntax(mermaid, safeTheme, options.allowAutoInstall);
305
+ validationResult.enabled = true;
306
+
307
+ if (validationResult.valid) {
308
+ if (!quietOutput) console.error(chalk.green('✅ Mermaid syntax is valid'));
309
+ } else {
310
+ if (!quietOutput) {
311
+ console.error(chalk.yellow('⚠️ Mermaid syntax issues detected:'));
312
+ for (const error of validationResult.errors) {
313
+ console.error(chalk.yellow(` Line ${error.line || '?'}: ${error.message}`));
314
+ }
315
+ }
316
+ if (options.failOnValidationError) {
317
+ if (!isJson) {
318
+ console.error(chalk.red('❌ Validation failed (exit 1)'));
319
+ console.error(chalk.gray(`Fix: run \`${buildGenerateHint(options, targetPath, ['--validate'])}\` and address listed lines.`));
320
+ }
321
+ failOnValidationErrorTriggered = true;
322
+ }
323
+ }
324
+ }
325
+
326
+ const fallbackReasons = [];
327
+ if (validationResult?.meta?.fallbackUsed) {
328
+ fallbackReasons.push(...(validationResult.meta.fallbackReasons || []));
329
+ }
330
+ if (pipeline.incremental.requested && !pipeline.incremental.used) {
331
+ const incrementalReason = pipeline.incremental.reason || 'unknown';
332
+ const expectedIncrementalBypassReasons = new Set(['cache_miss', 'incremental_disabled_in_ci']);
333
+ if (!expectedIncrementalBypassReasons.has(incrementalReason)) {
334
+ fallbackReasons.push(`incremental_${incrementalReason}`);
335
+ }
336
+ }
337
+ const fallback = {
338
+ used: fallbackReasons.length > 0,
339
+ reasons: fallbackReasons,
340
+ };
341
+
342
+ let strictConfidenceFailed = false;
343
+ let confidencePath = null;
344
+ if (confidenceEnabled) {
345
+ const report = buildConfidenceReport({
346
+ command: 'generate',
347
+ rootPath: root,
348
+ capabilities,
349
+ validation: {
350
+ enabled: Boolean(validationResult.enabled),
351
+ valid: Boolean(validationResult.valid),
352
+ errors: validationResult.errors || [],
353
+ mode: validationResult?.meta?.mode || 'basic',
354
+ },
355
+ fallback,
356
+ notes: [
357
+ pipeline.incremental.requested
358
+ ? `incremental:${pipeline.incremental.used ? 'hit' : pipeline.incremental.reason}`
359
+ : 'incremental:not_requested',
360
+ ],
361
+ });
362
+
363
+ if (options.confidenceReport || options.strictConfidence) {
364
+ confidencePath = writeConfidenceReport(root, report);
365
+ if (!quietOutput) console.error(chalk.gray('Confidence report:'), confidencePath);
366
+ }
367
+
368
+ if (options.strictConfidence && shouldFailStrictConfidence(report)) {
369
+ strictConfidenceFailed = true;
370
+ if (!isJson && !quietOutput) {
371
+ console.error(chalk.red('❌ Strict confidence check failed'));
372
+ }
373
+ }
374
+ }
375
+
376
+ const { url, large } = createMermaidUrl(mermaid);
377
+ const envelopeErrors = [...(validationResult.errors || [])];
378
+ if (strictConfidenceFailed) {
379
+ envelopeErrors.push({ message: 'Strict confidence check failed' });
380
+ }
381
+ if (failOnValidationErrorTriggered && envelopeErrors.length === 0) {
382
+ envelopeErrors.push({ message: 'Validation failed with --fail-on-validation-error' });
383
+ }
384
+ const failed = failOnValidationErrorTriggered || strictConfidenceFailed || !validationResult.valid;
385
+ let resolvedOutputPath = null;
386
+ const machinePayload = buildMachineEnvelope({
387
+ schemaVersion: '1.0',
388
+ command: 'generate',
389
+ rootPath: root,
390
+ deterministic: Boolean(options.deterministic),
391
+ status: failed ? 'failure' : 'success',
392
+ data: {
393
+ diagramType: options.type,
394
+ mermaid,
395
+ diagramMetadata: diagramArtifact.metadata || null,
396
+ previewUrl: url,
397
+ previewTooLarge: large,
398
+ analyzer: pipeline.analyzer,
399
+ incremental: pipeline.incremental,
400
+ validation: {
401
+ enabled: Boolean(validationResult.enabled),
402
+ valid: Boolean(validationResult.valid),
403
+ mode: validationResult?.meta?.mode || 'basic',
404
+ errors: validationResult.errors || [],
405
+ },
406
+ artifacts: {
407
+ architectureIrPath: irPath,
408
+ confidenceReportPath: confidencePath,
409
+ outputPath: null,
410
+ },
411
+ },
412
+ errors: envelopeErrors,
413
+ agentSummary: {
414
+ changedComponents: data.components?.length || 0,
415
+ riskReasons: fallbackReasons,
416
+ suggestedReviewerChecks: [
417
+ 'Review large fan-out modules in dependency diagram.',
418
+ 'Confirm auth/security edges match expected trust boundaries.',
419
+ ],
420
+ },
421
+ });
422
+
423
+ if (failOnValidationErrorTriggered || strictConfidenceFailed) {
424
+ machinePayload.data.artifacts.outputPath = resolvedOutputPath;
425
+ if (isJson) {
426
+ console.log(JSON.stringify(machinePayload, null, 2));
427
+ }
428
+ process.exit(1);
429
+ }
430
+
431
+ if (!options.output) {
432
+ machinePayload.data.artifacts.outputPath = resolvedOutputPath;
433
+ if (isJson) {
434
+ console.log(JSON.stringify(machinePayload, null, 2));
435
+ } else {
436
+ console.log(chalk.green('\n📐 Mermaid Diagram:\n'));
437
+ console.log('```mermaid');
438
+ console.log(mermaid);
439
+ console.log('```\n');
440
+
441
+ if (large || !url) {
442
+ console.error(chalk.yellow('⚠️ Diagram is too large for preview URL.'));
443
+ console.error(chalk.cyan('💾 Save to file:'), buildSaveHint(options, targetPath));
444
+ } else {
445
+ console.error(chalk.cyan('🔗 Preview:'), url);
446
+ }
447
+ }
448
+ }
449
+
450
+ if (options.output) {
451
+ let safeOutput;
452
+ try {
453
+ safeOutput = validateOutputPath(options.output, root);
454
+ } catch (error) {
455
+ console.error(chalk.red('❌ Output path error:'), error.message);
456
+ process.exit(2);
457
+ }
458
+
459
+ const outputDir = path.dirname(safeOutput);
460
+ fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
461
+ const ext = outputExt;
462
+ if (ext === '.md' || ext === '.mmd') {
463
+ try {
464
+ fs.writeFileSync(safeOutput, mermaid, { flag: options.force ? 'w' : 'wx' });
465
+ resolvedOutputPath = safeOutput;
466
+ if (!quietOutput) console.error(chalk.green('✅ Saved to'), options.output);
467
+ } catch (error) {
468
+ if (error.code === 'EEXIST') {
469
+ console.error(chalk.red(`❌ Target file exists: ${safeOutput}`));
470
+ console.error(chalk.yellow('Use --force to overwrite.'));
471
+ process.exit(1);
472
+ }
473
+ throw error;
474
+ }
475
+ } else {
476
+ let tempDir = null;
477
+ try {
478
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diagram-'));
479
+ const tempFile = path.join(tempDir, 'diagram.mmd');
480
+ fs.writeFileSync(tempFile, `%%{init: {'theme': '${safeTheme}'}}%%\n${mermaid}`);
481
+ runMermaidCli(
482
+ ['@mermaid-js/mermaid-cli', 'mmdc', '-i', tempFile, '-o', safeOutput, '-b', 'transparent'],
483
+ { allowAutoInstall: options.allowAutoInstall }
484
+ );
485
+ resolvedOutputPath = safeOutput;
486
+ if (!quietOutput) console.error(chalk.green('✅ Rendered to'), options.output);
487
+ } catch (error) {
488
+ console.error(chalk.red('❌ Could not render output file.'));
489
+ console.error(chalk.gray('Fix: npm install -g @mermaid-js/mermaid-cli'));
490
+ if (process.env.DEBUG) console.error(chalk.gray(error.message));
491
+ process.exit(2);
492
+ } finally {
493
+ cleanupTempDirectory(tempDir);
494
+ }
495
+ }
496
+ }
497
+
498
+ machinePayload.data.artifacts.outputPath = resolvedOutputPath;
499
+ if (isJson && options.output) {
500
+ console.log(JSON.stringify(machinePayload, null, 2));
501
+ }
502
+
503
+ if (options.open && url) {
504
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
505
+ console.error(chalk.red('❌ Invalid URL protocol'));
506
+ } else {
507
+ openPreviewUrl(url);
508
+ }
509
+ }
510
+
511
+ if (!isJson && !options.quiet) {
512
+ console.log(chalk.cyan('\nNext steps:'));
513
+ console.log(' 1) Run `archscope validate .` to enforce architecture policy constraints.');
514
+ console.log(' 2) Run `archscope generate-all . --artifact-profile agent` for AI-friendly context pack artifacts.');
515
+ }
516
+ });
517
+ }
518
+
519
+ module.exports = {
520
+ registerGenerateCommand,
521
+ validateMermaidSyntax,
522
+ };
@@ -0,0 +1,123 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const YAML = require('yaml');
5
+ const { getDefaultConfig } = require('../schema/rules-schema');
6
+ const { resolveRootPathOrExit } = require('./shared');
7
+
8
+ const DEFAULT_DIAGRAMRC = Object.freeze({
9
+ patterns: '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs',
10
+ exclude: 'node_modules/**,.git/**,dist/**,build/**',
11
+ maxFiles: 500,
12
+ theme: 'default',
13
+ });
14
+
15
+ const DEFAULT_CI_STEP = `# Sample GitHub Actions steps for Archscope
16
+ - name: Install dependencies
17
+ run: npm ci
18
+
19
+ - name: Install Archscope CLI
20
+ run: npm install --no-save @brainwav/diagram
21
+
22
+ - name: Validate architecture rules
23
+ run: npx --no-install archscope validate .
24
+
25
+ - name: Generate compact architecture artifacts
26
+ run: npx --no-install archscope generate-all . --output-dir .diagram --artifact-profile agent
27
+
28
+ - name: Refresh AI context pack
29
+ run: npx --no-install archscope context .
30
+
31
+ - name: Upload diagram artifacts
32
+ uses: actions/upload-artifact@v4
33
+ with:
34
+ name: diagram-artifacts
35
+ path: .diagram
36
+ `;
37
+
38
+ /**
39
+ * Create parent directories and write content to a file, failing if the file exists unless forced.
40
+ *
41
+ * Ensures the file's parent directory exists, then writes `content` to `filePath`. When `force` is
42
+ * false the write will fail if the target file already exists; when `force` is true the file is
43
+ * overwritten. Filesystem errors are not caught and will propagate to the caller.
44
+ *
45
+ * @param {string} filePath - Path of the file to write.
46
+ * @param {string|Buffer} content - Data to write to the file.
47
+ * @param {boolean} force - If `true`, overwrite an existing file; if `false`, fail when the file exists.
48
+ */
49
+ function writeFileSafely(filePath, content, force) {
50
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
51
+ fs.writeFileSync(filePath, content, { flag: force ? 'w' : 'wx' });
52
+ }
53
+
54
+ /**
55
+ * Register the `init [path]` CLI command which bootstraps repository starter files for the diagram tooling.
56
+ *
57
+ * The command creates `.architecture.yml`, `.diagramrc` and `.diagram/ci/github-actions-step.yml` in the resolved project root,
58
+ * and accepts `--force` to overwrite existing generated files. If one or more target files already exist and `--force` is not used,
59
+ * the command prints an error and exits the process with code `2`.
60
+ *
61
+ * @param {import('commander').Command} program - The CLI program instance to attach the `init` command to.
62
+ */
63
+ function registerInitCommand(program) {
64
+ program
65
+ .command('init [path]')
66
+ .description('Bootstrap architecture policy + diagram defaults + CI sample step')
67
+ .option('--force', 'Overwrite existing generated files', false)
68
+ .action((targetPath, options) => {
69
+ const root = resolveRootPathOrExit(targetPath);
70
+ const architecturePath = path.join(root, '.architecture.yml');
71
+ const diagramRcPath = path.join(root, '.diagramrc');
72
+ const ciSamplePath = path.join(root, '.diagram', 'ci', 'github-actions-step.yml');
73
+
74
+ // Preflight: check all targets if force is not set
75
+ if (!options.force) {
76
+ const existingFiles = [];
77
+ if (fs.existsSync(architecturePath)) existingFiles.push(architecturePath);
78
+ if (fs.existsSync(diagramRcPath)) existingFiles.push(diagramRcPath);
79
+ if (fs.existsSync(ciSamplePath)) existingFiles.push(ciSamplePath);
80
+
81
+ if (existingFiles.length > 0) {
82
+ console.error(chalk.red('❌ Initialization blocked: one or more files already exist.'));
83
+ existingFiles.forEach(file => console.error(chalk.gray(` - ${file}`)));
84
+ console.error(chalk.gray('Fix: rerun with `archscope init . --force` to overwrite generated starter files.'));
85
+ process.exit(2);
86
+ }
87
+ }
88
+
89
+ try {
90
+ writeFileSafely(
91
+ architecturePath,
92
+ YAML.stringify(getDefaultConfig(), { indent: 2, lineWidth: 0 }),
93
+ options.force
94
+ );
95
+ writeFileSafely(
96
+ diagramRcPath,
97
+ `${JSON.stringify(DEFAULT_DIAGRAMRC, null, 2)}\n`,
98
+ options.force
99
+ );
100
+ writeFileSafely(ciSamplePath, DEFAULT_CI_STEP, options.force);
101
+ } catch (error) {
102
+ if (error.code === 'EEXIST') {
103
+ console.error(chalk.red('❌ Initialization blocked: one or more files already exist.'));
104
+ console.error(chalk.gray('Fix: rerun with `archscope init . --force` to overwrite generated starter files.'));
105
+ process.exit(2);
106
+ }
107
+ throw error;
108
+ }
109
+
110
+ console.log(chalk.green('✅ archscope init complete'));
111
+ console.log(chalk.gray(` Created: ${architecturePath}`));
112
+ console.log(chalk.gray(` Created: ${diagramRcPath}`));
113
+ console.log(chalk.gray(` Created: ${ciSamplePath}`));
114
+ console.log(chalk.cyan('\nNext steps:'));
115
+ console.log(' 1) Edit `.architecture.yml` to match your real layer boundaries.');
116
+ console.log(' 2) Run `archscope validate .` and commit passing rules.');
117
+ console.log(' 3) Copy `.diagram/ci/github-actions-step.yml` into your workflow YAML.');
118
+ });
119
+ }
120
+
121
+ module.exports = {
122
+ registerInitCommand,
123
+ };