@brainwav/diagram 1.0.8 → 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 +178 -1761
  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,76 @@
1
+ /**
2
+ * Recursively sort keys of any plain object found within a value.
3
+ *
4
+ * Processes arrays element-wise, transforms plain objects by returning a new object
5
+ * whose keys are in lexicographic order (with each value processed recursively),
6
+ * and leaves non-object, non-array values unchanged.
7
+ *
8
+ * @param {*} value - The input to transform: plain objects will have their keys sorted recursively; arrays will be processed element-wise; primitives and other non-object values are returned as-is.
9
+ * @returns {*} The transformed value with object keys sorted lexicographically.
10
+ */
11
+ function sortObjectDeep(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map(sortObjectDeep);
14
+ }
15
+ if (value && typeof value === 'object') {
16
+ const sorted = {};
17
+ for (const key of Object.keys(value).sort()) {
18
+ sorted[key] = sortObjectDeep(value[key]);
19
+ }
20
+ return sorted;
21
+ }
22
+ return value;
23
+ }
24
+
25
+ /**
26
+ * Construct a standardized machine-readable envelope containing schema, command, status, metadata, payload and errors.
27
+ *
28
+ * The envelope includes `schemaVersion`, `command`, `status`, a `meta` object with `rootPath` (and `generatedAt` unless `deterministic` is true), `data`, and `errors`. If `agentSummary` is provided it is added to the top-level envelope. When `deterministic` is true the envelope is returned with keys sorted and without `meta.generatedAt`.
29
+ *
30
+ * @param {string} schemaVersion - Version identifier for the envelope schema.
31
+ * @param {string} command - Name of the command or operation the envelope describes.
32
+ * @param {string} rootPath - Root filesystem path to include in the envelope metadata.
33
+ * @param {string} [status='success'] - Status label for the operation.
34
+ * @param {Object} [data={}] - Payload data to include under `data`.
35
+ * @param {Array} [errors=[]] - Array of error descriptors to include under `errors`.
36
+ * @param {Object|null} [agentSummary=null] - Optional summary object about the agent to include when present.
37
+ * @param {boolean} [deterministic=false] - If true, omit `meta.generatedAt` and return a key-sorted representation of the envelope.
38
+ * @returns {Object} The constructed envelope object.
39
+ */
40
+ function buildMachineEnvelope({
41
+ schemaVersion,
42
+ command,
43
+ rootPath,
44
+ status = 'success',
45
+ data = {},
46
+ errors = [],
47
+ agentSummary = null,
48
+ deterministic = false,
49
+ }) {
50
+ const envelope = {
51
+ schemaVersion,
52
+ command,
53
+ status,
54
+ meta: {
55
+ rootPath,
56
+ generatedAt: deterministic ? undefined : new Date().toISOString(),
57
+ },
58
+ data,
59
+ errors,
60
+ };
61
+
62
+ if (agentSummary) {
63
+ envelope.agentSummary = agentSummary;
64
+ }
65
+
66
+ if (deterministic) {
67
+ delete envelope.meta.generatedAt;
68
+ return module.exports.sortObjectDeep(envelope);
69
+ }
70
+ return envelope;
71
+ }
72
+
73
+ module.exports = {
74
+ buildMachineEnvelope,
75
+ sortObjectDeep,
76
+ };
@@ -0,0 +1,624 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+ const chalk = require('chalk');
5
+ const { writeAgentContext } = require('../artifacts/agent-context');
6
+ const { writeArchitectureBrief } = require('../artifacts/brief');
7
+ const {
8
+ createScanEvidenceManifest,
9
+ writeJsonFile,
10
+ } = require('../artifacts/evidence-manifest');
11
+ const { generateDiagramArtifact } = require('../core/analysis-generation');
12
+ const { writeArchitectureReport } = require('../renderers/report-html');
13
+ const { buildMachineEnvelope } = require('./output');
14
+ const {
15
+ applyDiagramRcDefaults,
16
+ getDiagramRcFromProgram,
17
+ runAnalysisPipeline,
18
+ resolveRootPathOrExit,
19
+ validateOutputPath,
20
+ } = require('./shared');
21
+
22
+ function outcomeForManifest(manifest) {
23
+ const state = manifest.artifacts.reduce((current, entry) => ({
24
+ failed: current.failed || entry.status === 'failed',
25
+ partial: current.partial || entry.status === 'partial',
26
+ written: current.written || entry.status === 'written',
27
+ }), { failed: false, partial: false, written: false });
28
+ if (state.failed) {
29
+ if (state.written) return 'partial';
30
+ return 'failed';
31
+ }
32
+ if (state.partial) return 'partial';
33
+ return 'success';
34
+ }
35
+
36
+ function markArtifactFailure({
37
+ artifactStatuses,
38
+ artifactReasons,
39
+ artifactErrorCategories,
40
+ errors,
41
+ artifact,
42
+ reason = 'writer_failed',
43
+ category = 'artifact_write_failed',
44
+ error,
45
+ }) {
46
+ artifactStatuses[artifact] = 'failed';
47
+ artifactReasons[artifact] = reason;
48
+ artifactErrorCategories[artifact] = category;
49
+ errors.push(errorForArtifact(artifact, category, error));
50
+ }
51
+
52
+ function writeArtifact({
53
+ artifact,
54
+ write,
55
+ failureState,
56
+ reason = 'writer_failed',
57
+ category = 'artifact_write_failed',
58
+ }) {
59
+ try {
60
+ return write();
61
+ } catch (error) {
62
+ markArtifactFailure({
63
+ ...failureState,
64
+ artifact,
65
+ reason,
66
+ category,
67
+ error,
68
+ });
69
+ return null;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Mark given artifacts as failed because analysis failed and record their failure details.
75
+ *
76
+ * For each artifact id in `artifacts` this records status `'failed'`, reason `'analysis_failed'`,
77
+ * category `'analysis_partial'` and appends the provided error into the shared failure state.
78
+ *
79
+ * @param {string[]} artifacts - Artifact identifiers to mark as failed.
80
+ * @param {Object} failureState - Shared failure state containing `artifactStatuses`, `artifactReasons`, `artifactErrorCategories` and `errors`.
81
+ * @param {Error|any} error - The error that caused the analysis failure; its message is attached to each artifact's error entry.
82
+ */
83
+ function failArtifactsForAnalysis(artifacts, failureState, error) {
84
+ for (const artifact of artifacts) {
85
+ markArtifactFailure({
86
+ ...failureState,
87
+ artifact,
88
+ reason: 'analysis_failed',
89
+ category: 'analysis_partial',
90
+ error,
91
+ });
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Produce a semicolon-separated summary of warning messages or `'none'` when there are none.
97
+ * @param {Array<string>} warnings - Array of warning messages; any non-array or empty array is treated as no warnings.
98
+ * @return {string} `'none'` if `warnings` is not a non-empty array, otherwise the warnings joined with `'; '`.
99
+ */
100
+ function summarizeWarnings(warnings) {
101
+ if (!Array.isArray(warnings) || warnings.length === 0) return 'none';
102
+ return warnings.join('; ');
103
+ }
104
+
105
+ /**
106
+ * Convert an array of items into a single semicolon-separated string or return 'none'.
107
+ *
108
+ * @param {Array} items - The items to summarise; may be non-array or empty.
109
+ * @returns {string} `'none'` if `items` is not a non-empty array, otherwise the items joined with `'; '`.
110
+ */
111
+ function summarizeList(items) {
112
+ if (!Array.isArray(items) || items.length === 0) return 'none';
113
+ return items.join('; ');
114
+ }
115
+
116
+ /**
117
+ * Build a concise summary of an evidence pack suitable for CLI output and machine envelopes.
118
+ *
119
+ * @param {Object} manifest - Evidence pack manifest; must include `artifactReadOrder` and may include `primaryHumanArtifact`, `primaryAgentArtifact` and `warnings`.
120
+ * @param {Object} [options] - Optional inputs to augment the summary.
121
+ * @param {Object|null} [options.analysis=null] - Analysis result object; `analysis.components` is used to compute `componentCount`.
122
+ * @param {string} [options.outcome] - Overall pack outcome; defaults to a value derived from `manifest`.
123
+ * @param {string|null} [options.manifestPath] - Filesystem path to the written manifest artifact, if any.
124
+ * @param {Object|null} [options.prImpact=null] - PR impact data produced by workflow analysis; used to populate `pr` fields when present.
125
+ * @param {string|null} [options.prImpactPath=null] - Filesystem path to the written PR impact artifact, if any.
126
+ * @param {Object|null} [options.prSummary=null] - Machine PR summary used to keep failed PR scans visible in text summaries.
127
+ * @returns {Object} An object containing:
128
+ * - `manifestPath`: path to the primary manifest entry,
129
+ * - `primaryHumanArtifact` and `primaryAgentArtifact`: ids from the manifest,
130
+ * - `packStatus`: overall outcome,
131
+ * - `componentCount`: number of analysed components (0 if unavailable),
132
+ * - `warningSummary`: human-readable summary of manifest warnings,
133
+ * - `pr`: `null` or an object with `riskLevel`, `changedComponents`, `riskReasons`, `reviewerChecks`, and `prImpactPath`,
134
+ * - `nextAction`: a short instruction pointing to the manifest for artefact status.
135
+ */
136
+ function createScanSummary(manifest, {
137
+ analysis = null,
138
+ outcome = outcomeForManifest(manifest),
139
+ manifestPath = manifestArtifactPath(manifest, 'manifest', { requireWritten: true }),
140
+ prImpact = null,
141
+ prImpactPath = null,
142
+ prSummary = null,
143
+ } = {}) {
144
+ const componentCount = Array.isArray(analysis?.components) ? analysis.components.length : 0;
145
+ const warnings = Array.isArray(manifest.warnings) ? manifest.warnings : [];
146
+ const prAgentSummary = prImpact?.agentSummary || {};
147
+ return {
148
+ manifestPath,
149
+ primaryHumanArtifact: manifest.primaryHumanArtifact,
150
+ primaryAgentArtifact: manifest.primaryAgentArtifact,
151
+ packStatus: outcome,
152
+ componentCount,
153
+ warningSummary: summarizeWarnings(warnings),
154
+ pr: prImpact ? {
155
+ riskLevel: prImpact.risk?.level || 'unknown',
156
+ changedComponents: prAgentSummary.changedComponents ?? prImpact.changedComponents?.length ?? 0,
157
+ riskReasons: prAgentSummary.riskReasons || [],
158
+ reviewerChecks: prAgentSummary.suggestedReviewerChecks || [],
159
+ prImpactPath,
160
+ } : prSummary ? {
161
+ riskLevel: prSummary.risk?.level || 'unknown',
162
+ changedComponents: 0,
163
+ riskReasons: [prSummary.errorCategory || 'pr_evidence_unavailable'],
164
+ reviewerChecks: [`Resolve PR evidence failure before approving architecture-sensitive changes.`],
165
+ prImpactPath: prSummary.prImpactPath || null,
166
+ } : null,
167
+ nextAction: manifestPath
168
+ ? `Read ${manifestPath} for artifact status before opening optional files.`
169
+ : 'Manifest was not written; inspect the reported errors before consuming evidence artifacts.',
170
+ };
171
+ }
172
+
173
+ function manifestArtifactPath(manifest, id, { requireWritten = false } = {}) {
174
+ const artifact = manifest.artifacts.find((entry) => entry.id === id);
175
+ if (!artifact) return null;
176
+ if (requireWritten && artifact.status !== 'written') return null;
177
+ return artifact.path;
178
+ }
179
+
180
+ function errorForArtifact(artifact, category, error) {
181
+ return {
182
+ artifact,
183
+ category,
184
+ message: error?.message || String(error),
185
+ };
186
+ }
187
+
188
+ function writeArchitectureArtifact({ outDir, analysis }) {
189
+ const artifact = generateDiagramArtifact(analysis, 'architecture');
190
+ const architecturePath = path.join(outDir, 'architecture.mmd');
191
+ fs.writeFileSync(architecturePath, artifact.mermaid);
192
+ return architecturePath;
193
+ }
194
+
195
+ function optionArg(args, flag, value) {
196
+ if (value !== undefined && value !== null && String(value).trim() !== '') {
197
+ args.push(flag, String(value));
198
+ }
199
+ }
200
+
201
+ function parseWorkflowPrPayload(stdout) {
202
+ const trimmed = String(stdout || '').trim();
203
+ if (!trimmed) return null;
204
+ try {
205
+ return JSON.parse(trimmed);
206
+ } catch (_error) {
207
+ return null;
208
+ }
209
+ }
210
+
211
+ function inferWorkflowPrErrorCategory({ payload, result }) {
212
+ const payloadCategory = payload?.errors?.[0]?.category
213
+ || payload?.errors?.[0]?.extensions?.code
214
+ || payload?.data?.prImpact?.errorCategory;
215
+ const output = `${result?.stderr || ''}\n${result?.stdout || ''}`;
216
+ if (/bad revision|unknown revision|ambiguous argument|not a valid object name|invalid ref|unknown ref|git ref not found|needed a single revision|revision or path not in the working tree/i.test(output)) {
217
+ return 'git_refs_missing';
218
+ }
219
+ if (payloadCategory) return payloadCategory;
220
+ if (/permission denied|eacces|enoent|no such file|timed out|timeout|unexpected token|invalid json/i.test(output)) {
221
+ return 'internal_error';
222
+ }
223
+ return 'internal_error';
224
+ }
225
+
226
+ /**
227
+ * Invoke the local diagram workflow to generate PR-impact evidence for a repository and return the parsed PR impact data.
228
+ *
229
+ * @param {Object} params
230
+ * @param {string} params.root - Filesystem path to the repository root to analyse.
231
+ * @param {string} params.outDir - Directory where PR-impact output will be written (`<outDir>/pr-impact`).
232
+ * @param {Object} params.options - Options forwarded to the workflow; recognised properties:
233
+ * - {string} [head] - HEAD ref (defaults to `HEAD`).
234
+ * - {string} [base] - Base ref for comparison.
235
+ * - {string} [patterns] - File match patterns to include.
236
+ * - {string} [exclude] - File match patterns to exclude.
237
+ * - {number|string} [maxFiles] - Maximum files to consider.
238
+ * - {boolean} [deterministic] - Whether to run the workflow in deterministic mode.
239
+ * @returns {Object} The PR impact data object parsed from the workflow's JSON output.
240
+ * @throws {Error} If the workflow process fails or does not produce a `prImpact` payload; the thrown error carries the workflow category when available.
241
+ */
242
+ function runWorkflowPrEvidence({ root, outDir, options }) {
243
+ const prImpactDir = path.join(outDir, 'pr-impact');
244
+ const args = [
245
+ path.join(__dirname, '..', 'diagram.js'),
246
+ 'workflow',
247
+ 'pr',
248
+ root,
249
+ '--head',
250
+ options.head || 'HEAD',
251
+ '--output-dir',
252
+ prImpactDir,
253
+ '--format',
254
+ 'json',
255
+ ];
256
+ optionArg(args, '--base', options.base);
257
+ optionArg(args, '--patterns', options.patterns);
258
+ optionArg(args, '--exclude', options.exclude);
259
+ optionArg(args, '--max-files', options.maxFiles);
260
+ if (options.deterministic) {
261
+ args.push('--deterministic');
262
+ }
263
+
264
+ const result = spawnSync(process.execPath, args, {
265
+ cwd: root,
266
+ encoding: 'utf8',
267
+ timeout: 120_000, // 2 minutes
268
+ });
269
+ const payload = parseWorkflowPrPayload(result.stdout);
270
+ if (result.status !== 0 || !payload?.data?.prImpact) {
271
+ const error = new Error(
272
+ payload?.errors?.[0]?.message
273
+ || result.stderr.trim()
274
+ || result.stdout.trim()
275
+ || 'workflow pr evidence failed'
276
+ );
277
+ error.category = inferWorkflowPrErrorCategory({ payload, result });
278
+ throw error;
279
+ }
280
+
281
+ return payload.data.prImpact;
282
+ }
283
+
284
+ function printScanTextSummary(summary) {
285
+ console.log(` Pack status: ${summary.packStatus}`);
286
+ console.log(` Components detected: ${summary.componentCount}`);
287
+ console.log(` Manifest: ${summary.manifestPath || 'not written'}`);
288
+ console.log(` Human artifact: ${summary.primaryHumanArtifact}`);
289
+ console.log(` Agent artifact: ${summary.primaryAgentArtifact}`);
290
+ console.log(` Warnings: ${summary.warningSummary}`);
291
+ if (summary.pr) {
292
+ console.log(chalk.cyan('\nPR review focus:'));
293
+ console.log(` Risk: ${summary.pr.riskLevel}`);
294
+ console.log(` Changed components: ${summary.pr.changedComponents}`);
295
+ console.log(` Risk reasons: ${summarizeList(summary.pr.riskReasons)}`);
296
+ console.log(` Reviewer checks: ${summarizeList(summary.pr.reviewerChecks)}`);
297
+ console.log(` PR impact artifact: ${summary.pr.prImpactPath || 'not written'}`);
298
+ }
299
+ console.log(chalk.cyan('\nNext action:'));
300
+ console.log(` ${summary.nextAction}`);
301
+ }
302
+
303
+ /**
304
+ * Build a machine-oriented summary of pull-request impact analysis for inclusion in the scan envelope.
305
+ *
306
+ * When `prImpact` is provided, returns an object describing the PR analysis status, refs, risk metrics and suggested reviewer checks.
307
+ * When `prImpact` is absent but `options.base` or `options.head` are supplied, returns a failed summary containing the provided refs and an `errorCategory`.
308
+ * Returns `null` if no PR refs were supplied and no `prImpact` is available.
309
+ *
310
+ * @param {object|null} prImpact - The parsed PR impact payload produced by the workflow PR evidence run, or `null`.
311
+ * @param {string|null} prImpactPath - Filesystem path to the PR impact artifact, or `null`.
312
+ * @param {object} options - CLI options; expected to contain `base` and/or `head` when refs were supplied.
313
+ * @param {Array<object>} errors - Collected error objects produced while generating artifacts; used to derive an error category when `prImpact` is missing.
314
+ * @returns {object|null} When available, returns a summary object:
315
+ * - If `prImpact` present: `{ status, base, head, prImpactPath, risk, blastRadius, reviewerChecks }`.
316
+ * - If `prImpact` missing but refs provided: `{ status: 'failed', base, head, errorCategory }`.
317
+ * - Otherwise `null`.
318
+ */
319
+ function buildPrMachineSummary({
320
+ prImpact,
321
+ prImpactPath,
322
+ options,
323
+ errors,
324
+ }) {
325
+ if (prImpact) {
326
+ return {
327
+ status: prImpact._meta?.status || 'complete',
328
+ base: prImpact.base,
329
+ head: prImpact.head,
330
+ prImpactPath,
331
+ risk: prImpact.risk || null,
332
+ blastRadius: prImpact.blastRadius || null,
333
+ reviewerChecks: prImpact.agentSummary?.suggestedReviewerChecks || [],
334
+ };
335
+ }
336
+ if (options.base || options.head) {
337
+ const prError = errors.find((error) => error.artifact === 'pr-impact');
338
+ return {
339
+ status: 'failed',
340
+ base: options.base || null,
341
+ head: options.head || 'HEAD',
342
+ errorCategory: prError?.category || 'git_refs_missing',
343
+ };
344
+ }
345
+ return null;
346
+ }
347
+
348
+ /**
349
+ * Register the `scan [path]` CLI command.
350
+ *
351
+ * The scan implementation coordinates the non-visual evidence pack and writes
352
+ * manifest.json last so consumers can trust artifact-level statuses.
353
+ *
354
+ * @param {import('commander').Command} program - Commander program instance.
355
+ */
356
+ function registerScanCommand(program) {
357
+ program
358
+ .command('scan [path]')
359
+ .description('Generate architecture evidence pack')
360
+ .option('-O, --output-dir <dir>', 'Output directory', '.diagram')
361
+ .option('-p, --patterns <list>', 'File patterns')
362
+ .option('-e, --exclude <list>', 'Exclude patterns')
363
+ .option('-m, --max-files <n>', 'Max files to analyze')
364
+ .option('--analyzer <name>', 'Analyzer plugin to use', 'default')
365
+ .option('--base <ref>', 'Base git ref for PR evidence')
366
+ .option('--head <ref>', 'Head git ref for PR evidence')
367
+ .option('-f, --format <type>', 'Output format (text, json)', 'text')
368
+ .option('--deterministic', 'Use deterministic machine output', false)
369
+ .option('-q, --quiet', 'Suppress non-essential logging', false)
370
+ .action(async (targetPath, rawOptions) => {
371
+ const options = applyDiagramRcDefaults(
372
+ rawOptions,
373
+ getDiagramRcFromProgram(program),
374
+ ['patterns', 'exclude', 'maxFiles']
375
+ );
376
+ const root = resolveRootPathOrExit(targetPath);
377
+ const formatStr = String(options.format || 'text').toLowerCase().trim();
378
+ if (!['text', 'json'].includes(formatStr)) {
379
+ console.error(chalk.red('❌ Invalid format:'), options.format);
380
+ console.error(chalk.gray('Fix: use --format text or --format json.'));
381
+ process.exit(2);
382
+ }
383
+
384
+ let outDir;
385
+ try {
386
+ outDir = validateOutputPath(options.outputDir, root);
387
+ } catch (error) {
388
+ console.error(chalk.red('❌ Configuration error:'), error.message);
389
+ process.exit(2);
390
+ }
391
+ fs.mkdirSync(outDir, { recursive: true });
392
+
393
+ const warnings = [];
394
+ const artifactStatuses = {
395
+ manifest: 'written',
396
+ brief: 'written',
397
+ 'agent-context': 'written',
398
+ architecture: 'written',
399
+ report: 'written',
400
+ 'pr-impact': 'deferred',
401
+ };
402
+ const artifactReasons = {};
403
+ const artifactErrorCategories = {};
404
+ const errors = [];
405
+ const failureState = {
406
+ artifactStatuses,
407
+ artifactReasons,
408
+ artifactErrorCategories,
409
+ errors,
410
+ };
411
+ let analysis = null;
412
+ let prImpact = null;
413
+ const buildManifest = () => createScanEvidenceManifest({
414
+ root,
415
+ outDir,
416
+ deterministic: Boolean(options.deterministic),
417
+ warnings,
418
+ artifactStatuses,
419
+ artifactReasons,
420
+ artifactErrorCategories,
421
+ });
422
+
423
+ try {
424
+ const pipeline = await runAnalysisPipeline(root, options, 'scan');
425
+ analysis = pipeline.analysis;
426
+ } catch (error) {
427
+ failArtifactsForAnalysis(['brief', 'agent-context', 'architecture', 'report'], failureState, error);
428
+ }
429
+
430
+ if (analysis) {
431
+ writeArtifact({
432
+ artifact: 'architecture',
433
+ write: () => writeArchitectureArtifact({ outDir, analysis }),
434
+ failureState,
435
+ });
436
+ }
437
+
438
+ if (options.base || options.head) {
439
+ try {
440
+ const prEvidence = runWorkflowPrEvidence({ root, outDir, options });
441
+ prImpact = prEvidence;
442
+ if (prEvidence?._meta?.status === 'complete') {
443
+ artifactStatuses['pr-impact'] = 'written';
444
+ } else {
445
+ artifactStatuses['pr-impact'] = 'deferred';
446
+ artifactReasons['pr-impact'] = prEvidence?._meta?.status || 'no_pr_impact_artifact';
447
+ }
448
+ } catch (error) {
449
+ markArtifactFailure({
450
+ ...failureState,
451
+ artifact: 'pr-impact',
452
+ reason: 'pr_refs_unavailable',
453
+ category: error.category || 'git_refs_missing',
454
+ error,
455
+ });
456
+ }
457
+ }
458
+
459
+ let manifest = buildManifest();
460
+
461
+ if (analysis) {
462
+ const briefPath = path.join(outDir, 'brief.md');
463
+ writeArtifact({
464
+ artifact: 'brief',
465
+ write: () => writeArchitectureBrief(briefPath, {
466
+ manifest,
467
+ analysis,
468
+ prImpact,
469
+ warnings,
470
+ errors,
471
+ }),
472
+ failureState,
473
+ });
474
+
475
+ manifest = buildManifest();
476
+
477
+ const agentContextPath = path.join(outDir, 'agent-context.json');
478
+ writeArtifact({
479
+ artifact: 'agent-context',
480
+ write: () => writeAgentContext(agentContextPath, {
481
+ manifest,
482
+ analysis,
483
+ prImpact,
484
+ warnings,
485
+ errors,
486
+ }),
487
+ failureState,
488
+ });
489
+
490
+ manifest = buildManifest();
491
+
492
+ const reportPath = path.join(outDir, 'report.html');
493
+ writeArtifact({
494
+ artifact: 'report',
495
+ write: () => writeArchitectureReport(reportPath, {
496
+ manifest,
497
+ analysis,
498
+ prImpact,
499
+ warnings,
500
+ errors,
501
+ }),
502
+ failureState,
503
+ reason: 'write_failure',
504
+ category: 'artifact_write_failed',
505
+ });
506
+ }
507
+
508
+ manifest = buildManifest();
509
+ if (
510
+ analysis
511
+ && artifactStatuses.report === 'failed'
512
+ && artifactStatuses['agent-context'] === 'written'
513
+ ) {
514
+ const agentContextPath = path.join(outDir, 'agent-context.json');
515
+ writeArtifact({
516
+ artifact: 'agent-context',
517
+ write: () => writeAgentContext(agentContextPath, {
518
+ manifest,
519
+ analysis,
520
+ prImpact,
521
+ warnings,
522
+ errors,
523
+ }),
524
+ failureState,
525
+ });
526
+ manifest = buildManifest();
527
+ }
528
+ const manifestPath = path.join(outDir, 'manifest.json');
529
+ try {
530
+ writeJsonFile(manifestPath, manifest);
531
+ } catch (error) {
532
+ markArtifactFailure({
533
+ ...failureState,
534
+ artifact: 'manifest',
535
+ reason: 'write_failure',
536
+ category: 'artifact_write_failed',
537
+ error,
538
+ });
539
+ manifest = buildManifest();
540
+ }
541
+
542
+ const outcome = outcomeForManifest(manifest);
543
+ const writtenManifestPath = manifestArtifactPath(manifest, 'manifest', { requireWritten: true });
544
+ const prImpactPath = prImpact
545
+ ? manifestArtifactPath(manifest, 'pr-impact', { requireWritten: true })
546
+ : null;
547
+ const prSummary = buildPrMachineSummary({
548
+ prImpact,
549
+ prImpactPath,
550
+ options,
551
+ errors,
552
+ });
553
+ const summary = createScanSummary(manifest, {
554
+ analysis,
555
+ outcome,
556
+ manifestPath: writtenManifestPath,
557
+ prImpact,
558
+ prImpactPath,
559
+ prSummary,
560
+ });
561
+
562
+ if (formatStr === 'json') {
563
+ const payload = buildMachineEnvelope({
564
+ schemaVersion: '1.0',
565
+ command: 'scan',
566
+ rootPath: root,
567
+ deterministic: Boolean(options.deterministic),
568
+ status: outcome,
569
+ data: {
570
+ outcome,
571
+ partial: outcome === 'partial',
572
+ evidencePack: manifest,
573
+ artifacts: manifest.artifacts,
574
+ manifestPath: summary.manifestPath,
575
+ briefPath: manifestArtifactPath(manifest, 'brief', { requireWritten: true }),
576
+ agentContextPath: manifestArtifactPath(manifest, 'agent-context', { requireWritten: true }),
577
+ diagramPath: manifestArtifactPath(manifest, 'architecture', { requireWritten: true }),
578
+ reportPath: manifestArtifactPath(manifest, 'report', { requireWritten: true }),
579
+ prImpactPath,
580
+ ...(prSummary ? { pr: prSummary } : {}),
581
+ warnings: manifest.warnings,
582
+ },
583
+ errors,
584
+ agentSummary: {
585
+ changedComponents: prImpact?.agentSummary?.changedComponents ?? analysis?.components?.length ?? 0,
586
+ riskReasons: errors.length > 0
587
+ ? errors.map((entry) => entry.category)
588
+ : (prImpact?.agentSummary?.riskReasons || manifest.warnings),
589
+ suggestedReviewerChecks: [
590
+ ...(prImpact?.agentSummary?.suggestedReviewerChecks || []),
591
+ summary.manifestPath
592
+ ? `Read \`${summary.manifestPath}\` before consuming evidence artifacts.`
593
+ : 'Inspect scan errors before consuming evidence artifacts.',
594
+ ],
595
+ },
596
+ });
597
+ console.log(JSON.stringify(payload, null, 2));
598
+ process.exit(outcome === 'success' ? 0 : 1);
599
+ }
600
+
601
+ if (!options.quiet) {
602
+ console.error(chalk.blue('Scanning'), root);
603
+ console.error(chalk.green('✅ manifest'), '→', manifestPath);
604
+ }
605
+ if (outcome !== 'success') {
606
+ console.error(chalk.yellow('\nArchitecture evidence pack incomplete'));
607
+ console.error(` Outcome: ${outcome}`);
608
+ if (errors.length > 0) {
609
+ console.error(` Error: ${errors[0].category}: ${errors[0].message}`);
610
+ }
611
+ console.log(chalk.cyan('\nArchitecture evidence pack summary'));
612
+ printScanTextSummary(summary);
613
+ process.exit(1);
614
+ }
615
+ console.log(chalk.green('\nArchitecture evidence pack initialized'));
616
+ printScanTextSummary(summary);
617
+ });
618
+ }
619
+
620
+ module.exports = {
621
+ createScanSummary,
622
+ outcomeForManifest,
623
+ registerScanCommand,
624
+ };