@brainwav/diagram 1.0.7 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +180 -1760
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
@@ -0,0 +1,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, '&amp;')
112
+ .replace(/</g, '&lt;')
113
+ .replace(/>/g, '&gt;')
114
+ .replace(/"/g, '&quot;')
115
+ .replace(/'/g, '&apos;');
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 };