@brainwav/diagram 1.0.8 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.diagram/contracts/machine-command-coverage.json +73 -0
  2. package/.diagram/migration/finalization-policy.json +20 -0
  3. package/LICENSE +202 -21
  4. package/README.md +132 -339
  5. package/package.json +46 -13
  6. package/scripts/refresh-diagram-context.sh +274 -182
  7. package/src/analyzers/default-analyzer.js +11 -0
  8. package/src/analyzers/index.js +34 -0
  9. package/src/artifacts/agent-context.js +105 -0
  10. package/src/artifacts/artifact-budget.js +224 -0
  11. package/src/artifacts/brief.js +153 -0
  12. package/src/artifacts/evidence-manifest.js +206 -0
  13. package/src/artifacts/evidence-summary.js +29 -0
  14. package/src/commands/analyze.js +125 -0
  15. package/src/commands/changed.js +185 -0
  16. package/src/commands/context.js +110 -0
  17. package/src/commands/diff.js +142 -0
  18. package/src/commands/doctor.js +335 -0
  19. package/src/commands/explain.js +273 -0
  20. package/src/commands/generate-all.js +170 -0
  21. package/src/commands/generate-animated.js +50 -0
  22. package/src/commands/generate-video.js +65 -0
  23. package/src/commands/generate.js +522 -0
  24. package/src/commands/init.js +123 -0
  25. package/src/commands/output.js +76 -0
  26. package/src/commands/scan.js +624 -0
  27. package/src/commands/shared.js +396 -0
  28. package/src/commands/validate.js +328 -0
  29. package/src/commands/video-shared.js +105 -0
  30. package/src/commands/workflow-pr.js +26 -0
  31. package/src/confidence/pipeline.js +186 -0
  32. package/src/config/diagramrc.js +79 -0
  33. package/src/context/build-context-pack.js +291 -0
  34. package/src/context/normalize-diagram-manifest.js +282 -0
  35. package/src/core/analysis-generation-analyze-components.js +102 -0
  36. package/src/core/analysis-generation-analyze-dependencies.js +33 -0
  37. package/src/core/analysis-generation-analyze-files.js +48 -0
  38. package/src/core/analysis-generation-analyze-options.js +73 -0
  39. package/src/core/analysis-generation-analyze.js +63 -0
  40. package/src/core/analysis-generation-constants.js +53 -0
  41. package/src/core/analysis-generation-diagrams-core-architecture.js +105 -0
  42. package/src/core/analysis-generation-diagrams-core-dependency.js +68 -0
  43. package/src/core/analysis-generation-diagrams-core-sequence.js +142 -0
  44. package/src/core/analysis-generation-diagrams-core-shapes.js +104 -0
  45. package/src/core/analysis-generation-diagrams-core.js +12 -0
  46. package/src/core/analysis-generation-diagrams-empty.js +68 -0
  47. package/src/core/analysis-generation-diagrams-erd.js +59 -0
  48. package/src/core/analysis-generation-diagrams-limit.js +27 -0
  49. package/src/core/analysis-generation-diagrams-role-ai-agent.js +103 -0
  50. package/src/core/analysis-generation-diagrams-role-ai-context.js +186 -0
  51. package/src/core/analysis-generation-diagrams-role-ai.js +11 -0
  52. package/src/core/analysis-generation-diagrams-role-data.js +182 -0
  53. package/src/core/analysis-generation-diagrams-role-helpers.js +129 -0
  54. package/src/core/analysis-generation-diagrams-role-security.js +129 -0
  55. package/src/core/analysis-generation-diagrams-role.js +25 -0
  56. package/src/core/analysis-generation-diagrams.js +182 -0
  57. package/src/core/analysis-generation-role-tags-constants.js +55 -0
  58. package/src/core/analysis-generation-role-tags-imports.js +32 -0
  59. package/src/core/analysis-generation-role-tags-infer.js +49 -0
  60. package/src/core/analysis-generation-role-tags-match.js +19 -0
  61. package/src/core/analysis-generation-role-tags.js +7 -0
  62. package/src/core/analysis-generation-utils-core.js +308 -0
  63. package/src/core/analysis-generation-utils-graph.js +321 -0
  64. package/src/core/analysis-generation-utils-resolution.js +76 -0
  65. package/src/core/analysis-generation-utils.js +9 -0
  66. package/src/core/analysis-generation.js +44 -0
  67. package/src/diagram.js +178 -1761
  68. package/src/formatters/console.js +198 -0
  69. package/src/formatters/index.js +41 -0
  70. package/src/formatters/json.js +113 -0
  71. package/src/formatters/junit.js +123 -0
  72. package/src/graph.js +159 -0
  73. package/src/incremental/cache.js +210 -0
  74. package/src/ir/architecture-ir.js +48 -0
  75. package/src/migration/evidence.js +262 -0
  76. package/src/migration/finalization-policy.js +35 -0
  77. package/src/renderers/report-html.js +265 -0
  78. package/src/rules/factory.js +108 -0
  79. package/src/rules/types/base.js +54 -0
  80. package/src/rules/types/import-rule.js +286 -0
  81. package/src/rules.js +380 -0
  82. package/src/schema/erd-confidence.js +56 -0
  83. package/src/schema/erd-extractor.js +504 -0
  84. package/src/schema/erd-model.js +176 -0
  85. package/src/schema/rules-schema.js +170 -0
  86. package/src/utils/suggestions.js +67 -0
  87. package/src/video.js +4 -5
  88. package/src/workflow/git-helpers.js +576 -0
  89. package/src/workflow/pr-command.js +694 -0
  90. package/src/workflow/pr-impact.js +848 -0
  91. package/src/workflow/sort-utils.js +16 -0
