@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,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, '<')
|
|
9
|
+
.replace(/>/g, '>')
|
|
10
|
+
.replace(/"/g, '"')
|
|
11
|
+
.replace(/'/g, ''');
|
|
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 };
|