@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.
- package/.diagram/contracts/machine-command-coverage.json +73 -0
- package/.diagram/migration/finalization-policy.json +20 -0
- package/LICENSE +202 -21
- package/README.md +132 -339
- package/package.json +46 -13
- package/scripts/refresh-diagram-context.sh +274 -182
- package/src/analyzers/default-analyzer.js +11 -0
- package/src/analyzers/index.js +34 -0
- package/src/artifacts/agent-context.js +105 -0
- package/src/artifacts/artifact-budget.js +224 -0
- package/src/artifacts/brief.js +153 -0
- package/src/artifacts/evidence-manifest.js +206 -0
- package/src/artifacts/evidence-summary.js +29 -0
- package/src/commands/analyze.js +125 -0
- package/src/commands/changed.js +185 -0
- package/src/commands/context.js +110 -0
- package/src/commands/diff.js +142 -0
- package/src/commands/doctor.js +335 -0
- package/src/commands/explain.js +273 -0
- package/src/commands/generate-all.js +170 -0
- package/src/commands/generate-animated.js +50 -0
- package/src/commands/generate-video.js +65 -0
- package/src/commands/generate.js +522 -0
- package/src/commands/init.js +123 -0
- package/src/commands/output.js +76 -0
- package/src/commands/scan.js +624 -0
- package/src/commands/shared.js +396 -0
- package/src/commands/validate.js +328 -0
- package/src/commands/video-shared.js +105 -0
- package/src/commands/workflow-pr.js +26 -0
- package/src/confidence/pipeline.js +186 -0
- package/src/config/diagramrc.js +79 -0
- package/src/context/build-context-pack.js +291 -0
- package/src/context/normalize-diagram-manifest.js +282 -0
- package/src/core/analysis-generation-analyze-components.js +102 -0
- package/src/core/analysis-generation-analyze-dependencies.js +33 -0
- package/src/core/analysis-generation-analyze-files.js +48 -0
- package/src/core/analysis-generation-analyze-options.js +73 -0
- package/src/core/analysis-generation-analyze.js +63 -0
- package/src/core/analysis-generation-constants.js +53 -0
- package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
- package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
- package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
- package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
- package/src/core/analysis-generation-diagrams-core.js +12 -0
- package/src/core/analysis-generation-diagrams-empty.js +68 -0
- package/src/core/analysis-generation-diagrams-erd.js +59 -0
- package/src/core/analysis-generation-diagrams-limit.js +27 -0
- package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
- package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
- package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
- package/src/core/analysis-generation-diagrams-role-data.js +182 -0
- package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
- package/src/core/analysis-generation-diagrams-role-security.js +129 -0
- package/src/core/analysis-generation-diagrams-role.js +25 -0
- package/src/core/analysis-generation-diagrams.js +182 -0
- package/src/core/analysis-generation-role-tags-constants.js +55 -0
- package/src/core/analysis-generation-role-tags-imports.js +32 -0
- package/src/core/analysis-generation-role-tags-infer.js +49 -0
- package/src/core/analysis-generation-role-tags-match.js +19 -0
- package/src/core/analysis-generation-role-tags.js +7 -0
- package/src/core/analysis-generation-utils-core.js +308 -0
- package/src/core/analysis-generation-utils-graph.js +321 -0
- package/src/core/analysis-generation-utils-resolution.js +76 -0
- package/src/core/analysis-generation-utils.js +9 -0
- package/src/core/analysis-generation.js +44 -0
- package/src/diagram.js +180 -1760
- package/src/formatters/console.js +198 -0
- package/src/formatters/index.js +41 -0
- package/src/formatters/json.js +113 -0
- package/src/formatters/junit.js +123 -0
- package/src/graph.js +159 -0
- package/src/incremental/cache.js +210 -0
- package/src/ir/architecture-ir.js +48 -0
- package/src/migration/evidence.js +262 -0
- package/src/migration/finalization-policy.js +35 -0
- package/src/renderers/report-html.js +265 -0
- package/src/rules/factory.js +108 -0
- package/src/rules/types/base.js +54 -0
- package/src/rules/types/import-rule.js +286 -0
- package/src/rules.js +380 -0
- package/src/schema/erd-confidence.js +56 -0
- package/src/schema/erd-extractor.js +504 -0
- package/src/schema/erd-model.js +176 -0
- package/src/schema/rules-schema.js +170 -0
- package/src/utils/suggestions.js +67 -0
- package/src/video.js +4 -5
- package/src/workflow/git-helpers.js +576 -0
- package/src/workflow/pr-command.js +694 -0
- package/src/workflow/pr-impact.js +848 -0
- package/src/workflow/sort-utils.js +16 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const zlib = require('zlib');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { spawn, execFileSync } = require('child_process');
|
|
6
|
+
const { getOpenCommand, getNpxCommandCandidates } = require('../utils/commands');
|
|
7
|
+
const { runAnalyzer } = require('../analyzers');
|
|
8
|
+
const {
|
|
9
|
+
buildCacheKey,
|
|
10
|
+
readCachedAnalysis,
|
|
11
|
+
writeCachedAnalysis,
|
|
12
|
+
} = require('../incremental/cache');
|
|
13
|
+
const { toArchitectureIR, writeArchitectureIR } = require('../ir/architecture-ir');
|
|
14
|
+
const { findClosestMatch, formatSuggestion } = require('../utils/suggestions');
|
|
15
|
+
|
|
16
|
+
const ALLOWED_THEMES = ['default', 'dark', 'forest', 'neutral', 'light'];
|
|
17
|
+
|
|
18
|
+
const DEFAULTS = Object.freeze({
|
|
19
|
+
patterns: '**/*.ts,**/*.tsx,**/*.js,**/*.jsx,**/*.py,**/*.go,**/*.rs',
|
|
20
|
+
exclude: 'node_modules/**,.git/**,dist/**',
|
|
21
|
+
maxFiles: '100',
|
|
22
|
+
theme: 'default',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert a possibly comma-separated value into an array of trimmed, non-empty strings.
|
|
27
|
+
*
|
|
28
|
+
* If `list` is falsy the function returns an empty array. Non-string inputs are coerced to a string
|
|
29
|
+
* before splitting on commas.
|
|
30
|
+
*
|
|
31
|
+
* @param {string|Array} list - A comma-separated string (or other value coerced to string). May be falsy.
|
|
32
|
+
* @returns {string[]} An array of trimmed, non-empty strings derived from `list`.
|
|
33
|
+
*/
|
|
34
|
+
function splitList(list) {
|
|
35
|
+
if (!list) return [];
|
|
36
|
+
return String(list)
|
|
37
|
+
.split(',')
|
|
38
|
+
.map((item) => item.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Determine whether a CLI option value was explicitly provided.
|
|
44
|
+
* @param {*} value - The CLI option value to test.
|
|
45
|
+
* @returns {boolean} `true` if `value` is neither `undefined`, `null` nor an empty string, `false` otherwise.
|
|
46
|
+
*/
|
|
47
|
+
function hasCliValue(value) {
|
|
48
|
+
return !(value === undefined || value === null || value === '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Derives an exclude pattern string from a diagramrc configuration object.
|
|
53
|
+
*
|
|
54
|
+
* @param {Object} diagramRc - The parsed diagramrc configuration, may be undefined.
|
|
55
|
+
* @returns {string|null} `diagramRc.exclude` if present; otherwise a comma-joined string of trimmed `diagramRc.ignore` entries when that array is non-empty; otherwise `null`.
|
|
56
|
+
*/
|
|
57
|
+
function resolveDiagramRcExclude(diagramRc) {
|
|
58
|
+
if (diagramRc?.exclude) {
|
|
59
|
+
return diagramRc.exclude;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(diagramRc?.ignore) && diagramRc.ignore.length > 0) {
|
|
62
|
+
return diagramRc.ignore
|
|
63
|
+
.map((item) => String(item).trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.join(',');
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply diagram RC and command defaults to a CLI options object for the requested fields.
|
|
72
|
+
*
|
|
73
|
+
* Merges provided `options` with defaults (module `DEFAULTS` overridden by `commandDefaults`),
|
|
74
|
+
* filling missing values for any of the specified `fields` from `diagramRc` where available.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} options - CLI options provided by the user; values are preserved when present.
|
|
77
|
+
* @param {Object} diagramRc - Parsed diagram configuration (e.g. from a diagramrc file).
|
|
78
|
+
* @param {string[]} [fields=['patterns','exclude','maxFiles','theme']] - Which option keys to populate from defaults.
|
|
79
|
+
* @param {Object} [commandDefaults={}] - Overrides applied on top of the module `DEFAULTS`.
|
|
80
|
+
* @returns {Object} The resolved options object with defaults applied for the requested fields.
|
|
81
|
+
*/
|
|
82
|
+
function applyDiagramRcDefaults(
|
|
83
|
+
options,
|
|
84
|
+
diagramRc,
|
|
85
|
+
fields = ['patterns', 'exclude', 'maxFiles', 'theme'],
|
|
86
|
+
commandDefaults = {}
|
|
87
|
+
) {
|
|
88
|
+
const resolved = { ...(options || {}) };
|
|
89
|
+
const defaults = { ...DEFAULTS, ...(commandDefaults || {}) };
|
|
90
|
+
|
|
91
|
+
if (fields.includes('patterns') && !hasCliValue(resolved.patterns)) {
|
|
92
|
+
resolved.patterns = diagramRc?.patterns || defaults.patterns;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fields.includes('exclude')) {
|
|
96
|
+
if (!hasCliValue(resolved.exclude)) {
|
|
97
|
+
resolved.exclude = resolveDiagramRcExclude(diagramRc) || defaults.exclude;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (fields.includes('maxFiles') && !hasCliValue(resolved.maxFiles)) {
|
|
102
|
+
const rcMaxFiles = diagramRc?.maxFiles;
|
|
103
|
+
resolved.maxFiles = rcMaxFiles !== undefined ? String(rcMaxFiles) : String(defaults.maxFiles);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (fields.includes('theme') && !hasCliValue(resolved.theme)) {
|
|
107
|
+
resolved.theme = diagramRc?.theme || defaults.theme;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve a target path to an absolute project root directory; exits the process with code 2 on failure.
|
|
115
|
+
*
|
|
116
|
+
* @param {string|undefined} targetPath - Path to resolve; when falsy, the current working directory ('.') is used.
|
|
117
|
+
* @returns {string} The resolved absolute directory path.
|
|
118
|
+
* @throws Will exit the process with code 2 after printing an error if the path does not exist or is not a directory.
|
|
119
|
+
*/
|
|
120
|
+
function resolveRootPathOrExit(targetPath) {
|
|
121
|
+
const root = path.resolve(targetPath || '.');
|
|
122
|
+
try {
|
|
123
|
+
const stats = fs.statSync(root);
|
|
124
|
+
if (!stats.isDirectory()) {
|
|
125
|
+
console.error(chalk.red('❌ Path error:'), `Target is not a directory: ${root}`);
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
} catch (_error) {
|
|
129
|
+
console.error(chalk.red('❌ Path error:'), `Target directory not found: ${root}`);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
return root;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolvePathViaExistingAncestor(targetPath) {
|
|
136
|
+
const pending = [];
|
|
137
|
+
let probe = targetPath;
|
|
138
|
+
|
|
139
|
+
while (!fs.existsSync(probe)) {
|
|
140
|
+
pending.unshift(path.basename(probe));
|
|
141
|
+
const parent = path.dirname(probe);
|
|
142
|
+
if (parent === probe) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
probe = parent;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const canonicalBase = fs.realpathSync(probe);
|
|
149
|
+
return path.join(canonicalBase, ...pending);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validate and canonicalise an output path so it safely resides within a project root.
|
|
154
|
+
*
|
|
155
|
+
* Ensures `outputPath` is a non-empty string without null bytes, resolves it relative to
|
|
156
|
+
* the real path of `rootPath`, canonicalises through the nearest existing ancestor to
|
|
157
|
+
* tolerate non-existent segments, and rejects directory traversal that would place the
|
|
158
|
+
* resolved path outside the project root.
|
|
159
|
+
*
|
|
160
|
+
* @param {string} outputPath - The output path provided by the user (absolute or relative).
|
|
161
|
+
* @param {string} rootPath - The project root path used as the base for relative resolution.
|
|
162
|
+
* @returns {string} The canonical resolved output directory path.
|
|
163
|
+
* @throws {Error} When `outputPath` is missing or empty.
|
|
164
|
+
* @throws {Error} When `outputPath` contains null bytes.
|
|
165
|
+
* @throws {Error} When `rootPath` cannot be resolved to an existing project directory.
|
|
166
|
+
* @throws {Error} When the resolved path would traverse outside the project root.
|
|
167
|
+
*/
|
|
168
|
+
function validateOutputPath(outputPath, rootPath) {
|
|
169
|
+
if (typeof outputPath !== 'string' || outputPath.trim() === '') {
|
|
170
|
+
throw new Error('Invalid path: output path is required');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (outputPath.includes('\0')) {
|
|
174
|
+
throw new Error('Invalid path: null bytes detected');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let realRoot;
|
|
178
|
+
try {
|
|
179
|
+
realRoot = fs.realpathSync(rootPath);
|
|
180
|
+
} catch (_error) {
|
|
181
|
+
throw new Error(`Invalid project path: ${rootPath}`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const resolved = path.isAbsolute(outputPath)
|
|
185
|
+
? path.resolve(outputPath)
|
|
186
|
+
: path.resolve(realRoot, outputPath);
|
|
187
|
+
let canonicalResolved;
|
|
188
|
+
try {
|
|
189
|
+
canonicalResolved = resolvePathViaExistingAncestor(resolved);
|
|
190
|
+
} catch (_error) {
|
|
191
|
+
throw new Error(`Invalid path: cannot resolve "${outputPath}"`);
|
|
192
|
+
}
|
|
193
|
+
const relative = path.relative(realRoot, canonicalResolved);
|
|
194
|
+
|
|
195
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
196
|
+
throw new Error(`Invalid path: directory traversal detected in "${outputPath}"`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return canonicalResolved;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Retrieve the diagram configuration object from a CLI program context.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} program - The parsed CLI program object that may contain `diagramContext`.
|
|
206
|
+
* @returns {object} The `diagramRc` object found at `program.diagramContext.diagramRc`, or an empty object if not present.
|
|
207
|
+
*/
|
|
208
|
+
function getDiagramRcFromProgram(program) {
|
|
209
|
+
return program?.diagramContext?.diagramRc || {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Attempts to open the given URL in the user's default browser and detach the launcher.
|
|
214
|
+
*
|
|
215
|
+
* Logs a yellow warning message if the browser could not be opened.
|
|
216
|
+
*
|
|
217
|
+
* @param {string} url - The URL to open.
|
|
218
|
+
*/
|
|
219
|
+
function openPreviewUrl(url) {
|
|
220
|
+
const { cmd, args } = getOpenCommand(url, process.platform);
|
|
221
|
+
const handleOpenError = (error) => {
|
|
222
|
+
console.error(chalk.yellow('⚠️ Failed to open browser:'), error.message);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
const child = spawn(cmd, args, {
|
|
227
|
+
stdio: 'ignore',
|
|
228
|
+
detached: true,
|
|
229
|
+
windowsHide: true,
|
|
230
|
+
});
|
|
231
|
+
child.on('error', handleOpenError);
|
|
232
|
+
if (child && typeof child.unref === 'function') {
|
|
233
|
+
child.unref();
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
handleOpenError(error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Invoke the Mermaid CLI via available `npx` command candidates, optionally permitting automatic install.
|
|
242
|
+
*
|
|
243
|
+
* Attempts each `npx` candidate for the current platform until one successfully executes the provided arguments.
|
|
244
|
+
*
|
|
245
|
+
* @param {string[]} args - Arguments to pass to the Mermaid CLI.
|
|
246
|
+
* @param {Object} [options] - Call options.
|
|
247
|
+
* @param {boolean} [options.allowAutoInstall=false] - When true, prepends `-y` to allow `npx` to automatically install missing packages.
|
|
248
|
+
* @throws {Error} If all candidate `npx` executions fail, rethrows the last execution error; if no `npx` candidate exists, throws `Error('npx command not found')`.
|
|
249
|
+
*/
|
|
250
|
+
function runMermaidCli(args, options = {}) {
|
|
251
|
+
const mermaidArgs = options.allowAutoInstall ? ['-y', ...args] : args;
|
|
252
|
+
const candidates = getNpxCommandCandidates(process.platform);
|
|
253
|
+
let lastError = null;
|
|
254
|
+
for (const candidate of candidates) {
|
|
255
|
+
try {
|
|
256
|
+
execFileSync(candidate, mermaidArgs, { stdio: 'pipe', windowsHide: true });
|
|
257
|
+
return;
|
|
258
|
+
} catch (error) {
|
|
259
|
+
lastError = error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (lastError) throw lastError;
|
|
263
|
+
throw new Error('npx command not found');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Generate a Mermaid Live editor "pako" URL encoding the given diagram source.
|
|
268
|
+
*
|
|
269
|
+
* If the diagram is too large to encode or compression/encoding fails, the result will indicate the diagram is large.
|
|
270
|
+
* @param {string} mermaidCode - Mermaid diagram source to encode.
|
|
271
|
+
* @returns {{url: string|null, large: boolean}} An object with `url` set to the Mermaid Live edit URL when encoding succeeds, otherwise `null`; `large` is `true` when the diagram is too large or encoding failed, otherwise `false`.
|
|
272
|
+
*/
|
|
273
|
+
function createMermaidUrl(mermaidCode) {
|
|
274
|
+
if (mermaidCode.length > 5000) return { url: null, large: true };
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const payload = JSON.stringify({ code: mermaidCode });
|
|
278
|
+
const compressed = zlib.deflateSync(payload);
|
|
279
|
+
const encoded = compressed
|
|
280
|
+
.toString('base64')
|
|
281
|
+
.replace(/\+/g, '-')
|
|
282
|
+
.replace(/\//g, '_')
|
|
283
|
+
.replace(/=+$/g, '');
|
|
284
|
+
const url = `https://mermaid.live/edit#pako:${encoded}`;
|
|
285
|
+
if (url.length > 8000) return { url: null, large: true };
|
|
286
|
+
return { url, large: false };
|
|
287
|
+
} catch (_error) {
|
|
288
|
+
return { url: null, large: true };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Normalize a theme name to one of the permitted themes, using a fallback when the input is missing or unrecognised.
|
|
294
|
+
* @param {string|undefined|null} theme - Candidate theme name; may be undefined or null.
|
|
295
|
+
* @param {string} [fallback='default'] - Theme to use when `theme` is missing or not allowed.
|
|
296
|
+
* @returns {string} The allowed theme name (lowercased) if `theme` is permitted, otherwise the `fallback`.
|
|
297
|
+
*/
|
|
298
|
+
function normalizeThemeOption(theme, fallback = 'default') {
|
|
299
|
+
const normalized = String(theme || fallback).toLowerCase();
|
|
300
|
+
return ALLOWED_THEMES.includes(normalized) ? normalized : fallback;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Run the project analysis and optionally use or update an incremental cache.
|
|
305
|
+
*
|
|
306
|
+
* Executes the named analyzer for a project root, attempts to read a cached
|
|
307
|
+
* analysis when incremental mode is requested (disabled in CI), and writes an
|
|
308
|
+
* updated cache after a full analysis when applicable.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} rootPath - Absolute path to the project root to analyse.
|
|
311
|
+
* @param {object} options - Analyzer and pipeline options (e.g. `analyzer`, `incremental`).
|
|
312
|
+
* @param {string} commandName - Name of the invoking command used to build the cache key.
|
|
313
|
+
* @returns {{ analysis: object, analyzer: { name: string, version?: string }, incremental: { requested: boolean, used: boolean, reason: string } }}
|
|
314
|
+
* `analysis`: The analysis result produced by the analyzer.
|
|
315
|
+
* `analyzer`: Metadata about the analyzer used; when returned from cache this is read from `_meta.analyzer` or set to `{ name: <analyzerName>, version: 'unknown' }`.
|
|
316
|
+
* `incremental`: Metadata about incremental caching:
|
|
317
|
+
* - `requested`: whether incremental mode was requested via `options`.
|
|
318
|
+
* - `used`: whether a cached analysis was returned.
|
|
319
|
+
* - `reason`: human-readable reason for the incremental outcome (for example `not_requested`, `incremental_disabled_in_ci`, a cache miss reason, or the cached reason when used).
|
|
320
|
+
*/
|
|
321
|
+
async function runAnalysisPipeline(rootPath, options, commandName) {
|
|
322
|
+
const analyzerName = options.analyzer || 'default';
|
|
323
|
+
const incrementalRequested = Boolean(options.incremental);
|
|
324
|
+
const cacheKey = incrementalRequested && !process.env.CI
|
|
325
|
+
? buildCacheKey(commandName, { ...options, analyzer: analyzerName })
|
|
326
|
+
: null;
|
|
327
|
+
const incrementalState = {
|
|
328
|
+
requested: incrementalRequested,
|
|
329
|
+
used: false,
|
|
330
|
+
reason: 'not_requested',
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (incrementalRequested && process.env.CI) {
|
|
334
|
+
incrementalState.reason = 'incremental_disabled_in_ci';
|
|
335
|
+
} else if (incrementalRequested) {
|
|
336
|
+
const cached = readCachedAnalysis(rootPath, cacheKey);
|
|
337
|
+
if (cached.hit) {
|
|
338
|
+
incrementalState.used = true;
|
|
339
|
+
incrementalState.reason = cached.reason;
|
|
340
|
+
return {
|
|
341
|
+
analysis: cached.data,
|
|
342
|
+
analyzer: cached.data?._meta?.analyzer || { name: analyzerName, version: 'unknown' },
|
|
343
|
+
incremental: incrementalState,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
incrementalState.reason = cached.reason;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const { analyzer, analysis } = await runAnalyzer(analyzerName, rootPath, options);
|
|
350
|
+
|
|
351
|
+
if (cacheKey) {
|
|
352
|
+
writeCachedAnalysis(rootPath, cacheKey, {
|
|
353
|
+
...analysis,
|
|
354
|
+
_meta: {
|
|
355
|
+
...(analysis._meta || {}),
|
|
356
|
+
analyzer,
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return { analysis, analyzer, incremental: incrementalState };
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Write an architecture intermediate representation (IR) derived from analysis to disk when requested.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} rootPath - Project root directory where the IR will be written.
|
|
368
|
+
* @param {Object} analysis - Analyzer output used to produce the architecture IR.
|
|
369
|
+
* @param {Object} analyzer - Analyzer metadata included in the IR conversion (e.g. name, version).
|
|
370
|
+
* @param {boolean} shouldWrite - If falsy, writing is skipped.
|
|
371
|
+
* @returns {?*} The result of `writeArchitectureIR` when the IR is written, or `null` if writing was skipped.
|
|
372
|
+
*/
|
|
373
|
+
function maybeWriteArchitectureIR(rootPath, analysis, analyzer, shouldWrite) {
|
|
374
|
+
if (!shouldWrite) return null;
|
|
375
|
+
const ir = toArchitectureIR(analysis, { rootPath, analyzer });
|
|
376
|
+
return writeArchitectureIR(rootPath, ir);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
module.exports = {
|
|
380
|
+
ALLOWED_THEMES,
|
|
381
|
+
DEFAULTS,
|
|
382
|
+
applyDiagramRcDefaults,
|
|
383
|
+
createMermaidUrl,
|
|
384
|
+
findClosestMatch,
|
|
385
|
+
formatSuggestion,
|
|
386
|
+
getDiagramRcFromProgram,
|
|
387
|
+
maybeWriteArchitectureIR,
|
|
388
|
+
normalizeThemeOption,
|
|
389
|
+
openPreviewUrl,
|
|
390
|
+
resolveRootPathOrExit,
|
|
391
|
+
resolvePathViaExistingAncestor,
|
|
392
|
+
runAnalysisPipeline,
|
|
393
|
+
runMermaidCli,
|
|
394
|
+
splitList,
|
|
395
|
+
validateOutputPath,
|
|
396
|
+
};
|