@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.
- 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 +178 -1761
- 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,198 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
|
|
3
|
+
const ExitCodes = {
|
|
4
|
+
SUCCESS: 0,
|
|
5
|
+
VALIDATION_FAILED: 1,
|
|
6
|
+
CONFIG_ERROR: 2
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detect CI environment and encoding support
|
|
11
|
+
*/
|
|
12
|
+
function detectEnvironment() {
|
|
13
|
+
const isCI = process.env.CI === 'true' || !process.stdout.isTTY;
|
|
14
|
+
const locale = `${process.env.LC_ALL || ''} ${process.env.LC_CTYPE || ''} ${process.env.LANG || ''}`;
|
|
15
|
+
const windowsUnicodeTerminal = Boolean(
|
|
16
|
+
process.env.WT_SESSION ||
|
|
17
|
+
process.env.TERM_PROGRAM === 'vscode' ||
|
|
18
|
+
process.env.TERM === 'xterm-256color'
|
|
19
|
+
);
|
|
20
|
+
const supportsUnicode = process.platform !== 'win32' ||
|
|
21
|
+
windowsUnicodeTerminal ||
|
|
22
|
+
/UTF-?8/i.test(locale);
|
|
23
|
+
|
|
24
|
+
return { isCI, supportsUnicode };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get icons based on environment
|
|
29
|
+
*/
|
|
30
|
+
function getIcons(env) {
|
|
31
|
+
if (env.isCI || !env.supportsUnicode) {
|
|
32
|
+
return {
|
|
33
|
+
success: '[OK]',
|
|
34
|
+
error: '[FAIL]',
|
|
35
|
+
warning: '[WARN]',
|
|
36
|
+
skipped: '[SKIP]',
|
|
37
|
+
arrow: '->'
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
success: '✅',
|
|
42
|
+
error: '❌',
|
|
43
|
+
warning: '⚠️',
|
|
44
|
+
skipped: '⏭️',
|
|
45
|
+
arrow: '→'
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format results for console output
|
|
51
|
+
* @param {Object} results - Validation results
|
|
52
|
+
* @param {Object} options - Output options
|
|
53
|
+
* @param {number} startTime - Start timestamp for duration calculation
|
|
54
|
+
* @returns {number} Exit code
|
|
55
|
+
*/
|
|
56
|
+
function formatConsole(results, options = {}, startTime = Date.now()) {
|
|
57
|
+
const env = detectEnvironment();
|
|
58
|
+
const icons = getIcons(env);
|
|
59
|
+
const { verbose } = options;
|
|
60
|
+
|
|
61
|
+
// Validate startTime
|
|
62
|
+
if (!Number.isFinite(startTime)) {
|
|
63
|
+
startTime = Date.now();
|
|
64
|
+
}
|
|
65
|
+
const duration = Math.max(0, Date.now() - startTime);
|
|
66
|
+
|
|
67
|
+
const safeResults = results && typeof results === 'object' ? results : {};
|
|
68
|
+
const summary = safeResults.summary && typeof safeResults.summary === 'object' ? safeResults.summary : {};
|
|
69
|
+
const safeRules = Array.isArray(safeResults.rules) ? safeResults.rules : [];
|
|
70
|
+
const computed = safeRules.reduce((acc, item) => {
|
|
71
|
+
const rule = item && typeof item === 'object' ? item : {};
|
|
72
|
+
if (rule.status === 'failed') {
|
|
73
|
+
acc.failed++;
|
|
74
|
+
} else if (rule.status === 'skipped') {
|
|
75
|
+
acc.skipped++;
|
|
76
|
+
} else {
|
|
77
|
+
acc.passed++;
|
|
78
|
+
}
|
|
79
|
+
if (Array.isArray(rule.violations)) {
|
|
80
|
+
acc.violations += rule.violations.length;
|
|
81
|
+
}
|
|
82
|
+
return acc;
|
|
83
|
+
}, { total: safeRules.length, passed: 0, failed: 0, skipped: 0, violations: 0 });
|
|
84
|
+
const total = Number.isInteger(summary.total) && summary.total >= 0
|
|
85
|
+
? Math.max(summary.total, computed.total)
|
|
86
|
+
: computed.total;
|
|
87
|
+
const passed = Number.isInteger(summary.passed) && summary.passed >= 0
|
|
88
|
+
? Math.max(summary.passed, computed.passed)
|
|
89
|
+
: computed.passed;
|
|
90
|
+
const failed = Number.isInteger(summary.failed) && summary.failed >= 0
|
|
91
|
+
? Math.max(summary.failed, computed.failed)
|
|
92
|
+
: computed.failed;
|
|
93
|
+
const violations = Number.isInteger(summary.violations) && summary.violations >= 0
|
|
94
|
+
? Math.max(summary.violations, computed.violations)
|
|
95
|
+
: computed.violations;
|
|
96
|
+
|
|
97
|
+
// Configure chalk for CI
|
|
98
|
+
const c = new chalk.Instance({ level: env.isCI ? 0 : undefined });
|
|
99
|
+
|
|
100
|
+
let hasFailures = false;
|
|
101
|
+
|
|
102
|
+
for (const rule of safeRules) {
|
|
103
|
+
const currentRule = rule && typeof rule === 'object' ? rule : {};
|
|
104
|
+
const ruleIcon = currentRule.status === 'passed' ? icons.success :
|
|
105
|
+
currentRule.status === 'skipped' ? icons.skipped :
|
|
106
|
+
icons.error;
|
|
107
|
+
|
|
108
|
+
const statusColor = currentRule.status === 'passed' ? c.green :
|
|
109
|
+
currentRule.status === 'skipped' ? c.yellow :
|
|
110
|
+
c.red;
|
|
111
|
+
|
|
112
|
+
// Rule header
|
|
113
|
+
const filesChecked = Number.isInteger(currentRule.filesChecked) ? currentRule.filesChecked : 0;
|
|
114
|
+
const ruleName = typeof currentRule.name === 'string' && currentRule.name.trim() !== ''
|
|
115
|
+
? currentRule.name
|
|
116
|
+
: 'unnamed';
|
|
117
|
+
console.log(`${ruleIcon} ${statusColor(ruleName)} ${c.gray(`(${filesChecked} files)`)}`);
|
|
118
|
+
|
|
119
|
+
if (typeof currentRule.description === 'string' && currentRule.description && verbose) {
|
|
120
|
+
console.log(c.gray(` ${currentRule.description}`));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Violations
|
|
124
|
+
const ruleViolations = Array.isArray(currentRule.violations) ? currentRule.violations : [];
|
|
125
|
+
if (ruleViolations.length > 0) {
|
|
126
|
+
// Only count as failure if baseline exceeded or no baseline
|
|
127
|
+
if (!currentRule.baseline || currentRule.baselineExceeded) {
|
|
128
|
+
hasFailures = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Group by file for cleaner output
|
|
132
|
+
const byFile = {};
|
|
133
|
+
for (const v of ruleViolations) {
|
|
134
|
+
const fileKey = typeof v?.file === 'string' && v.file !== '' ? v.file : '<unknown file>';
|
|
135
|
+
if (!byFile[fileKey]) byFile[fileKey] = [];
|
|
136
|
+
byFile[fileKey].push(v || {});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
for (const [file, violations] of Object.entries(byFile)) {
|
|
140
|
+
console.log(c.gray(` ${file}`));
|
|
141
|
+
for (const v of violations) {
|
|
142
|
+
// Validate line number
|
|
143
|
+
let line = '';
|
|
144
|
+
if (typeof v.line === 'number' && v.line >= 0 && v.line < 1000000) {
|
|
145
|
+
line = c.cyan(`:${v.line}`);
|
|
146
|
+
}
|
|
147
|
+
const message = typeof v.message === 'string' ? v.message.slice(0, 200) : 'Unknown error';
|
|
148
|
+
console.log(` ${icons.arrow} ${c.red(message)}${line}`);
|
|
149
|
+
if (typeof v.suggestion === 'string' && v.suggestion) {
|
|
150
|
+
console.log(c.gray(` 💡 ${v.suggestion.slice(0, 200)}`));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Baseline warning (violations within accepted baseline)
|
|
157
|
+
if (currentRule.baselineWarning) {
|
|
158
|
+
console.log(c.yellow(` ${icons.warning} ${currentRule.baselineWarning}`));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Baseline exceeded
|
|
162
|
+
if (currentRule.baselineExceeded) {
|
|
163
|
+
console.log(c.red(` ${icons.error} Baseline exceeded by ${currentRule.baselineExceeded} violation(s)`));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Skipped message
|
|
167
|
+
if (currentRule.status === 'skipped' && currentRule.message) {
|
|
168
|
+
console.log(c.yellow(` ${icons.warning} ${currentRule.message}`));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
console.log();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Summary
|
|
175
|
+
const skipped = Math.max(0, total - passed - failed, computed.skipped);
|
|
176
|
+
|
|
177
|
+
if (hasFailures) {
|
|
178
|
+
console.log(c.red(`${icons.error} ${failed} of ${total} rules failed (${violations} violations)`));
|
|
179
|
+
} else if (passed === total) {
|
|
180
|
+
console.log(c.green(`${icons.success} All ${total} rules passed`));
|
|
181
|
+
} else {
|
|
182
|
+
console.log(c.yellow(`${icons.warning} ${passed} passed, ${skipped} skipped`));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Duration (only in verbose mode)
|
|
186
|
+
if (verbose) {
|
|
187
|
+
console.log(c.gray(`\nDuration: ${(duration / 1000).toFixed(3)}s`));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return hasFailures ? ExitCodes.VALIDATION_FAILED : ExitCodes.SUCCESS;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
formatConsole,
|
|
195
|
+
ExitCodes,
|
|
196
|
+
detectEnvironment,
|
|
197
|
+
getIcons
|
|
198
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const { formatConsole, ExitCodes } = require('./console');
|
|
2
|
+
const { formatJSON } = require('./json');
|
|
3
|
+
const { formatJUnit } = require('./junit');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format results based on specified format
|
|
7
|
+
* @param {Object} results - Validation results
|
|
8
|
+
* @param {string} format - Output format (console, json, junit)
|
|
9
|
+
* @param {Object} options - Output options
|
|
10
|
+
* @param {number} startTime - Start timestamp for duration calculation
|
|
11
|
+
* @returns {number} Exit code
|
|
12
|
+
*/
|
|
13
|
+
function formatResults(results, format, options = {}, startTime = Date.now()) {
|
|
14
|
+
const normalizedFormat = typeof format === 'string' ? format.trim().toLowerCase() : 'console';
|
|
15
|
+
const validFormats = ['json', 'junit', 'console'];
|
|
16
|
+
|
|
17
|
+
if (!validFormats.includes(normalizedFormat)) {
|
|
18
|
+
if (format && typeof format === 'string') {
|
|
19
|
+
console.warn(`Warning: Unknown format "${format}", using console`);
|
|
20
|
+
}
|
|
21
|
+
return formatConsole(results, options, startTime);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
switch (normalizedFormat) {
|
|
25
|
+
case 'json':
|
|
26
|
+
return formatJSON(results, options, startTime);
|
|
27
|
+
case 'junit':
|
|
28
|
+
return formatJUnit(results, options, startTime);
|
|
29
|
+
case 'console':
|
|
30
|
+
default:
|
|
31
|
+
return formatConsole(results, options, startTime);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
formatResults,
|
|
37
|
+
formatConsole,
|
|
38
|
+
formatJSON,
|
|
39
|
+
formatJUnit,
|
|
40
|
+
ExitCodes
|
|
41
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ExitCodes = {
|
|
5
|
+
SUCCESS: 0,
|
|
6
|
+
VALIDATION_FAILED: 1,
|
|
7
|
+
CONFIG_ERROR: 2
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function buildJSONOutput(results, startTime = Date.now()) {
|
|
11
|
+
if (!Number.isFinite(startTime)) {
|
|
12
|
+
startTime = Date.now();
|
|
13
|
+
}
|
|
14
|
+
const duration = Math.max(0, Date.now() - startTime);
|
|
15
|
+
const safeResults = results && typeof results === 'object' ? results : {};
|
|
16
|
+
const summary = safeResults.summary && typeof safeResults.summary === 'object' ? safeResults.summary : {};
|
|
17
|
+
const rules = Array.isArray(safeResults.rules) ? safeResults.rules : [];
|
|
18
|
+
const computed = rules.reduce((acc, item) => {
|
|
19
|
+
const rule = item && typeof item === 'object' ? item : {};
|
|
20
|
+
if (rule.status === 'failed') {
|
|
21
|
+
acc.failed++;
|
|
22
|
+
} else if (rule.status === 'skipped') {
|
|
23
|
+
acc.skipped++;
|
|
24
|
+
} else {
|
|
25
|
+
acc.passed++;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(rule.violations)) {
|
|
28
|
+
acc.violations += rule.violations.length;
|
|
29
|
+
}
|
|
30
|
+
return acc;
|
|
31
|
+
}, { total: rules.length, passed: 0, failed: 0, skipped: 0, violations: 0 });
|
|
32
|
+
const total = Number.isInteger(summary.total) && summary.total >= 0
|
|
33
|
+
? Math.max(summary.total, computed.total)
|
|
34
|
+
: computed.total;
|
|
35
|
+
const passed = Number.isInteger(summary.passed) && summary.passed >= 0
|
|
36
|
+
? Math.max(summary.passed, computed.passed)
|
|
37
|
+
: computed.passed;
|
|
38
|
+
const failed = Number.isInteger(summary.failed) && summary.failed >= 0
|
|
39
|
+
? Math.max(summary.failed, computed.failed)
|
|
40
|
+
: computed.failed;
|
|
41
|
+
const violations = Number.isInteger(summary.violations) && summary.violations >= 0
|
|
42
|
+
? Math.max(summary.violations, computed.violations)
|
|
43
|
+
: computed.violations;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
version: '1.0.0',
|
|
47
|
+
schema: 'https://diagram-cli.dev/schemas/output-v1.json',
|
|
48
|
+
summary: {
|
|
49
|
+
total,
|
|
50
|
+
passed,
|
|
51
|
+
failed,
|
|
52
|
+
skipped: Math.max(0, total - passed - failed, computed.skipped),
|
|
53
|
+
violations,
|
|
54
|
+
duration: duration / 1000, // Convert to seconds
|
|
55
|
+
exitCode: failed > 0 ? ExitCodes.VALIDATION_FAILED : ExitCodes.SUCCESS
|
|
56
|
+
},
|
|
57
|
+
rules: rules.map(rule => {
|
|
58
|
+
const safeRule = rule && typeof rule === 'object' ? rule : {};
|
|
59
|
+
return {
|
|
60
|
+
name: typeof safeRule.name === 'string' ? safeRule.name : 'unnamed',
|
|
61
|
+
description: typeof safeRule.description === 'string' ? safeRule.description : '',
|
|
62
|
+
status: typeof safeRule.status === 'string' ? safeRule.status : 'failed',
|
|
63
|
+
filesChecked: Number.isInteger(safeRule.filesChecked) ? safeRule.filesChecked : 0,
|
|
64
|
+
message: typeof safeRule.message === 'string' ? safeRule.message : undefined,
|
|
65
|
+
violations: (Array.isArray(safeRule.violations) ? safeRule.violations : []).map(v => ({
|
|
66
|
+
file: typeof v?.file === 'string' ? v.file : undefined,
|
|
67
|
+
line: Number.isInteger(v?.line) ? v.line : undefined,
|
|
68
|
+
column: Number.isInteger(v?.column) ? v.column : undefined,
|
|
69
|
+
severity: typeof v?.severity === 'string' ? v.severity : 'error',
|
|
70
|
+
message: typeof v?.message === 'string' ? v.message : 'Unknown error',
|
|
71
|
+
suggestion: typeof v?.suggestion === 'string' ? v.suggestion : undefined,
|
|
72
|
+
relatedFile: typeof v?.relatedFile === 'string' ? v.relatedFile : undefined,
|
|
73
|
+
source: typeof v?.source === 'string' ? v.source : undefined
|
|
74
|
+
}))
|
|
75
|
+
};
|
|
76
|
+
})
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Format results as JSON
|
|
82
|
+
* @param {Object} results - Validation results
|
|
83
|
+
* @param {Object} options - Output options
|
|
84
|
+
* @param {number} startTime - Start timestamp for duration calculation
|
|
85
|
+
* @returns {number} Exit code
|
|
86
|
+
*/
|
|
87
|
+
function formatJSON(results, options = {}, startTime = Date.now()) {
|
|
88
|
+
const output = buildJSONOutput(results, startTime);
|
|
89
|
+
|
|
90
|
+
const json = JSON.stringify(output, null, 2);
|
|
91
|
+
|
|
92
|
+
if (options.output) {
|
|
93
|
+
try {
|
|
94
|
+
const outputDir = path.dirname(options.output);
|
|
95
|
+
if (!fs.existsSync(outputDir)) {
|
|
96
|
+
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
97
|
+
}
|
|
98
|
+
fs.writeFileSync(options.output, json, { mode: 0o644 });
|
|
99
|
+
if (options.verbose) {
|
|
100
|
+
console.log(`Results written to ${options.output}`);
|
|
101
|
+
}
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error(`Failed to write JSON output: ${error.message}`);
|
|
104
|
+
return ExitCodes.CONFIG_ERROR;
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
console.log(json);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return output.summary.exitCode;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { buildJSONOutput, formatJSON, ExitCodes };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const ExitCodes = {
|
|
5
|
+
SUCCESS: 0,
|
|
6
|
+
VALIDATION_FAILED: 1,
|
|
7
|
+
CONFIG_ERROR: 2
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Format results as JUnit XML
|
|
12
|
+
* @param {Object} results - Validation results
|
|
13
|
+
* @param {Object} options - Output options
|
|
14
|
+
* @param {number} startTime - Start timestamp for duration calculation
|
|
15
|
+
* @returns {number} Exit code
|
|
16
|
+
*/
|
|
17
|
+
function formatJUnit(results, options = {}, startTime = Date.now()) {
|
|
18
|
+
if (!Number.isFinite(startTime)) {
|
|
19
|
+
startTime = Date.now();
|
|
20
|
+
}
|
|
21
|
+
const duration = Math.max(0, (Date.now() - startTime) / 1000); // Convert to seconds
|
|
22
|
+
const safeResults = results && typeof results === 'object' ? results : {};
|
|
23
|
+
const summary = safeResults.summary && typeof safeResults.summary === 'object' ? safeResults.summary : {};
|
|
24
|
+
const rules = Array.isArray(safeResults.rules) ? safeResults.rules : [];
|
|
25
|
+
const computed = rules.reduce((acc, item) => {
|
|
26
|
+
const rule = item && typeof item === 'object' ? item : {};
|
|
27
|
+
if (rule.status === 'failed') {
|
|
28
|
+
acc.failed++;
|
|
29
|
+
} else if (rule.status === 'skipped') {
|
|
30
|
+
acc.skipped++;
|
|
31
|
+
} else {
|
|
32
|
+
acc.passed++;
|
|
33
|
+
}
|
|
34
|
+
return acc;
|
|
35
|
+
}, { total: rules.length, passed: 0, failed: 0, skipped: 0 });
|
|
36
|
+
const totalTests = Number.isInteger(summary.total) && summary.total >= 0
|
|
37
|
+
? Math.max(summary.total, computed.total)
|
|
38
|
+
: computed.total;
|
|
39
|
+
const failures = Number.isInteger(summary.failed) && summary.failed >= 0
|
|
40
|
+
? Math.max(summary.failed, computed.failed)
|
|
41
|
+
: computed.failed;
|
|
42
|
+
const passed = Number.isInteger(summary.passed) && summary.passed >= 0
|
|
43
|
+
? Math.max(summary.passed, computed.passed)
|
|
44
|
+
: computed.passed;
|
|
45
|
+
const skipped = Math.max(0, totalTests - passed - failures, computed.skipped);
|
|
46
|
+
const timestamp = new Date().toISOString();
|
|
47
|
+
|
|
48
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n`;
|
|
49
|
+
xml += `<testsuites name="Architecture" tests="${totalTests}" failures="${failures}" skipped="${skipped}" time="${duration.toFixed(3)}">\n`;
|
|
50
|
+
xml += ` <testsuite name="Architecture Validation" tests="${totalTests}" failures="${failures}" errors="0" skipped="${skipped}" time="${duration.toFixed(3)}" timestamp="${timestamp}">\n`;
|
|
51
|
+
|
|
52
|
+
for (const rule of rules) {
|
|
53
|
+
const safeRule = rule && typeof rule === 'object' ? rule : {};
|
|
54
|
+
const testName = escapeXml(typeof safeRule.name === 'string' ? safeRule.name : 'unnamed');
|
|
55
|
+
const violationsList = Array.isArray(safeRule.violations) ? safeRule.violations : [];
|
|
56
|
+
|
|
57
|
+
if (safeRule.status === 'failed') {
|
|
58
|
+
const violations = violationsList.map(v => {
|
|
59
|
+
let msg = `File: ${v.file || 'unknown'}`;
|
|
60
|
+
if (v.line !== undefined && v.line !== null) msg += `:${v.line}`;
|
|
61
|
+
msg += `\nMessage: ${v.message || 'No message'}`;
|
|
62
|
+
if (v.suggestion) msg += `\nSuggestion: ${v.suggestion}`;
|
|
63
|
+
return msg;
|
|
64
|
+
}).join('\n\n') || 'Rule failed with no violations payload';
|
|
65
|
+
|
|
66
|
+
xml += ` <testcase name="${testName}" time="${duration.toFixed(3)}">\n`;
|
|
67
|
+
xml += ` <failure message="${violationsList.length} violation(s)" type="ArchitectureViolation"><![CDATA[`;
|
|
68
|
+
xml += escapeCdata(violations);
|
|
69
|
+
xml += `]]></failure>\n`;
|
|
70
|
+
xml += ` </testcase>\n`;
|
|
71
|
+
} else if (safeRule.status === 'skipped') {
|
|
72
|
+
const skipMessage = escapeXml(safeRule.message || 'No files matched layer pattern');
|
|
73
|
+
xml += ` <testcase name="${testName}" time="0">\n`;
|
|
74
|
+
xml += ` <skipped message="${skipMessage}"/>\n`;
|
|
75
|
+
xml += ` </testcase>\n`;
|
|
76
|
+
} else {
|
|
77
|
+
xml += ` <testcase name="${testName}" time="0"/>\n`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
xml += ` </testsuite>\n`;
|
|
82
|
+
xml += `</testsuites>\n`;
|
|
83
|
+
|
|
84
|
+
if (options.output) {
|
|
85
|
+
try {
|
|
86
|
+
const outputDir = path.dirname(options.output);
|
|
87
|
+
if (!fs.existsSync(outputDir)) {
|
|
88
|
+
fs.mkdirSync(outputDir, { recursive: true, mode: 0o755 });
|
|
89
|
+
}
|
|
90
|
+
fs.writeFileSync(options.output, xml, { mode: 0o644 });
|
|
91
|
+
if (options.verbose) {
|
|
92
|
+
console.log(`Results written to ${options.output}`);
|
|
93
|
+
}
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error(`Failed to write JUnit output: ${error.message}`);
|
|
96
|
+
return ExitCodes.CONFIG_ERROR;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
console.log(xml);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return failures > 0 ? ExitCodes.VALIDATION_FAILED : ExitCodes.SUCCESS;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Escape XML special characters
|
|
107
|
+
*/
|
|
108
|
+
function escapeXml(str) {
|
|
109
|
+
if (str == null) return '';
|
|
110
|
+
return String(str)
|
|
111
|
+
.replace(/&/g, '&')
|
|
112
|
+
.replace(/</g, '<')
|
|
113
|
+
.replace(/>/g, '>')
|
|
114
|
+
.replace(/"/g, '"')
|
|
115
|
+
.replace(/'/g, ''');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function escapeCdata(str) {
|
|
119
|
+
if (!str) return '';
|
|
120
|
+
return str.replace(/\]\]>/g, ']]]]><![CDATA[>');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { formatJUnit, ExitCodes };
|
package/src/graph.js
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ComponentGraph - Wrapper around analyze results with indexes
|
|
3
|
+
* Provides efficient lookups and reverse dependency tracking
|
|
4
|
+
*/
|
|
5
|
+
class ComponentGraph {
|
|
6
|
+
constructor(analyzeResult) {
|
|
7
|
+
// Validate input
|
|
8
|
+
if (!analyzeResult || typeof analyzeResult !== 'object') {
|
|
9
|
+
throw new TypeError('analyzeResult must be an object');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
this.components = Array.isArray(analyzeResult.components) ? analyzeResult.components : [];
|
|
13
|
+
this.rootPath = analyzeResult.rootPath || '';
|
|
14
|
+
this.languages = analyzeResult.languages || {};
|
|
15
|
+
this.directories = Array.isArray(analyzeResult.directories) ? analyzeResult.directories : [];
|
|
16
|
+
|
|
17
|
+
// Validate and limit component count
|
|
18
|
+
if (this.components.length > 10000) {
|
|
19
|
+
console.warn(`Warning: Limiting to 10000 components (received ${this.components.length})`);
|
|
20
|
+
this.components = this.components.slice(0, 10000);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Build indexes for efficient lookups
|
|
24
|
+
this._buildIndexes();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build lookup indexes
|
|
29
|
+
* @private
|
|
30
|
+
*/
|
|
31
|
+
_buildIndexes() {
|
|
32
|
+
// Component name -> component
|
|
33
|
+
this._componentByName = new Map();
|
|
34
|
+
|
|
35
|
+
// File path -> component
|
|
36
|
+
this._componentByPath = new Map();
|
|
37
|
+
|
|
38
|
+
// Component name -> dependent component names (reverse lookup)
|
|
39
|
+
this._dependents = new Map();
|
|
40
|
+
|
|
41
|
+
// Track duplicates
|
|
42
|
+
const seenNames = new Set();
|
|
43
|
+
|
|
44
|
+
for (const component of this.components) {
|
|
45
|
+
// Index by name
|
|
46
|
+
if (component.name) {
|
|
47
|
+
if (seenNames.has(component.name)) {
|
|
48
|
+
console.warn(`Warning: Duplicate component name "${component.name}"`);
|
|
49
|
+
} else {
|
|
50
|
+
seenNames.add(component.name);
|
|
51
|
+
this._componentByName.set(component.name, component);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Index by file path
|
|
56
|
+
if (component.filePath) {
|
|
57
|
+
this._componentByPath.set(component.filePath, component);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Initialize dependents list
|
|
61
|
+
if (component.name) {
|
|
62
|
+
this._dependents.set(component.name, []);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Build reverse dependency index using Set for O(1) lookup
|
|
67
|
+
for (const component of this.components) {
|
|
68
|
+
if (Array.isArray(component.dependencies)) {
|
|
69
|
+
for (const depName of component.dependencies) {
|
|
70
|
+
if (!this._dependents.has(depName)) {
|
|
71
|
+
this._dependents.set(depName, []);
|
|
72
|
+
}
|
|
73
|
+
const dependents = this._dependents.get(depName);
|
|
74
|
+
if (!dependents.includes(component.name)) {
|
|
75
|
+
dependents.push(component.name);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get component by name
|
|
84
|
+
* @param {string} name - Component name
|
|
85
|
+
* @returns {Object|undefined}
|
|
86
|
+
*/
|
|
87
|
+
getComponent(name) {
|
|
88
|
+
return this._componentByName.get(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get component by file path
|
|
93
|
+
* @param {string} filePath - File path
|
|
94
|
+
* @returns {Object|undefined}
|
|
95
|
+
*/
|
|
96
|
+
getComponentByPath(filePath) {
|
|
97
|
+
return this._componentByPath.get(filePath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get direct dependencies of a component
|
|
102
|
+
* @param {string} componentName - Component name
|
|
103
|
+
* @returns {Array<Object>}
|
|
104
|
+
*/
|
|
105
|
+
getDependencies(componentName) {
|
|
106
|
+
const component = this._componentByName.get(componentName);
|
|
107
|
+
if (!component || !Array.isArray(component.dependencies)) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return component.dependencies
|
|
112
|
+
.map(depName => this._componentByName.get(depName))
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get components that depend on a given component (reverse lookup)
|
|
118
|
+
* @param {string} componentName - Component name
|
|
119
|
+
* @returns {Array<Object>}
|
|
120
|
+
*/
|
|
121
|
+
getDependents(componentName) {
|
|
122
|
+
const dependentNames = this._dependents.get(componentName) || [];
|
|
123
|
+
return dependentNames
|
|
124
|
+
.map(name => this._componentByName.get(name))
|
|
125
|
+
.filter(Boolean);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all files that match a layer pattern
|
|
130
|
+
* @param {Array<Function>} matchers - Compiled picomatch functions
|
|
131
|
+
* @returns {Array<Object>}
|
|
132
|
+
*/
|
|
133
|
+
getFilesInLayer(matchers) {
|
|
134
|
+
if (!Array.isArray(matchers)) {
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return this.components.filter(component => {
|
|
139
|
+
if (!component || typeof component.filePath !== 'string') return false;
|
|
140
|
+
return matchers.some(matcher => {
|
|
141
|
+
try {
|
|
142
|
+
return matcher(component.filePath);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get total number of components
|
|
152
|
+
* @returns {number}
|
|
153
|
+
*/
|
|
154
|
+
get size() {
|
|
155
|
+
return this.components.length;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { ComponentGraph };
|