@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,102 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const {
|
|
5
|
+
detectLanguage,
|
|
6
|
+
inferType,
|
|
7
|
+
extractImportsWithPositions,
|
|
8
|
+
normalizePath,
|
|
9
|
+
} = require('./analysis-generation-utils');
|
|
10
|
+
const { inferRoleTags } = require('./analysis-generation-role-tags');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts component metadata from a set of files beneath a root path.
|
|
14
|
+
*
|
|
15
|
+
* Processes each path in `uniqueFiles`, reading file contents (files larger than 10 MB are skipped), detecting language and relative path, identifying entry points, ensuring unique component names, extracting imports and type information, and collecting directory and language statistics.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} rootPath - Root directory used to compute relative file paths.
|
|
18
|
+
* @param {string[]} uniqueFiles - Array of file paths to analyse.
|
|
19
|
+
* @returns {{components: Array, entryPoints: string[], languages: Object<string, number>, directories: string[]}} An object containing:
|
|
20
|
+
* - `components`: array of component descriptors with properties `{ name, originalName, filePath, type, imports, roleTags, directory }`.
|
|
21
|
+
* - `entryPoints`: list of relative file paths that match common entry-point filenames.
|
|
22
|
+
* - `languages`: mapping of detected language to file count.
|
|
23
|
+
* - `directories`: sorted array of unique relative directory names.
|
|
24
|
+
*/
|
|
25
|
+
function extractComponents(rootPath, uniqueFiles) {
|
|
26
|
+
const components = [];
|
|
27
|
+
const languages = {};
|
|
28
|
+
const directories = new Set();
|
|
29
|
+
const entryPoints = [];
|
|
30
|
+
const seenNames = new Set();
|
|
31
|
+
|
|
32
|
+
for (const filePath of uniqueFiles) {
|
|
33
|
+
try {
|
|
34
|
+
const fd = fs.openSync(filePath, 'r');
|
|
35
|
+
let content;
|
|
36
|
+
try {
|
|
37
|
+
const { size } = fs.fstatSync(fd);
|
|
38
|
+
if (size > 10 * 1024 * 1024) {
|
|
39
|
+
console.warn(chalk.yellow(`⚠️ Skipping large file: ${path.basename(filePath)} (${(size / 1024 / 1024).toFixed(2)} MB)`));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
content = fs.readFileSync(fd, 'utf-8');
|
|
43
|
+
} finally {
|
|
44
|
+
// Close fd even if continue is called in the size check above (uniqueFiles loop)
|
|
45
|
+
fs.closeSync(fd);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const lang = detectLanguage(filePath);
|
|
49
|
+
let rel = normalizePath(path.relative(rootPath, filePath));
|
|
50
|
+
const dir = path.dirname(rel);
|
|
51
|
+
if (dir === '.') {
|
|
52
|
+
rel = `./${rel}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
languages[lang] = (languages[lang] || 0) + 1;
|
|
56
|
+
if (dir !== '.') directories.add(dir);
|
|
57
|
+
|
|
58
|
+
const entryPattern = /\/(index|main|app|server)\.(ts|js|tsx|jsx|mts|mjs|py|go|rs)$/i;
|
|
59
|
+
if (entryPattern.test(rel)) {
|
|
60
|
+
entryPoints.push(rel);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const baseName = path.basename(filePath, path.extname(filePath));
|
|
64
|
+
let uniqueName = baseName;
|
|
65
|
+
let counter = 1;
|
|
66
|
+
while (seenNames.has(uniqueName)) {
|
|
67
|
+
uniqueName = `${baseName}_${counter}`;
|
|
68
|
+
counter++;
|
|
69
|
+
}
|
|
70
|
+
seenNames.add(uniqueName);
|
|
71
|
+
|
|
72
|
+
const imports = extractImportsWithPositions(content, lang);
|
|
73
|
+
const type = inferType(filePath, content);
|
|
74
|
+
|
|
75
|
+
components.push({
|
|
76
|
+
name: uniqueName,
|
|
77
|
+
originalName: baseName,
|
|
78
|
+
filePath: rel,
|
|
79
|
+
type,
|
|
80
|
+
imports,
|
|
81
|
+
roleTags: inferRoleTags(rel, baseName, content, imports, type),
|
|
82
|
+
directory: dir,
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (process.env.DEBUG) {
|
|
86
|
+
const safePath = path.basename(filePath);
|
|
87
|
+
console.error(chalk.gray(`Skipped ${safePath}: ${error.message}`));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
components,
|
|
94
|
+
entryPoints,
|
|
95
|
+
languages,
|
|
96
|
+
directories: [...directories].sort(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
extractComponents,
|
|
102
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const {
|
|
2
|
+
getImportPath,
|
|
3
|
+
resolveInternalImport,
|
|
4
|
+
findComponentByResolvedPath,
|
|
5
|
+
} = require('./analysis-generation-utils');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Populate each component's `dependencies` with the names of internal components it imports.
|
|
9
|
+
*
|
|
10
|
+
* Iterates over `components`, resets each component's `dependencies` to an empty array, resolves internal imports relative to `rootPath`, and appends the `name` of any component that matches a resolved import. Unresolved or external imports are ignored.
|
|
11
|
+
*
|
|
12
|
+
* @param {Array<Object>} components - Array of component objects; each should have `imports` (array) and `filePath` (string). This function sets `dependencies` on each component.
|
|
13
|
+
* @param {string} rootPath - Project root path used when resolving internal import paths.
|
|
14
|
+
*/
|
|
15
|
+
function linkDependencies(components, rootPath) {
|
|
16
|
+
for (const comp of components) {
|
|
17
|
+
const deps = new Set();
|
|
18
|
+
const imports = Array.isArray(comp.imports) ? comp.imports : [];
|
|
19
|
+
for (const imp of imports) {
|
|
20
|
+
const importPath = getImportPath(imp);
|
|
21
|
+
if (!importPath) continue;
|
|
22
|
+
const resolved = resolveInternalImport(comp.filePath, importPath, rootPath);
|
|
23
|
+
if (!resolved) continue;
|
|
24
|
+
const dep = findComponentByResolvedPath(components, resolved);
|
|
25
|
+
if (dep && dep.name) deps.add(dep.name);
|
|
26
|
+
}
|
|
27
|
+
comp.dependencies = [...deps];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
linkDependencies,
|
|
33
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { glob } = require('glob');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve candidate files either from an explicit list or by expanding glob patterns, returning a de-duplicated list of absolute file paths.
|
|
8
|
+
*
|
|
9
|
+
* When `explicitFiles` is a non-empty array it takes precedence: each entry is resolved to an absolute path (relative entries resolved against `rootPath`), discarded if outside `rootPath`, and kept only if it exists and is a file. When `explicitFiles` is not provided or empty, `patterns` are expanded with `glob` using `rootPath` as the current working directory and `exclude` as ignore patterns; invalid glob patterns are skipped with a warning.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} rootPath - Base directory used to resolve relative explicit paths and as the `cwd` for globbing.
|
|
12
|
+
* @param {string[]} patterns - Glob patterns to expand when `explicitFiles` is not provided or empty.
|
|
13
|
+
* @param {string|string[]} exclude - Patterns to pass to glob's `ignore` option.
|
|
14
|
+
* @param {string[]} explicitFiles - Optional explicit file paths; if provided and non-empty these are used instead of globbing.
|
|
15
|
+
* @returns {string[]} An array of absolute, existing file paths with duplicates removed.
|
|
16
|
+
*/
|
|
17
|
+
async function resolveCandidateFiles(rootPath, patterns, exclude, explicitFiles) {
|
|
18
|
+
if (Array.isArray(explicitFiles) && explicitFiles.length > 0) {
|
|
19
|
+
return [...new Set(explicitFiles
|
|
20
|
+
.map((filePath) => {
|
|
21
|
+
const absolute = path.isAbsolute(filePath) ? filePath : path.resolve(rootPath, filePath);
|
|
22
|
+
const relativeToRoot = path.relative(rootPath, absolute);
|
|
23
|
+
if (relativeToRoot.startsWith('..')) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return absolute;
|
|
27
|
+
})
|
|
28
|
+
.filter((filePath) => filePath && fs.existsSync(filePath) && fs.statSync(filePath).isFile())
|
|
29
|
+
)];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const files = [];
|
|
33
|
+
for (const pattern of patterns) {
|
|
34
|
+
if (!pattern || pattern.trim() === '') continue;
|
|
35
|
+
try {
|
|
36
|
+
const matches = await glob(pattern.trim(), { cwd: rootPath, absolute: true, ignore: exclude });
|
|
37
|
+
files.push(...matches);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
console.warn(chalk.yellow(`⚠️ Invalid pattern: ${pattern} — ${message}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(files)];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
resolveCandidateFiles,
|
|
48
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a comma-separated string into an array of trimmed, non-empty values.
|
|
3
|
+
*
|
|
4
|
+
* @param {string|null|undefined} input - Comma-separated string to parse.
|
|
5
|
+
* @returns {string[]} Array of trimmed non-empty strings; empty array if input is falsy.
|
|
6
|
+
*/
|
|
7
|
+
function parseCsv(input) {
|
|
8
|
+
if (!input) return [];
|
|
9
|
+
return input.split(',').map(s => s.trim()).filter(Boolean);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Determine and normalise the maximum number of files to analyse.
|
|
14
|
+
*
|
|
15
|
+
* @param {object} options - Options object.
|
|
16
|
+
* @param {number|string} [options.maxFiles] - Desired maximum; parsed as a base-10 integer.
|
|
17
|
+
* @returns {number} The validated `maxFiles` clamped to the range 1–10000; defaults to 100 when invalid.
|
|
18
|
+
*/
|
|
19
|
+
function parseMaxFiles(options) {
|
|
20
|
+
let maxFiles = parseInt(options.maxFiles, 10);
|
|
21
|
+
if (isNaN(maxFiles) || maxFiles < 1 || maxFiles > 10000) {
|
|
22
|
+
maxFiles = 100;
|
|
23
|
+
}
|
|
24
|
+
return Math.min(Math.max(maxFiles, 1), 10000);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse and return file glob patterns from an options object.
|
|
29
|
+
*
|
|
30
|
+
* If `options.patterns` is provided as a comma-separated string it is split
|
|
31
|
+
* into an array of glob patterns; otherwise a default set of common source
|
|
32
|
+
* file globs is returned.
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} options - Options object that may contain a `patterns` property.
|
|
35
|
+
* When present, `options.patterns` must be a comma-separated string of glob patterns.
|
|
36
|
+
* @returns {string[]} Array of glob patterns.
|
|
37
|
+
* @throws {TypeError} If `options.patterns` is present and is not a string.
|
|
38
|
+
*/
|
|
39
|
+
function parsePatterns(options) {
|
|
40
|
+
let patterns = ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.py', '**/*.go', '**/*.rs'];
|
|
41
|
+
if (options.patterns) {
|
|
42
|
+
if (typeof options.patterns !== 'string') {
|
|
43
|
+
throw new TypeError('patterns must be a string');
|
|
44
|
+
}
|
|
45
|
+
patterns = parseCsv(options.patterns);
|
|
46
|
+
}
|
|
47
|
+
return patterns;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Parse exclude glob patterns from the given options, falling back to a default set.
|
|
52
|
+
*
|
|
53
|
+
* @param {Object} options - Configuration object; may include an `exclude` property.
|
|
54
|
+
* @param {string} [options.exclude] - Comma-delimited string of glob patterns to use instead of defaults.
|
|
55
|
+
* @returns {string[]} The array of exclude glob patterns.
|
|
56
|
+
* @throws {TypeError} If `options.exclude` is provided but is not a string.
|
|
57
|
+
*/
|
|
58
|
+
function parseExclude(options) {
|
|
59
|
+
let exclude = ['node_modules/**', '.git/**', 'dist/**', 'build/**', '*.test.*', '*.spec.*'];
|
|
60
|
+
if (options.exclude) {
|
|
61
|
+
if (typeof options.exclude !== 'string') {
|
|
62
|
+
throw new TypeError('exclude must be a string');
|
|
63
|
+
}
|
|
64
|
+
exclude = parseCsv(options.exclude);
|
|
65
|
+
}
|
|
66
|
+
return exclude;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
module.exports = {
|
|
70
|
+
parseMaxFiles,
|
|
71
|
+
parsePatterns,
|
|
72
|
+
parseExclude,
|
|
73
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { parseMaxFiles, parsePatterns, parseExclude } = require('./analysis-generation-analyze-options');
|
|
3
|
+
const { resolveCandidateFiles } = require('./analysis-generation-analyze-files');
|
|
4
|
+
const { extractComponents } = require('./analysis-generation-analyze-components');
|
|
5
|
+
const { linkDependencies } = require('./analysis-generation-analyze-dependencies');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Orchestrates a multi-step analysis of files under a root path and returns a structured summary of the results.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} rootPath - Filesystem root to analyse.
|
|
11
|
+
* @param {Object} options - Analysis options.
|
|
12
|
+
* @param {string[]} [options.includeFiles] - Explicit files to include in the analysis.
|
|
13
|
+
* @param {boolean} [options.deterministic] - If true, sort candidate files deterministically.
|
|
14
|
+
* @param {...*} [options.*] - Other options may influence patterns, exclusions and max-files.
|
|
15
|
+
* @returns {Object} Analysis result containing:
|
|
16
|
+
* - rootPath: the analysed root path.
|
|
17
|
+
* - components: extracted component data.
|
|
18
|
+
* - entryPoints: discovered entry points.
|
|
19
|
+
* - languages: detected languages metadata.
|
|
20
|
+
* - directories: analysed directory metadata.
|
|
21
|
+
* - totalFilesFound: total number of candidate files discovered before truncation.
|
|
22
|
+
* - maxFilesApplied: the max-files limit that was applied.
|
|
23
|
+
*/
|
|
24
|
+
async function analyze(rootPath, options) {
|
|
25
|
+
const maxFiles = parseMaxFiles(options);
|
|
26
|
+
const patterns = parsePatterns(options);
|
|
27
|
+
const exclude = parseExclude(options);
|
|
28
|
+
const explicitFiles = Array.isArray(options.includeFiles) ? options.includeFiles : [];
|
|
29
|
+
let allUniqueFiles = await resolveCandidateFiles(rootPath, patterns, exclude, explicitFiles);
|
|
30
|
+
|
|
31
|
+
if (options.deterministic) {
|
|
32
|
+
allUniqueFiles = allUniqueFiles.sort();
|
|
33
|
+
}
|
|
34
|
+
const totalFilesFound = allUniqueFiles.length;
|
|
35
|
+
const uniqueFiles = allUniqueFiles.slice(0, maxFiles);
|
|
36
|
+
|
|
37
|
+
if (totalFilesFound > maxFiles) {
|
|
38
|
+
console.warn(
|
|
39
|
+
chalk.yellow(
|
|
40
|
+
`⚠️ Max-files limit reached: analyzing ${maxFiles} of ${totalFilesFound} files. Use --max-files ${Math.ceil(totalFilesFound / 100) * 100} to expand.`
|
|
41
|
+
)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { components, entryPoints, languages, directories } = extractComponents(rootPath, uniqueFiles);
|
|
46
|
+
linkDependencies(components, rootPath);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
rootPath,
|
|
50
|
+
components,
|
|
51
|
+
entryPoints,
|
|
52
|
+
languages,
|
|
53
|
+
directories,
|
|
54
|
+
patterns,
|
|
55
|
+
exclude,
|
|
56
|
+
totalFilesFound,
|
|
57
|
+
maxFilesApplied: maxFiles,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = {
|
|
62
|
+
analyze,
|
|
63
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
const ROLE_COLOURS = Object.freeze({
|
|
2
|
+
llm: { fill: '#6d28d9', color: '#fff' },
|
|
3
|
+
agent: { fill: '#7c3aed', color: '#fff' },
|
|
4
|
+
tool: { fill: '#1d4ed8', color: '#fff' },
|
|
5
|
+
memory: { fill: '#065f46', color: '#fff' },
|
|
6
|
+
database: { fill: '#0e7490', color: '#fff' },
|
|
7
|
+
auth: { fill: '#9d174d', color: '#fff' },
|
|
8
|
+
security: { fill: '#991b1b', color: '#fff' },
|
|
9
|
+
user: { fill: '#15803d', color: '#fff' },
|
|
10
|
+
events: { fill: '#b45309', color: '#fff' },
|
|
11
|
+
integrations: { fill: '#0369a1', color: '#fff' },
|
|
12
|
+
service: { fill: '#374151', color: '#fff' },
|
|
13
|
+
general: { fill: '#374151', color: '#fff' },
|
|
14
|
+
});
|
|
15
|
+
Object.values(ROLE_COLOURS).forEach((entry) => Object.freeze(entry));
|
|
16
|
+
|
|
17
|
+
const ROLE_ARCH_ICON = Object.freeze({
|
|
18
|
+
llm: 'cloud',
|
|
19
|
+
agent: 'server',
|
|
20
|
+
tool: 'disk',
|
|
21
|
+
memory: 'database',
|
|
22
|
+
database: 'database',
|
|
23
|
+
auth: 'server',
|
|
24
|
+
security: 'server',
|
|
25
|
+
user: 'internet',
|
|
26
|
+
events: 'server',
|
|
27
|
+
integrations: 'cloud',
|
|
28
|
+
service: 'server',
|
|
29
|
+
general: 'server',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const SUPPORTED_DIAGRAM_TYPES = Object.freeze([
|
|
33
|
+
'architecture',
|
|
34
|
+
'sequence',
|
|
35
|
+
'dependency',
|
|
36
|
+
'class',
|
|
37
|
+
'flow',
|
|
38
|
+
'database',
|
|
39
|
+
'erd',
|
|
40
|
+
'user',
|
|
41
|
+
'events',
|
|
42
|
+
'auth',
|
|
43
|
+
'security',
|
|
44
|
+
'agent',
|
|
45
|
+
'c4context',
|
|
46
|
+
'rag',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
ROLE_COLOURS,
|
|
51
|
+
ROLE_ARCH_ICON,
|
|
52
|
+
SUPPORTED_DIAGRAM_TYPES,
|
|
53
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { ROLE_ARCH_ICON } = require('./analysis-generation-constants');
|
|
3
|
+
const {
|
|
4
|
+
normalizePath,
|
|
5
|
+
escapeMermaid,
|
|
6
|
+
sanitize,
|
|
7
|
+
mapSafeNames,
|
|
8
|
+
byNameIndex,
|
|
9
|
+
} = require('./analysis-generation-utils');
|
|
10
|
+
const { graphNote, architectureNote } = require('./analysis-generation-diagrams-empty');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a Mermaid "architecture-beta" diagram for the provided components, optionally restricted to a focus path.
|
|
14
|
+
*
|
|
15
|
+
* The diagram groups components by directory, assigns an icon based on role tags or type, marks entry points with a star,
|
|
16
|
+
* and draws dependency edges between components. If `data` is missing or malformed, or if no components match `focus`,
|
|
17
|
+
* a text note is returned instead of the diagram.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} data - Input data object containing architecture information.
|
|
20
|
+
* @param {Array<Object>} data.components - List of component objects to include in the diagram.
|
|
21
|
+
* @param {Array<string>} [data.entryPoints] - Optional list of entry-point file paths used to mark entry nodes.
|
|
22
|
+
* @param {string} [focus] - Optional path or name to filter components; normalised before matching.
|
|
23
|
+
* @returns {string} The complete Mermaid diagram as a newline-joined string, or a note message when no data/components are available.
|
|
24
|
+
*/
|
|
25
|
+
function generateArchitecture(data, focus) {
|
|
26
|
+
if (!data || !Array.isArray(data.components)) {
|
|
27
|
+
return architectureNote('No data available');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const stripTrailingSlashes = (value) => (value.length > 1 ? value.replace(/\/+$/, '') : value);
|
|
31
|
+
const focusNorm = focus ? stripTrailingSlashes(normalizePath(focus)) : null;
|
|
32
|
+
const comps = focusNorm
|
|
33
|
+
? data.components.filter((component) => {
|
|
34
|
+
const fp = stripTrailingSlashes(normalizePath(component.filePath || ''));
|
|
35
|
+
return fp === focusNorm || fp.startsWith(`${focusNorm}/`) || component.name === focusNorm;
|
|
36
|
+
})
|
|
37
|
+
: data.components;
|
|
38
|
+
|
|
39
|
+
if (comps.length === 0) {
|
|
40
|
+
return graphNote(`No components found${focus ? ` for focus: ${escapeMermaid(focus)}` : ''}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const iconFor = (component) => {
|
|
44
|
+
const tags = Array.isArray(component.roleTags) ? component.roleTags : [];
|
|
45
|
+
const priority = ['llm', 'agent', 'tool', 'memory', 'database', 'auth', 'user', 'events'];
|
|
46
|
+
for (const tag of priority) {
|
|
47
|
+
if (tags.includes(tag) && ROLE_ARCH_ICON[tag]) return ROLE_ARCH_ICON[tag];
|
|
48
|
+
}
|
|
49
|
+
return component.type === 'service' ? 'server' : 'disk';
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const entryNames = new Set(
|
|
53
|
+
(data.entryPoints || []).map((ep) => path.basename(ep, path.extname(ep)))
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const byDir = new Map();
|
|
57
|
+
for (const component of comps) {
|
|
58
|
+
const dir = component.directory === '.' ? 'root' : (component.directory || 'root');
|
|
59
|
+
if (!byDir.has(dir)) byDir.set(dir, []);
|
|
60
|
+
byDir.get(dir).push(component);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const lines = ['architecture-beta'];
|
|
64
|
+
const safeNames = mapSafeNames(comps);
|
|
65
|
+
const seenGroupIds = new Set();
|
|
66
|
+
|
|
67
|
+
for (const [dir, items] of byDir) {
|
|
68
|
+
if (items.length === 0) continue;
|
|
69
|
+
const groupId = sanitize(dir === 'root' ? 'root_group' : dir);
|
|
70
|
+
if (seenGroupIds.has(groupId)) continue;
|
|
71
|
+
seenGroupIds.add(groupId);
|
|
72
|
+
const displayDir = dir === 'root' ? 'Root' : escapeMermaid(dir);
|
|
73
|
+
lines.push(` group ${groupId}(cloud)[${displayDir}]`);
|
|
74
|
+
for (const component of items) {
|
|
75
|
+
const safe = safeNames.get(component);
|
|
76
|
+
if (!safe) continue;
|
|
77
|
+
const icon = iconFor(component);
|
|
78
|
+
const label = `${escapeMermaid(component.originalName)}${entryNames.has(component.originalName) ? ' ⭐' : ''}`;
|
|
79
|
+
lines.push(` service ${safe}(${icon})[${label}] in ${groupId}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const edges = new Set();
|
|
84
|
+
const byName = byNameIndex(comps);
|
|
85
|
+
for (const component of comps) {
|
|
86
|
+
const from = safeNames.get(component);
|
|
87
|
+
if (!from) continue;
|
|
88
|
+
for (const depName of component.dependencies || []) {
|
|
89
|
+
const dep = byName.get(depName);
|
|
90
|
+
if (!dep) continue;
|
|
91
|
+
const to = safeNames.get(dep);
|
|
92
|
+
if (!to || to === from) continue;
|
|
93
|
+
const key = `${from}->${to}`;
|
|
94
|
+
if (edges.has(key)) continue;
|
|
95
|
+
edges.add(key);
|
|
96
|
+
lines.push(` ${from}:B --> T:${to}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
generateArchitecture,
|
|
105
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const {
|
|
2
|
+
normalizePath,
|
|
3
|
+
escapeMermaid,
|
|
4
|
+
sanitize,
|
|
5
|
+
getImportPath,
|
|
6
|
+
resolveInternalImport,
|
|
7
|
+
findComponentByResolvedPath,
|
|
8
|
+
getExternalPackageName,
|
|
9
|
+
} = require('./analysis-generation-utils');
|
|
10
|
+
const { graphNote, noteNode } = require('./analysis-generation-diagrams-empty');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate a Mermaid LR dependency graph showing component-to-component and external-package imports.
|
|
14
|
+
*
|
|
15
|
+
* Produces a `graph LR` string with edges from importing components to the components or external packages they import. External packages are rendered as distinct nodes and styled with an orange fill and white text. If `focus` is provided the graph is limited to components whose file path equals or is nested under the normalized focus path.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} data - Analysis data containing components and a rootPath used to resolve internal imports. Expected shape: `{ components: Array, rootPath?: string }`.
|
|
18
|
+
* @param {string} [focus] - Optional file or directory path to restrict the graph to a subtree; the path is normalised before matching.
|
|
19
|
+
* @returns {string} A Mermaid `graph LR` diagram as a string. If `data` is missing or `data.components` is not an array, the returned diagram is a left-to-right note stating "No data available". If no components match `focus`, the diagram contains a "No components found" note.
|
|
20
|
+
*/
|
|
21
|
+
function generateDependency(data, focus) {
|
|
22
|
+
if (!data || !Array.isArray(data.components)) {
|
|
23
|
+
return graphNote('No data available', 'LR');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const lines = ['graph LR'];
|
|
27
|
+
const focusNorm = focus ? normalizePath(focus) : null;
|
|
28
|
+
const comps = focusNorm ? data.components.filter((component) => {
|
|
29
|
+
const normalizedPath = normalizePath(component.filePath || '');
|
|
30
|
+
return normalizedPath === focusNorm || normalizedPath.startsWith(`${focusNorm}/`);
|
|
31
|
+
}) : data.components;
|
|
32
|
+
|
|
33
|
+
if (comps.length === 0) {
|
|
34
|
+
lines.push(noteNode('No components found'));
|
|
35
|
+
return lines.join('\n');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const external = new Set();
|
|
39
|
+
for (const component of comps) {
|
|
40
|
+
const imports = Array.isArray(component.imports) ? component.imports : [];
|
|
41
|
+
for (const importInfo of imports) {
|
|
42
|
+
const importPath = getImportPath(importInfo);
|
|
43
|
+
if (!importPath) continue;
|
|
44
|
+
if (!importPath.startsWith('.')) {
|
|
45
|
+
const pkg = getExternalPackageName(importPath);
|
|
46
|
+
if (pkg) {
|
|
47
|
+
external.add(pkg);
|
|
48
|
+
lines.push(` ${sanitize(pkg)}["${escapeMermaid(pkg)}"] --> ${sanitize(component.name)}`);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
const basePath = resolveInternalImport(component.filePath, importPath, data.rootPath);
|
|
52
|
+
if (!basePath) continue;
|
|
53
|
+
const resolved = findComponentByResolvedPath(comps, basePath);
|
|
54
|
+
if (resolved) lines.push(` ${sanitize(component.name)} --> ${sanitize(resolved.name)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const packageName of external) {
|
|
60
|
+
lines.push(` style ${sanitize(packageName)} fill:#f59e0b,color:#fff`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
generateDependency,
|
|
68
|
+
};
|