@@ -0,0 +1,265 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { summarizeAnalysis } = require('../artifacts/evidence-summary');
4
+
5
+ function escapeHtml(value) {
6
+ return String(value ?? '')
7
+ .replace(/&/g, '&')
8
+ .replace(/</g, '&lt;')
9
+ .replace(/>/g, '&gt;')
10
+ .replace(/"/g, '&quot;')
11
+ .replace(/'/g, '&#39;');
12
+ }
13
+
14
+ function attr(value) {
15
+ return escapeHtml(value);
16
+ }
17
+
18
+ function riskLevel(prImpact) {
19
+ return prImpact?.risk?.level || 'unknown';
20
+ }
21
+
22
+ function statusLabel(status) {
23
+ return String(status || 'unknown').replace(/_/g, ' ');
24
+ }
25
+
26
+ function hrefForArtifact(manifest, artifactPath) {
27
+ const outputDirectory = String(manifest.outputDirectory || '.');
28
+ const normalized = String(artifactPath || '');
29
+ if (outputDirectory === '.') return normalized;
30
+ const prefix = `${outputDirectory}/`;
31
+ return normalized.startsWith(prefix) ? normalized.slice(prefix.length) : normalized;
32
+ }
33
+
34
+ function componentRows(components) {
35
+ const rows = [...(components || [])]
36
+ .sort((left, right) => String(left.filePath || left.name).localeCompare(String(right.filePath || right.name)))
37
+ .slice(0, 20)
38
+ .map((component) => `
39
+ <tr>
40
+ <td>${escapeHtml(component.name || component.originalName || 'unknown')}</td>
41
+ <td>${escapeHtml(component.type || 'unknown')}</td>
42
+ <td>${escapeHtml(component.language || 'unknown')}</td>
43
+ <td>${escapeHtml(component.filePath || 'unknown')}</td>
44
+ </tr>`);
45
+ if (rows.length === 0) {
46
+ return '<tr><td colspan="4">No components detected.</td></tr>';
47
+ }
48
+ return rows.join('\n');
49
+ }
50
+
51
+ function dependencyRows(components) {
52
+ const rows = [...(components || [])]
53
+ .filter((component) => Array.isArray(component.dependencies) && component.dependencies.length > 0)
54
+ .sort((left, right) => String(left.name).localeCompare(String(right.name)))
55
+ .slice(0, 20)
56
+ .map((component) => `
57
+ <tr>
58
+ <td>${escapeHtml(component.name || 'unknown')}</td>
59
+ <td>${escapeHtml(component.dependencies.join(', '))}</td>
60
+ </tr>`);
61
+ if (rows.length === 0) {
62
+ return '<tr><td colspan="2">No internal dependency links detected.</td></tr>';
63
+ }
64
+ return rows.join('\n');
65
+ }
66
+
67
+ function artifactRows(manifest) {
68
+ return manifest.artifacts.map((entry) => `
69
+ <tr>
70
+ <td>${entry.status === 'written'
71
+ ? `<a href="${attr(hrefForArtifact(manifest, entry.path))}">${escapeHtml(entry.path)}</a>`
72
+ : escapeHtml(entry.path)}</td>
73
+ <td><span class="status status-${attr(entry.status)}">${escapeHtml(statusLabel(entry.status))}</span></td>
74
+ <td>${escapeHtml(entry.role)}</td>
75
+ <td>${escapeHtml(entry.reason || entry.errorCategory || 'ready')}</td>
76
+ </tr>`).join('\n');
77
+ }
78
+
79
+ function readOrderItems(manifest) {
80
+ return manifest.artifactReadOrder
81
+ .map((entry) => `<li><a href="${attr(hrefForArtifact(manifest, entry))}">${escapeHtml(entry)}</a></li>`)
82
+ .join('\n');
83
+ }
84
+
85
+ function reviewerChecks(prImpact) {
86
+ const checks = prImpact?.agentSummary?.suggestedReviewerChecks || [];
87
+ if (checks.length === 0) {
88
+ return '<li>Run PR scan mode with <code>--base</code> and <code>--head</code> to populate reviewer checks.</li>';
89
+ }
90
+ return checks.map((check) => `<li>${escapeHtml(check)}</li>`).join('\n');
91
+ }
92
+
93
+ function riskReasons(prImpact) {
94
+ const reasons = prImpact?.agentSummary?.riskReasons || [];
95
+ if (reasons.length === 0) {
96
+ return '<li>No PR risk reasons recorded.</li>';
97
+ }
98
+ return reasons.map((reason) => `<li>${escapeHtml(reason)}</li>`).join('\n');
99
+ }
100
+
101
+ function buildArchitectureReportHtml({
102
+ manifest,
103
+ analysis,
104
+ prImpact = null,
105
+ warnings = [],
106
+ errors = [],
107
+ }) {
108
+ const summary = summarizeAnalysis(analysis);
109
+ const mode = prImpact ? 'PR scan' : 'Repository scan';
110
+ const risk = riskLevel(prImpact);
111
+ const artifactStatus = manifest.artifacts.some((entry) => ['failed', 'partial'].includes(entry.status))
112
+ ? 'partial'
113
+ : 'complete';
114
+ const architecturePath = manifest.artifacts.find((entry) => entry.id === 'architecture')?.path || 'architecture.mmd';
115
+ const prImpactArtifact = manifest.artifacts.find((entry) => entry.id === 'pr-impact');
116
+ const prImpactPath = prImpactArtifact?.path || 'pr-impact/pr-impact.json';
117
+ const prImpactLink = prImpactArtifact?.status === 'written'
118
+ ? `<p>PR impact: <a href="${attr(hrefForArtifact(manifest, prImpactPath))}">${escapeHtml(prImpactPath)}</a></p>`
119
+ : '';
120
+ const warningItems = warnings.length === 0
121
+ ? '<li>No warnings recorded.</li>'
122
+ : warnings.map((warning) => `<li>${escapeHtml(warning)}</li>`).join('\n');
123
+ const errorItems = errors.length === 0
124
+ ? '<li>No errors recorded.</li>'
125
+ : errors.map((error) => `<li>${escapeHtml(error.category)}: ${escapeHtml(error.message)}</li>`).join('\n');
126
+
127
+ return `<!doctype html>
128
+ <html lang="en">
129
+ <head>
130
+ <meta charset="utf-8">
131
+ <meta name="viewport" content="width=device-width, initial-scale=1">
132
+ <title>Archscope Evidence Report</title>
133
+ <style>
134
+ :root { color-scheme: light; --ink: #172026; --muted: #58626b; --line: #d8dee4; --panel: #f6f8fa; --accent: #0b6bcb; --good: #176f3d; --warn: #9a5b00; --bad: #a91d3a; }
135
+ * { box-sizing: border-box; }
136
+ body { margin: 0; color: var(--ink); background: #ffffff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; line-height: 1.5; }
137
+ main { width: min(1120px, calc(100% - 32px)); margin: 0 auto; padding: 28px 0 48px; }
138
+ header { border-bottom: 1px solid var(--line); padding-bottom: 18px; margin-bottom: 24px; }
139
+ h1 { font-size: 2rem; line-height: 1.15; margin: 0 0 12px; letter-spacing: 0; }
140
+ h2 { font-size: 1.25rem; margin: 0 0 12px; letter-spacing: 0; }
141
+ h3 { font-size: 1rem; margin: 0 0 8px; letter-spacing: 0; }
142
+ section { padding: 18px 0; border-bottom: 1px solid var(--line); }
143
+ .summary-grid { display: grid; gap: 12px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
144
+ .metric { border: 1px solid var(--line); border-radius: 8px; padding: 12px; background: var(--panel); min-width: 0; }
145
+ .metric strong { display: block; font-size: 1.35rem; line-height: 1.2; overflow-wrap: anywhere; }
146
+ .metric span, .muted { color: var(--muted); }
147
+ .badge, .status { display: inline-flex; align-items: center; border-radius: 999px; padding: 2px 8px; font-size: 0.85rem; font-weight: 650; border: 1px solid var(--line); }
148
+ .risk-low, .status-written { color: var(--good); border-color: #9bd2b1; background: #effaf3; }
149
+ .risk-medium, .status-partial, .status-deferred { color: var(--warn); border-color: #e6bf78; background: #fff8e8; }
150
+ .risk-high, .status-failed { color: var(--bad); border-color: #ef9fb0; background: #fff0f3; }
151
+ table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
152
+ th, td { border-bottom: 1px solid var(--line); padding: 8px; text-align: left; vertical-align: top; }
153
+ th { background: var(--panel); font-weight: 700; }
154
+ a { color: var(--accent); }
155
+ a:focus-visible { outline: 3px solid #7db7ff; outline-offset: 2px; }
156
+ pre { white-space: pre-wrap; overflow-wrap: anywhere; background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 12px; }
157
+ .columns { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
158
+ @media (max-width: 520px) {
159
+ main { width: min(100% - 20px, 1120px); padding-top: 18px; }
160
+ h1 { font-size: 1.55rem; }
161
+ th, td { padding: 7px 6px; }
162
+ }
163
+ </style>
164
+ </head>
165
+ <body>
166
+ <main>
167
+ <header>
168
+ <p class="muted">Generated by archscope scan</p>
169
+ <h1>Archscope Evidence Report</h1>
170
+ <div class="summary-grid" aria-label="Evidence summary">
171
+ <div class="metric"><strong>${escapeHtml(mode)}</strong><span>Mode</span></div>
172
+ <div class="metric"><strong>${escapeHtml(summary.componentCount)}</strong><span>Components</span></div>
173
+ <div class="metric"><strong>${escapeHtml(summary.totalFilesFound)}</strong><span>Files considered</span></div>
174
+ <div class="metric"><strong><span class="badge risk-${attr(risk)}">${escapeHtml(risk)}</span></strong><span>Risk</span></div>
175
+ </div>
176
+ </header>
177
+
178
+ <section aria-labelledby="evidence-status">
179
+ <h2 id="evidence-status">Evidence Status</h2>
180
+ <p>Status is <strong>${escapeHtml(artifactStatus)}</strong>. Read the manifest first, then consume artifacts in the listed order.</p>
181
+ <ol>
182
+ ${readOrderItems(manifest)}
183
+ </ol>
184
+ </section>
185
+
186
+ <section aria-labelledby="risk-review">
187
+ <h2 id="risk-review">Risk And Review Focus</h2>
188
+ <div class="columns">
189
+ <div>
190
+ <h3>Risk Reasons</h3>
191
+ <ul>${riskReasons(prImpact)}</ul>
192
+ </div>
193
+ <div>
194
+ <h3>Reviewer Checks</h3>
195
+ <ul>${reviewerChecks(prImpact)}</ul>
196
+ </div>
197
+ </div>
198
+ </section>
199
+
200
+ <section aria-labelledby="components">
201
+ <h2 id="components">Architecture Components</h2>
202
+ <table>
203
+ <thead><tr><th>Name</th><th>Type</th><th>Language</th><th>Path</th></tr></thead>
204
+ <tbody>${componentRows(analysis.components)}</tbody>
205
+ </table>
206
+ </section>
207
+
208
+ <section aria-labelledby="dependencies">
209
+ <h2 id="dependencies">Dependency Neighborhood</h2>
210
+ <table>
211
+ <thead><tr><th>Component</th><th>Internal dependencies</th></tr></thead>
212
+ <tbody>${dependencyRows(analysis.components)}</tbody>
213
+ </table>
214
+ </section>
215
+
216
+ <section aria-labelledby="diagrams">
217
+ <h2 id="diagrams">Diagrams</h2>
218
+ <p><a href="${attr(hrefForArtifact(manifest, architecturePath))}">${escapeHtml(architecturePath)}</a></p>
219
+ <pre aria-label="Architecture diagram summary">Architecture diagram is stored as Mermaid source. Open the linked artifact to inspect or render it.</pre>
220
+ </section>
221
+
222
+ <section aria-labelledby="validation">
223
+ <h2 id="validation">Validation And Evidence</h2>
224
+ <p>Validation status: <strong>${escapeHtml(manifest.validation.status)}</strong>. ${escapeHtml(manifest.validation.summary)}</p>
225
+ <div class="columns">
226
+ <div><h3>Warnings</h3><ul>${warningItems}</ul></div>
227
+ <div><h3>Errors</h3><ul>${errorItems}</ul></div>
228
+ </div>
229
+ </section>
230
+
231
+ <section aria-labelledby="agent-handoff">
232
+ <h2 id="agent-handoff">Agent Handoff</h2>
233
+ <p>Agents should read <a href="${attr(hrefForArtifact(manifest, manifest.artifactReadOrder[0]))}">${escapeHtml(manifest.artifactReadOrder[0])}</a> before optional artifacts, then use <a href="${attr(hrefForArtifact(manifest, manifest.primaryAgentArtifact))}">${escapeHtml(manifest.primaryAgentArtifact)}</a> as the parser-safe context.</p>
234
+ </section>
235
+
236
+ <section aria-labelledby="raw-artifacts">
237
+ <h2 id="raw-artifacts">Raw Artifacts</h2>
238
+ <table>
239
+ <thead><tr><th>Artifact</th><th>Status</th><th>Role</th><th>Note</th></tr></thead>
240
+ <tbody>${artifactRows(manifest)}</tbody>
241
+ </table>
242
+ ${prImpactLink}
243
+ </section>
244
+ </main>
245
+ </body>
246
+ </html>
247
+ `;
248
+ }
249
+
250
+ function writeArchitectureReport(filePath, input) {
251
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
252
+ const content = buildArchitectureReportHtml(input);
253
+ fs.writeFileSync(filePath, content);
254
+ return {
255
+ path: filePath,
256
+ bytes: Buffer.byteLength(content, 'utf8'),
257
+ };
258
+ }
259
+
260
+ module.exports = {
261
+ buildArchitectureReportHtml,
262
+ escapeHtml,
263
+ hrefForArtifact,
264
+ writeArchitectureReport,
265
+ };
@@ -0,0 +1,108 @@
1
+ const { ImportRule } = require('./types/import-rule');
2
+
3
+ // Security limits for inward_only rules
4
+ const MAX_PATTERN_LENGTH = 200;
5
+ const MAX_BRACE_DEPTH = 3;
6
+
7
+ /**
8
+ * Validate pattern complexity for ReDoS protection
9
+ * @param {string} pattern - Layer pattern to validate
10
+ * @param {string} ruleName - Rule name for error messages
11
+ */
12
+ function validatePatternComplexity(pattern, ruleName) {
13
+ if (typeof pattern !== 'string') return;
14
+
15
+ if (pattern.length > MAX_PATTERN_LENGTH) {
16
+ throw new Error(
17
+ `Rule "${ruleName}": Layer pattern too long (${pattern.length} chars). Maximum is ${MAX_PATTERN_LENGTH}.`
18
+ );
19
+ }
20
+
21
+ const braceDepth = (pattern.match(/\{/g) || []).length;
22
+ if (braceDepth > MAX_BRACE_DEPTH) {
23
+ throw new Error(
24
+ `Rule "${ruleName}": Layer pattern has too many braces (${braceDepth}). Maximum is ${MAX_BRACE_DEPTH}.`
25
+ );
26
+ }
27
+ }
28
+
29
+ /**
30
+ * RuleFactory - Creates rule instances from configuration
31
+ */
32
+ class RuleFactory {
33
+ /**
34
+ * Create rule instances from config
35
+ * @param {Object} config - Configuration object with rules array
36
+ * @returns {Array<Rule>}
37
+ */
38
+ static createRules(config) {
39
+ if (!config || typeof config !== 'object') {
40
+ throw new TypeError('Config must be an object');
41
+ }
42
+
43
+ if (!Array.isArray(config.rules)) {
44
+ throw new TypeError('Config must have a "rules" array');
45
+ }
46
+
47
+ // Limit number of rules
48
+ if (config.rules.length > 10000) {
49
+ throw new Error('Too many rules (maximum 10000)');
50
+ }
51
+
52
+ return config.rules.map((ruleConfig, index) => {
53
+ const type = this.detectRuleType(ruleConfig);
54
+
55
+ // Validate pattern complexity for inward_only rules
56
+ if (ruleConfig.inward_only === true && ruleConfig.layer) {
57
+ const layers = Array.isArray(ruleConfig.layer) ? ruleConfig.layer : [ruleConfig.layer];
58
+ for (const layer of layers) {
59
+ validatePatternComplexity(layer, ruleConfig.name || `rule ${index}`);
60
+ }
61
+ }
62
+
63
+ switch (type) {
64
+ case 'import':
65
+ return new ImportRule(ruleConfig);
66
+ default:
67
+ throw new Error(`Unknown rule type for "${ruleConfig?.name || `rule ${index}`}": ${type}`);
68
+ }
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Detect rule type from configuration
74
+ * @param {Object} config - Rule configuration
75
+ * @returns {string} Rule type identifier
76
+ */
77
+ static detectRuleType(config) {
78
+ if (!config || typeof config !== 'object') {
79
+ throw new TypeError('Rule config must be an object');
80
+ }
81
+
82
+ // Import rules have import-related constraints (including inward_only)
83
+ if (config.must_not_import_from ||
84
+ config.may_import_from ||
85
+ config.must_import_from ||
86
+ config.inward_only === true) {
87
+ return 'import';
88
+ }
89
+
90
+ throw new Error(`Cannot determine rule type for: ${config?.name || 'unnamed rule'}`);
91
+ }
92
+
93
+ /**
94
+ * Get required data fields for a rule type
95
+ * @param {string} type - Rule type
96
+ * @returns {Array<string>}
97
+ */
98
+ static getRequiredData(type) {
99
+ switch (type) {
100
+ case 'import':
101
+ return ['imports'];
102
+ default:
103
+ return [];
104
+ }
105
+ }
106
+ }
107
+
108
+ module.exports = { RuleFactory };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Base Rule class for architecture testing
3
+ * All rule types extend this class
4
+ */
5
+ class Rule {
6
+ constructor(config) {
7
+ // Deep clone config to prevent external mutation
8
+ this.config = Object.freeze(JSON.parse(JSON.stringify(config || {})));
9
+ }
10
+
11
+ get name() {
12
+ const name = this.config.name;
13
+ return typeof name === 'string' ? name.slice(0, 100) : 'unnamed';
14
+ }
15
+
16
+ get description() {
17
+ const desc = this.config.description;
18
+ return typeof desc === 'string' ? desc.slice(0, 500) : '';
19
+ }
20
+
21
+ get layer() {
22
+ return this.config.layer;
23
+ }
24
+
25
+ /**
26
+ * Validate a file against this rule
27
+ * @param {Object} file - Component file object
28
+ * @param {ComponentGraph} graph - Component graph for lookups
29
+ * @returns {Array<Violation>} Array of violations (empty if passed)
30
+ */
31
+ validate(file, graph) {
32
+ throw new Error(`Rule "${this.name}" must implement validate()`);
33
+ }
34
+
35
+ /**
36
+ * Get required data fields for this rule
37
+ * @returns {Array<string>} Required fields (e.g., ['imports'])
38
+ */
39
+ getRequiredData() {
40
+ return ['imports'];
41
+ }
42
+
43
+ /**
44
+ * Check if this rule applies to a given file path
45
+ * @param {string} filePath - File path to check
46
+ * @param {Array<Function>} matchers - Compiled picomatch functions
47
+ * @returns {boolean}
48
+ */
49
+ matchesLayer(filePath, matchers) {
50
+ return matchers.some(matcher => matcher(filePath));
51
+ }
52
+ }
53
+
54
+ module.exports = { Rule };