@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 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
+ }
@@ -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 extractClassUses(html: string): ClassUse[] {
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({ className: cls, selector: `.${cls}` })
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): string[] {
64
- const classes = new Set<string>()
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(styleMatch[1])) !== null) {
73
- classes.add(classMatch[1])
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: "warning",
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: { class: use.className, selector: use.selector },
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 cls of extractCssDefinedClasses(html)) {
125
- if (reported.has(cls)) continue
126
- if (allowedClasses.has(cls)) continue
127
- if (isExemptClass(cls, prefixExemptions)) continue
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(cls)
186
+ reported.add(use.className)
130
187
  first.issues.push({
131
188
  type: "compliance",
132
189
  sub: "novel_css_rule",
133
- severity: "warning",
134
- detail: `<style> defines CSS class \`.${cls}\` 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.`,
135
- data: { class: cls },
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",