@cyber-dash-tech/revela 0.7.5 → 0.7.7
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/lib/qa/checks.ts +62 -0
- package/lib/qa/compliance.ts +81 -18
- package/package.json +1 -1
package/lib/qa/checks.ts
CHANGED
|
@@ -571,10 +571,17 @@ export function runChecks(
|
|
|
571
571
|
* Format a QAReport into a markdown string suitable for the LLM to read.
|
|
572
572
|
*/
|
|
573
573
|
export function formatReport(report: QAReport): string {
|
|
574
|
+
const issues = report.slides.flatMap((slide) => slide.issues)
|
|
575
|
+
const complianceOnly = issues.length > 0 && issues.every((issue) => issue.type === "compliance")
|
|
576
|
+
|
|
574
577
|
if (report.totalIssues === 0) {
|
|
575
578
|
return `## Layout QA: PASSED\n\nAll ${report.slides.length} slide(s) passed layout checks. No issues found.`
|
|
576
579
|
}
|
|
577
580
|
|
|
581
|
+
if (complianceOnly) {
|
|
582
|
+
return formatComplianceReport(report)
|
|
583
|
+
}
|
|
584
|
+
|
|
578
585
|
const lines: string[] = [
|
|
579
586
|
`## Layout QA Report`,
|
|
580
587
|
``,
|
|
@@ -605,3 +612,58 @@ export function formatReport(report: QAReport): string {
|
|
|
605
612
|
|
|
606
613
|
return lines.join("\n")
|
|
607
614
|
}
|
|
615
|
+
|
|
616
|
+
function formatComplianceReport(report: QAReport): string {
|
|
617
|
+
const lines: string[] = [
|
|
618
|
+
`## Static Design Compliance Report`,
|
|
619
|
+
``,
|
|
620
|
+
`**File:** \`${report.file}\``,
|
|
621
|
+
`**Result:** FAILED — ${report.summary}`,
|
|
622
|
+
``,
|
|
623
|
+
]
|
|
624
|
+
|
|
625
|
+
for (const slide of report.slides) {
|
|
626
|
+
if (slide.issues.length === 0) continue
|
|
627
|
+
lines.push(`### Slide ${slide.index + 1}: ${slide.title}`)
|
|
628
|
+
for (const issue of slide.issues) {
|
|
629
|
+
lines.push(formatComplianceIssue(issue))
|
|
630
|
+
}
|
|
631
|
+
lines.push("")
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
lines.push(
|
|
635
|
+
`### Action Required`,
|
|
636
|
+
``,
|
|
637
|
+
`You must fix the design vocabulary errors above before continuing. These are static class-name checks, not layout QA failures.`,
|
|
638
|
+
`Do not leave unknown classes or custom class selectors in deck HTML.`,
|
|
639
|
+
`- For **unknown HTML classes**, remove ad-hoc/test classes or replace them with classes from the active design's Layout Index or Component Index.`,
|
|
640
|
+
`- For **novel CSS rules**, remove custom class selectors from \`<style>\`; use existing design components, or inline \`style=""\` for minor one-off positioning/sizing tweaks.`,
|
|
641
|
+
`- If you need the correct class names, call \`revela-designs\` to read the relevant layout/component details.`,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
return lines.join("\n")
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function formatComplianceIssue(issue: LayoutIssue): string {
|
|
648
|
+
const data = issue.data ?? {}
|
|
649
|
+
const cls = typeof data.class === "string" ? data.class : "unknown"
|
|
650
|
+
const location = typeof data.location === "string" ? data.location : "unknown"
|
|
651
|
+
const line = typeof data.line === "number" ? data.line : undefined
|
|
652
|
+
const excerpt = typeof data.excerpt === "string" ? data.excerpt : ""
|
|
653
|
+
const classAttr = typeof data.classAttr === "string" ? data.classAttr : ""
|
|
654
|
+
const tag = typeof data.tag === "string" ? data.tag : ""
|
|
655
|
+
const label = issue.sub ? `compliance/${issue.sub}` : "compliance"
|
|
656
|
+
const icon = issue.severity === "error" ? "🔴" : "🟡"
|
|
657
|
+
const lines = [`- ${icon} **${label}**: \`${cls}\``]
|
|
658
|
+
|
|
659
|
+
lines.push(` - Location: ${location}${line ? `, line ${line}` : ""}`)
|
|
660
|
+
if (tag || classAttr) {
|
|
661
|
+
lines.push(` - Element: ${tag ? `<${tag}>` : "HTML element"}${classAttr ? ` with class=\"${classAttr}\"` : ""}`)
|
|
662
|
+
}
|
|
663
|
+
if (excerpt) {
|
|
664
|
+
lines.push(` - Source: \`${excerpt}\``)
|
|
665
|
+
}
|
|
666
|
+
lines.push(` - Fix: ${issue.detail}`)
|
|
667
|
+
|
|
668
|
+
return lines.join("\n")
|
|
669
|
+
}
|
package/lib/qa/compliance.ts
CHANGED
|
@@ -5,6 +5,11 @@ import type { LayoutIssue, QAReport, SlideReport } from "./checks"
|
|
|
5
5
|
interface ClassUse {
|
|
6
6
|
className: string
|
|
7
7
|
selector: string
|
|
8
|
+
location: "html_class" | "style_rule"
|
|
9
|
+
line: number
|
|
10
|
+
tagName?: string
|
|
11
|
+
classAttr?: string
|
|
12
|
+
excerpt: string
|
|
8
13
|
}
|
|
9
14
|
|
|
10
15
|
interface SlideClassUses {
|
|
@@ -26,15 +31,44 @@ function extractTitle(html: string, index: number): string {
|
|
|
26
31
|
return title || `Slide ${index + 1}`
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
function
|
|
34
|
+
function lineNumberAt(value: string, offset: number): number {
|
|
35
|
+
let line = 1
|
|
36
|
+
for (let i = 0; i < offset; i++) {
|
|
37
|
+
if (value.charCodeAt(i) === 10) line++
|
|
38
|
+
}
|
|
39
|
+
return line
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeExcerpt(value: string, maxLength = 240): string {
|
|
43
|
+
const normalized = value.replace(/\s+/g, " ").trim()
|
|
44
|
+
return normalized.length > maxLength ? `${normalized.slice(0, maxLength - 1)}…` : normalized
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function extractClassUses(html: string, fullHtml = html, baseOffset = 0): ClassUse[] {
|
|
30
48
|
const uses: ClassUse[] = []
|
|
31
49
|
const classAttrRe = /class\s*=\s*(["'])([\s\S]*?)\1/gi
|
|
32
50
|
let match: RegExpExecArray | null
|
|
33
51
|
|
|
34
52
|
while ((match = classAttrRe.exec(html)) !== null) {
|
|
35
53
|
const raw = match[2] || ""
|
|
54
|
+
const absoluteOffset = baseOffset + match.index
|
|
55
|
+
const tagStart = html.lastIndexOf("<", match.index)
|
|
56
|
+
const tagEnd = html.indexOf(">", match.index)
|
|
57
|
+
const tag = tagStart >= 0 && tagEnd >= 0 ? html.slice(tagStart, tagEnd + 1) : match[0]
|
|
58
|
+
const tagNameMatch = /^<\s*([\w:-]+)/.exec(tag)
|
|
59
|
+
const tagName = tagNameMatch?.[1]
|
|
60
|
+
const excerpt = normalizeExcerpt(tag)
|
|
61
|
+
|
|
36
62
|
for (const cls of raw.split(/\s+/).map((v) => v.trim()).filter(Boolean)) {
|
|
37
|
-
uses.push({
|
|
63
|
+
uses.push({
|
|
64
|
+
className: cls,
|
|
65
|
+
selector: `.${cls}`,
|
|
66
|
+
location: "html_class",
|
|
67
|
+
line: lineNumberAt(fullHtml, absoluteOffset),
|
|
68
|
+
tagName,
|
|
69
|
+
classAttr: raw,
|
|
70
|
+
excerpt,
|
|
71
|
+
})
|
|
38
72
|
}
|
|
39
73
|
}
|
|
40
74
|
|
|
@@ -49,7 +83,7 @@ function extractSlideClassUses(html: string): SlideClassUses[] {
|
|
|
49
83
|
|
|
50
84
|
while ((match = sectionRe.exec(html)) !== null) {
|
|
51
85
|
const chunk = match[0]
|
|
52
|
-
slides.push({ title: extractTitle(chunk, index), uses: extractClassUses(chunk) })
|
|
86
|
+
slides.push({ title: extractTitle(chunk, index), uses: extractClassUses(chunk, html, match.index) })
|
|
53
87
|
index++
|
|
54
88
|
}
|
|
55
89
|
|
|
@@ -60,21 +94,36 @@ function extractSlideClassUses(html: string): SlideClassUses[] {
|
|
|
60
94
|
return slides
|
|
61
95
|
}
|
|
62
96
|
|
|
63
|
-
function extractCssDefinedClasses(html: string):
|
|
64
|
-
const classes = new
|
|
97
|
+
function extractCssDefinedClasses(html: string): ClassUse[] {
|
|
98
|
+
const classes = new Map<string, ClassUse>()
|
|
65
99
|
const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi
|
|
66
100
|
const classRe = /\.([a-zA-Z_][\w-]*)/g
|
|
67
101
|
let styleMatch: RegExpExecArray | null
|
|
68
102
|
|
|
69
103
|
while ((styleMatch = styleRe.exec(html)) !== null) {
|
|
104
|
+
const styleBody = styleMatch[1]
|
|
105
|
+
const bodyOffset = styleMatch.index + styleMatch[0].indexOf(styleBody)
|
|
70
106
|
classRe.lastIndex = 0
|
|
71
107
|
let classMatch: RegExpExecArray | null
|
|
72
|
-
while ((classMatch = classRe.exec(
|
|
73
|
-
|
|
108
|
+
while ((classMatch = classRe.exec(styleBody)) !== null) {
|
|
109
|
+
const cls = classMatch[1]
|
|
110
|
+
if (classes.has(cls)) continue
|
|
111
|
+
|
|
112
|
+
const absoluteOffset = bodyOffset + classMatch.index
|
|
113
|
+
const lineStart = html.lastIndexOf("\n", absoluteOffset) + 1
|
|
114
|
+
const lineEnd = html.indexOf("\n", absoluteOffset)
|
|
115
|
+
const line = lineEnd === -1 ? html.slice(lineStart) : html.slice(lineStart, lineEnd)
|
|
116
|
+
classes.set(cls, {
|
|
117
|
+
className: cls,
|
|
118
|
+
selector: `.${cls}`,
|
|
119
|
+
location: "style_rule",
|
|
120
|
+
line: lineNumberAt(html, absoluteOffset),
|
|
121
|
+
excerpt: normalizeExcerpt(line),
|
|
122
|
+
})
|
|
74
123
|
}
|
|
75
124
|
}
|
|
76
125
|
|
|
77
|
-
return [...classes]
|
|
126
|
+
return [...classes.values()]
|
|
78
127
|
}
|
|
79
128
|
|
|
80
129
|
function summarize(filePath: string, slides: SlideReport[]): QAReport {
|
|
@@ -108,9 +157,17 @@ export function runComplianceQA(htmlFilePath: string, vocabulary?: DesignClassVo
|
|
|
108
157
|
issues.push({
|
|
109
158
|
type: "compliance",
|
|
110
159
|
sub: "unknown_class",
|
|
111
|
-
severity: "
|
|
160
|
+
severity: "error",
|
|
112
161
|
detail: `HTML uses CSS class \`${use.className}\` which is not defined in the active design. Replace it with a class from the Component Index or Layout Index.`,
|
|
113
|
-
data: {
|
|
162
|
+
data: {
|
|
163
|
+
class: use.className,
|
|
164
|
+
selector: use.selector,
|
|
165
|
+
location: use.location,
|
|
166
|
+
line: use.line,
|
|
167
|
+
tag: use.tagName ?? "",
|
|
168
|
+
classAttr: use.classAttr ?? "",
|
|
169
|
+
excerpt: use.excerpt,
|
|
170
|
+
},
|
|
114
171
|
})
|
|
115
172
|
}
|
|
116
173
|
}
|
|
@@ -121,18 +178,24 @@ export function runComplianceQA(htmlFilePath: string, vocabulary?: DesignClassVo
|
|
|
121
178
|
if (allowedClasses && slides.length > 0) {
|
|
122
179
|
const first = slides[0]
|
|
123
180
|
const reported = new Set<string>()
|
|
124
|
-
for (const
|
|
125
|
-
if (reported.has(
|
|
126
|
-
if (allowedClasses.has(
|
|
127
|
-
if (isExemptClass(
|
|
181
|
+
for (const use of extractCssDefinedClasses(html)) {
|
|
182
|
+
if (reported.has(use.className)) continue
|
|
183
|
+
if (allowedClasses.has(use.className)) continue
|
|
184
|
+
if (isExemptClass(use.className, prefixExemptions)) continue
|
|
128
185
|
|
|
129
|
-
reported.add(
|
|
186
|
+
reported.add(use.className)
|
|
130
187
|
first.issues.push({
|
|
131
188
|
type: "compliance",
|
|
132
189
|
sub: "novel_css_rule",
|
|
133
|
-
severity: "
|
|
134
|
-
detail: `<style> defines CSS class \`.${
|
|
135
|
-
data: {
|
|
190
|
+
severity: "error",
|
|
191
|
+
detail: `<style> defines CSS class \`.${use.className}\` which is not part of the active design. Remove this custom rule and use the design's existing component styles. For minor adjustments, use inline \`style=""\` instead.`,
|
|
192
|
+
data: {
|
|
193
|
+
class: use.className,
|
|
194
|
+
selector: use.selector,
|
|
195
|
+
location: use.location,
|
|
196
|
+
line: use.line,
|
|
197
|
+
excerpt: use.excerpt,
|
|
198
|
+
},
|
|
136
199
|
})
|
|
137
200
|
}
|
|
138
201
|
}
|