Skip to content

Commit 4080105

Browse files
authored
Merge pull request #151 from bookedsolidtech/feature/validation-summary
feat: add validation summary with severity scoring
2 parents 49ca9eb + ea9caff commit 4080105

File tree

4 files changed

+626
-0
lines changed

4 files changed

+626
-0
lines changed

packages/core/src/handlers/code-validator.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { checkColorContrast, type ColorContrastResult } from './color-contrast-c
4848
import { checkTransitionAnimation, type TransitionCheckResult } from './transition-checker.js';
4949
import { checkShadowDomJs, type ShadowDomJsResult } from './shadow-dom-js-checker.js';
5050
import { parseCem } from './cem.js';
51+
import { summarizeValidation, type ValidationSummary } from './validation-summary.js';
5152

5253
// ─── Types ───────────────────────────────────────────────────────────────────
5354

@@ -63,6 +64,7 @@ export interface ValidateComponentCodeInput {
6364
export interface ValidateComponentCodeResult {
6465
clean: boolean;
6566
totalIssues: number;
67+
summary?: ValidationSummary;
6668
htmlUsage?: HtmlUsageCheckResult;
6769
shadowDom?: ShadowDomCheckResult;
6870
eventUsage?: EventUsageCheckResult;
@@ -338,5 +340,8 @@ export function validateComponentCode(
338340
result.totalIssues = totalIssues;
339341
result.clean = totalIssues === 0;
340342

343+
// Generate prioritized summary with severity scoring
344+
result.summary = summarizeValidation(result);
345+
341346
return result;
342347
}

packages/core/src/handlers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export * from './theme-checker.js';
4444
export * from './theme-detection.js';
4545
export * from './token-fallback-checker.js';
4646
export * from './tokens.js';
47+
export * from './validation-summary.js';
4748
export * from './typegenerate.js';
4849
export * from './typescript.js';
4950
export * from './validate.js';
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/**
2+
* Validation Summary — post-processes validate_component_code results into a
3+
* prioritized, severity-scored summary that agents can act on immediately.
4+
*
5+
* Severity levels:
6+
* - error: Will break at runtime or produce visually broken output
7+
* - warning: Will break in specific contexts (themes, a11y, screen readers)
8+
* - info: Best practice violation, may cause issues over time
9+
*/
10+
11+
import type { ValidateComponentCodeResult } from './code-validator.js';
12+
13+
// ─── Types ───────────────────────────────────────────────────────────────────
14+
15+
export type IssueSeverity = 'error' | 'warning' | 'info';
16+
17+
export interface PrioritizedIssue {
18+
severity: IssueSeverity;
19+
category: string;
20+
message: string;
21+
line?: number;
22+
suggestion?: string;
23+
}
24+
25+
export interface ValidationSummary {
26+
clean: boolean;
27+
totalIssues: number;
28+
errors: number;
29+
warnings: number;
30+
info: number;
31+
topIssues: PrioritizedIssue[];
32+
verdict: string;
33+
}
34+
35+
// ─── Severity Classification ────────────────────────────────────────────────
36+
37+
const SEVERITY_ORDER: Record<IssueSeverity, number> = {
38+
error: 0,
39+
warning: 1,
40+
info: 2,
41+
};
42+
43+
// ─── Main Entry Point ───────────────────────────────────────────────────────
44+
45+
export function summarizeValidation(result: ValidateComponentCodeResult): ValidationSummary {
46+
const issues: PrioritizedIssue[] = [];
47+
48+
// ERROR-level: will break at runtime
49+
if (result.shadowDom) {
50+
for (const issue of result.shadowDom.issues) {
51+
issues.push({
52+
severity: 'error',
53+
category: 'shadowDom',
54+
message: issue.message,
55+
line: issue.line,
56+
suggestion: issue.suggestion,
57+
});
58+
}
59+
}
60+
61+
if (result.imports) {
62+
for (const tag of result.imports.unknownTags) {
63+
issues.push({
64+
severity: 'error',
65+
category: 'imports',
66+
message: `Unknown component tag: <${tag}>`,
67+
});
68+
}
69+
}
70+
71+
if (result.shadowDomJs) {
72+
for (const issue of result.shadowDomJs.issues) {
73+
issues.push({
74+
severity: 'error',
75+
category: 'shadowDomJs',
76+
message: issue.message,
77+
line: issue.line,
78+
suggestion: issue.suggestion,
79+
});
80+
}
81+
}
82+
83+
if (result.eventUsage) {
84+
for (const issue of result.eventUsage.issues) {
85+
issues.push({
86+
severity: 'error',
87+
category: 'eventUsage',
88+
message: issue.message,
89+
line: issue.line,
90+
});
91+
}
92+
}
93+
94+
if (result.methodCalls) {
95+
for (const issue of result.methodCalls.issues) {
96+
issues.push({
97+
severity: 'error',
98+
category: 'methodCalls',
99+
message: issue.message,
100+
line: issue.line,
101+
...(issue.suggestion ? { suggestion: issue.suggestion } : {}),
102+
});
103+
}
104+
}
105+
106+
// WARNING-level: breaks in specific contexts
107+
if (result.a11yUsage) {
108+
for (const issue of result.a11yUsage.issues) {
109+
issues.push({
110+
severity: 'warning',
111+
category: 'a11y',
112+
message: issue.message,
113+
line: issue.line,
114+
});
115+
}
116+
}
117+
118+
if (result.tokenFallbacks) {
119+
for (const issue of result.tokenFallbacks.issues) {
120+
issues.push({
121+
severity: 'warning',
122+
category: 'tokenFallbacks',
123+
message: issue.message,
124+
line: issue.line,
125+
});
126+
}
127+
}
128+
129+
if (result.themeCompat) {
130+
for (const issue of result.themeCompat.issues) {
131+
issues.push({
132+
severity: 'warning',
133+
category: 'themeCompat',
134+
message: issue.message,
135+
line: issue.line,
136+
});
137+
}
138+
}
139+
140+
if (result.colorContrast) {
141+
for (const issue of result.colorContrast.issues) {
142+
issues.push({
143+
severity: 'warning',
144+
category: 'colorContrast',
145+
message: issue.message,
146+
line: issue.line,
147+
});
148+
}
149+
}
150+
151+
if (result.scope) {
152+
for (const issue of result.scope.issues) {
153+
issues.push({
154+
severity: 'warning',
155+
category: 'scope',
156+
message: issue.message,
157+
line: issue.line,
158+
});
159+
}
160+
}
161+
162+
if (result.shorthand) {
163+
for (const issue of result.shorthand.issues) {
164+
issues.push({
165+
severity: 'warning',
166+
category: 'shorthand',
167+
message: issue.message,
168+
line: issue.line,
169+
suggestion: issue.suggestion,
170+
});
171+
}
172+
}
173+
174+
if (result.slotChildren) {
175+
for (const issue of result.slotChildren.issues) {
176+
issues.push({
177+
severity: 'warning',
178+
category: 'slotChildren',
179+
message: issue.message,
180+
line: issue.line,
181+
});
182+
}
183+
}
184+
185+
if (result.attributeConflicts) {
186+
for (const issue of result.attributeConflicts.issues) {
187+
issues.push({
188+
severity: 'warning',
189+
category: 'attributeConflicts',
190+
message: issue.message,
191+
line: issue.line,
192+
});
193+
}
194+
}
195+
196+
// INFO-level: best practice violations
197+
if (result.specificity) {
198+
for (const issue of result.specificity.issues) {
199+
issues.push({
200+
severity: 'info',
201+
category: 'specificity',
202+
message: issue.message,
203+
line: issue.line,
204+
});
205+
}
206+
}
207+
208+
if (result.layout) {
209+
for (const issue of result.layout.issues) {
210+
issues.push({
211+
severity: 'info',
212+
category: 'layout',
213+
message: issue.message,
214+
line: issue.line,
215+
});
216+
}
217+
}
218+
219+
if (result.transitionAnimation) {
220+
for (const issue of result.transitionAnimation.issues) {
221+
issues.push({
222+
severity: 'info',
223+
category: 'transitionAnimation',
224+
message: issue.message,
225+
line: issue.line,
226+
});
227+
}
228+
}
229+
230+
if (result.htmlUsage) {
231+
for (const issue of result.htmlUsage.issues) {
232+
issues.push({
233+
severity: 'info',
234+
category: 'htmlUsage',
235+
message: issue.message,
236+
line: issue.line,
237+
});
238+
}
239+
}
240+
241+
if (result.cssVars) {
242+
for (const issue of result.cssVars.issues) {
243+
issues.push({
244+
severity: 'info',
245+
category: 'cssVars',
246+
message: issue.message,
247+
line: issue.line,
248+
});
249+
}
250+
}
251+
252+
if (result.composition) {
253+
for (const issue of result.composition.issues) {
254+
issues.push({
255+
severity: 'info',
256+
category: 'composition',
257+
message: issue.message,
258+
});
259+
}
260+
}
261+
262+
// Sort: errors first, then warnings, then info
263+
issues.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
264+
265+
const errors = issues.filter((i) => i.severity === 'error').length;
266+
const warnings = issues.filter((i) => i.severity === 'warning').length;
267+
const info = issues.filter((i) => i.severity === 'info').length;
268+
269+
const topIssues = issues.slice(0, 10);
270+
271+
const verdict = buildVerdict(errors, warnings, info);
272+
273+
return {
274+
clean: issues.length === 0,
275+
totalIssues: issues.length,
276+
errors,
277+
warnings,
278+
info,
279+
topIssues,
280+
verdict,
281+
};
282+
}
283+
284+
// ─── Verdict Builder ────────────────────────────────────────────────────────
285+
286+
function buildVerdict(errors: number, warnings: number, info: number): string {
287+
if (errors === 0 && warnings === 0 && info === 0) {
288+
return 'Clean — no issues detected.';
289+
}
290+
291+
const parts: string[] = [];
292+
293+
if (errors > 0) {
294+
parts.push(
295+
`${errors} error${errors > 1 ? 's' : ''} (will break at runtime — fix before shipping)`,
296+
);
297+
}
298+
if (warnings > 0) {
299+
parts.push(
300+
`${warnings} warning${warnings > 1 ? 's' : ''} (may break in dark mode, a11y, or theme changes)`,
301+
);
302+
}
303+
if (info > 0) {
304+
parts.push(`${info} info (best practice suggestions)`);
305+
}
306+
307+
return parts.join(', ') + '.';
308+
}

0 commit comments

Comments
 (0)