@cyber-dash-tech/revela 0.7.3 → 0.7.4
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/README.md +6 -11
- package/README.zh-CN.md +6 -11
- package/lib/commands/pdf.ts +4 -1
- package/lib/commands/pptx.ts +4 -1
- package/lib/edit/prompt.ts +1 -1
- package/lib/qa/checks.ts +8 -124
- package/lib/qa/compliance.ts +141 -0
- package/lib/qa/export-gate.ts +11 -0
- package/lib/qa/index.ts +9 -14
- package/lib/qa/measure.ts +2 -2
- package/package.json +1 -1
- package/plugin.ts +61 -39
- package/skill/SKILL.md +5 -5
- package/tools/decks.ts +1 -1
- package/tools/pdf.ts +3 -1
- package/tools/pptx.ts +3 -1
- package/tools/qa.ts +7 -18
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="800" />
|
|
@@ -22,7 +22,7 @@ Enable it for the current session, assign a presentation task, and the agent can
|
|
|
22
22
|
- supports workspace document discovery, transparent text extraction for `.pdf`, `.docx`, `.pptx`, and `.xlsx`, and cached embedded-material extraction for those formats
|
|
23
23
|
- uses workspace `DECKS.json` as machine-readable deck memory, slide spec, and prewrite readiness state
|
|
24
24
|
- blocks premature writes to `decks/*.html` until the active deck is marked structurally ready
|
|
25
|
-
- runs
|
|
25
|
+
- runs fast design compliance checks whenever the agent writes, patches, or edits `decks/*.html`
|
|
26
26
|
- opens a visual comment editor for existing decks so users can Ctrl/Cmd-click elements and send precise edit requests back to OpenCode
|
|
27
27
|
- exports finished decks to PDF and editable PPTX
|
|
28
28
|
- switches designs and domains locally with zero LLM cost
|
|
@@ -302,23 +302,18 @@ This keeps final decks stable, offline-friendly, and independent from expiring r
|
|
|
302
302
|
|
|
303
303
|
## Layout QA And Compliance
|
|
304
304
|
|
|
305
|
-
Every time the agent writes `decks/*.html`, Revela runs
|
|
306
|
-
The
|
|
305
|
+
Every time the agent writes, patches, or edits `decks/*.html`, Revela runs a fast static design compliance check.
|
|
306
|
+
The manual `revela-qa` tool and PDF/PPTX export preflight also run a Puppeteer-based overflow check at `1920x1080`.
|
|
307
307
|
|
|
308
|
-
Current QA
|
|
308
|
+
Current QA checks:
|
|
309
309
|
|
|
310
310
|
| Dimension | What it checks |
|
|
311
311
|
|---|---|
|
|
312
312
|
| `overflow` | Elements extending outside the slide canvas |
|
|
313
|
-
| `balance` | Sparse slides, centroid drift, and bottom-gap issues |
|
|
314
|
-
| `symmetry` | Side-by-side column imbalance in height or density |
|
|
315
|
-
| `rhythm` | Irregular vertical spacing between stacked siblings |
|
|
316
313
|
| `compliance` | Unknown design classes and novel CSS rules outside the active design vocabulary |
|
|
317
314
|
|
|
318
315
|
Each slide must declare `slide-qa="true"` or `slide-qa="false"`.
|
|
319
|
-
|
|
320
|
-
- use `slide-qa="true"` for content-heavy slides that should undergo full QA
|
|
321
|
-
- use `slide-qa="false"` for structural slides such as cover, TOC, quote, summary, or closing pages
|
|
316
|
+
The current QA path keeps this as deck metadata; it does not enable additional subjective balance or spacing checks.
|
|
322
317
|
|
|
323
318
|
You can also run QA manually with the `revela-qa` tool.
|
|
324
319
|
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **中文**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="800" />
|
|
@@ -22,7 +22,7 @@ Revela 是一个 [OpenCode](https://opencode.ai) 插件,可以把你当前使
|
|
|
22
22
|
- 支持工作区文档扫描,以及 `.pdf`、`.docx`、`.pptx`、`.xlsx` 的透明文本提取和嵌入素材缓存提取
|
|
23
23
|
- 使用工作区 `DECKS.json` 保存机器可读的 deck 记忆、逐页规格和写入前 readiness 状态
|
|
24
24
|
- 在 active deck 结构化 ready 前,阻止过早写入 `decks/*.html`
|
|
25
|
-
- agent
|
|
25
|
+
- agent 每次写入、patch 或 edit `decks/*.html` 时自动执行快速 design compliance 检查
|
|
26
26
|
- 为已有 deck 打开可视化评论编辑器,用户可以 Ctrl/Cmd + 点击元素,并把精确修改意见发回 OpenCode
|
|
27
27
|
- 支持导出成 PDF 和可编辑 PPTX
|
|
28
28
|
- design 和 domain 的切换都在本地完成,不消耗 LLM token
|
|
@@ -268,23 +268,18 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
|
|
|
268
268
|
|
|
269
269
|
## 布局 QA 与合规检查
|
|
270
270
|
|
|
271
|
-
每次 agent
|
|
272
|
-
|
|
271
|
+
每次 agent 写入、patch 或 edit `decks/*.html` 时,Revela 都会自动运行快速静态 design compliance 检查。
|
|
272
|
+
手动 `revela-qa` 工具以及 PDF/PPTX 导出前置检查会额外在 `1920x1080` 下运行基于 Puppeteer 的 overflow 检查。
|
|
273
273
|
|
|
274
|
-
当前 QA
|
|
274
|
+
当前 QA 检查:
|
|
275
275
|
|
|
276
276
|
| 维度 | 检查内容 |
|
|
277
277
|
|---|---|
|
|
278
278
|
| `overflow` | 元素是否超出 slide canvas |
|
|
279
|
-
| `balance` | 是否过稀、重心偏移、底部留白过大 |
|
|
280
|
-
| `symmetry` | 并列列之间的高度或密度是否明显失衡 |
|
|
281
|
-
| `rhythm` | 垂直堆叠元素之间的间距节奏是否不稳定 |
|
|
282
279
|
| `compliance` | 是否使用了 active design 之外的 class 或新增 CSS 规则 |
|
|
283
280
|
|
|
284
281
|
每张 slide 都必须声明 `slide-qa="true"` 或 `slide-qa="false"`。
|
|
285
|
-
|
|
286
|
-
- `slide-qa="true"` 适用于内容型页面,执行完整 QA
|
|
287
|
-
- `slide-qa="false"` 适用于封面、目录、引用、总结、结尾等结构型页面
|
|
282
|
+
当前 QA 路径将其保留为 deck metadata,不再启用额外的主观平衡或间距检查。
|
|
288
283
|
|
|
289
284
|
也可以手动调用 `revela-qa` 工具执行 QA。
|
|
290
285
|
|
package/lib/commands/pdf.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { resolve } from "path"
|
|
11
11
|
import { exportToPdf } from "../pdf/export"
|
|
12
|
+
import { assertExportQAPassed } from "../qa/export-gate"
|
|
12
13
|
|
|
13
14
|
export async function handlePdf(
|
|
14
15
|
filePath: string,
|
|
@@ -23,9 +24,11 @@ export async function handlePdf(
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
const abs = resolve(filePath)
|
|
26
|
-
await send(`
|
|
27
|
+
await send(`Running pre-export QA for \`${abs}\`...`)
|
|
27
28
|
|
|
28
29
|
try {
|
|
30
|
+
await assertExportQAPassed(abs)
|
|
31
|
+
await send(`Exporting \`${abs}\` to PDF...`)
|
|
29
32
|
const result = await exportToPdf(filePath)
|
|
30
33
|
const secs = (result.durationMs / 1000).toFixed(1)
|
|
31
34
|
await send(
|
package/lib/commands/pptx.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { resolve } from "path"
|
|
11
11
|
import { exportToPptx } from "../pptx/export"
|
|
12
|
+
import { assertExportQAPassed } from "../qa/export-gate"
|
|
12
13
|
|
|
13
14
|
function formatSecs(ms: number): string {
|
|
14
15
|
return `${(ms / 1000).toFixed(1)}s`
|
|
@@ -27,9 +28,11 @@ export async function handlePptx(
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
const abs = resolve(filePath)
|
|
30
|
-
await send(`
|
|
31
|
+
await send(`Running pre-export QA for \`${abs}\`...`)
|
|
31
32
|
|
|
32
33
|
try {
|
|
34
|
+
await assertExportQAPassed(abs)
|
|
35
|
+
await send(`Exporting \`${abs}\` to PPTX...`)
|
|
33
36
|
let lastSlideUpdate = 0
|
|
34
37
|
let longDeckThreshold: number | null = null
|
|
35
38
|
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -76,6 +76,6 @@ Instructions:
|
|
|
76
76
|
- Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.
|
|
77
77
|
- Before patching or writing ${"`decks/*.html`"}, ensure ${"`DECKS.json`"} contains this deck and call ${"`revela-decks`"} with action ${"`review`"}. If ${"`DECKS.json`"} or the deck entry is missing, initialize/upsert the deck state with ${"`revela-decks`"} first. If readiness remains blocked, explain the blockers instead of forcing the edit.
|
|
78
78
|
- Apply the edit to ${payload.file} only after readiness allows deck HTML changes.
|
|
79
|
-
-
|
|
79
|
+
- Design compliance is checked automatically after deck writes. Run ${"`revela-qa`"} on ${payload.file} only when the edit may affect layout geometry, such as size, spacing, font scale, content amount, or container structure; fix any overflow it reports.
|
|
80
80
|
- If the comment is ambiguous, ask one concise clarification question instead of guessing.`
|
|
81
81
|
}
|
package/lib/qa/checks.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/checks.ts
|
|
3
3
|
*
|
|
4
|
-
* Geometry-based layout quality checks
|
|
5
|
-
*
|
|
4
|
+
* Geometry-based layout quality checks. The active default path only checks
|
|
5
|
+
* overflow; softer visual heuristics are kept here for future opt-in use.
|
|
6
6
|
*
|
|
7
7
|
* Dimension 1: Overflow — elements exceed canvas bounds (correctness)
|
|
8
8
|
* Dimension 2: Balance — content centroid & distribution (fill, sparsity)
|
|
@@ -523,106 +523,13 @@ function checkRhythm(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
523
523
|
return issues
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
// ── Compliance checks ─────────────────────────────────────────────────────────
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Check whether a class name is exempt from compliance checking.
|
|
530
|
-
* Returns true if the class matches any of the given prefix exemptions.
|
|
531
|
-
*/
|
|
532
|
-
function isExemptClass(cls: string, prefixExemptions: string[]): boolean {
|
|
533
|
-
return prefixExemptions.some((prefix) => cls.startsWith(prefix))
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/**
|
|
537
|
-
* Dimension 5a: unknown_class
|
|
538
|
-
*
|
|
539
|
-
* Walk the element tree and flag any CSS class not in `allowedClasses`
|
|
540
|
-
* and not matching any `prefixExemptions`. Each unique unknown class name
|
|
541
|
-
* is reported at most once per slide (de-duplicated).
|
|
542
|
-
*/
|
|
543
|
-
function checkCompliance(
|
|
544
|
-
slide: SlideMetrics,
|
|
545
|
-
allowedClasses: Set<string>,
|
|
546
|
-
prefixExemptions: string[],
|
|
547
|
-
): LayoutIssue[] {
|
|
548
|
-
const issues: LayoutIssue[] = []
|
|
549
|
-
const reported = new Set<string>()
|
|
550
|
-
|
|
551
|
-
function walk(el: ElementInfo): void {
|
|
552
|
-
for (const cls of el.classList) {
|
|
553
|
-
if (!cls) continue
|
|
554
|
-
if (reported.has(cls)) continue
|
|
555
|
-
if (allowedClasses.has(cls)) continue
|
|
556
|
-
if (isExemptClass(cls, prefixExemptions)) continue
|
|
557
|
-
|
|
558
|
-
reported.add(cls)
|
|
559
|
-
issues.push({
|
|
560
|
-
type: "compliance",
|
|
561
|
-
sub: "unknown_class",
|
|
562
|
-
severity: "warning",
|
|
563
|
-
detail: `Element \`${el.selector}\` uses CSS class \`${cls}\` which is not defined in the active design. Replace it with a class from the Component Index or Layout Index.`,
|
|
564
|
-
data: { class: cls, selector: el.selector },
|
|
565
|
-
})
|
|
566
|
-
}
|
|
567
|
-
for (const child of el.children) {
|
|
568
|
-
walk(child)
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
for (const el of slide.elements) {
|
|
573
|
-
walk(el)
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
return issues
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/**
|
|
580
|
-
* Dimension 5b: novel_css_rule
|
|
581
|
-
*
|
|
582
|
-
* Check whether the <style> block defines CSS classes not in `allowedClasses`.
|
|
583
|
-
* Returns issues as a flat list (caller attaches them to slide 0).
|
|
584
|
-
*/
|
|
585
|
-
function checkNovelCssRules(
|
|
586
|
-
cssDefinedClasses: string[],
|
|
587
|
-
allowedClasses: Set<string>,
|
|
588
|
-
prefixExemptions: string[],
|
|
589
|
-
): LayoutIssue[] {
|
|
590
|
-
const issues: LayoutIssue[] = []
|
|
591
|
-
const reported = new Set<string>()
|
|
592
|
-
|
|
593
|
-
for (const cls of cssDefinedClasses) {
|
|
594
|
-
if (!cls) continue
|
|
595
|
-
if (reported.has(cls)) continue
|
|
596
|
-
if (allowedClasses.has(cls)) continue
|
|
597
|
-
if (isExemptClass(cls, prefixExemptions)) continue
|
|
598
|
-
|
|
599
|
-
reported.add(cls)
|
|
600
|
-
issues.push({
|
|
601
|
-
type: "compliance",
|
|
602
|
-
sub: "novel_css_rule",
|
|
603
|
-
severity: "warning",
|
|
604
|
-
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.`,
|
|
605
|
-
data: { class: cls },
|
|
606
|
-
})
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
return issues
|
|
610
|
-
}
|
|
611
|
-
|
|
612
526
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
613
527
|
|
|
614
528
|
/**
|
|
615
|
-
* Options for
|
|
616
|
-
*
|
|
529
|
+
* Options for future geometry checks. The current default path only checks
|
|
530
|
+
* overflow, regardless of options.
|
|
617
531
|
*/
|
|
618
|
-
export interface RunChecksOptions {
|
|
619
|
-
/** Allowed CSS class vocabulary from the active design (enables compliance checks). */
|
|
620
|
-
allowedClasses?: Set<string>
|
|
621
|
-
/** Class name prefixes exempt from compliance checks (e.g. "lucide-", "echarts-"). */
|
|
622
|
-
prefixExemptions?: string[]
|
|
623
|
-
/** CSS class names defined in <style> blocks (enables novel_css_rule check). */
|
|
624
|
-
cssDefinedClasses?: string[]
|
|
625
|
-
}
|
|
532
|
+
export interface RunChecksOptions {}
|
|
626
533
|
|
|
627
534
|
/**
|
|
628
535
|
* Run all dimension checks on a set of slide metrics and produce a QA report.
|
|
@@ -630,31 +537,12 @@ export interface RunChecksOptions {
|
|
|
630
537
|
export function runChecks(
|
|
631
538
|
filePath: string,
|
|
632
539
|
allMetrics: SlideMetrics[],
|
|
633
|
-
|
|
540
|
+
_options?: RunChecksOptions,
|
|
634
541
|
): QAReport {
|
|
635
542
|
const slides: SlideReport[] = []
|
|
636
|
-
const { allowedClasses, prefixExemptions = [], cssDefinedClasses } = options ?? {}
|
|
637
|
-
|
|
638
|
-
// novel_css_rule issues are global (not per-slide); attach to slide 0.
|
|
639
|
-
const novelCssIssues: LayoutIssue[] =
|
|
640
|
-
allowedClasses && cssDefinedClasses
|
|
641
|
-
? checkNovelCssRules(cssDefinedClasses, allowedClasses, prefixExemptions)
|
|
642
|
-
: []
|
|
643
543
|
|
|
644
544
|
for (const metrics of allMetrics) {
|
|
645
|
-
const
|
|
646
|
-
allowedClasses
|
|
647
|
-
? checkCompliance(metrics, allowedClasses, prefixExemptions)
|
|
648
|
-
: []
|
|
649
|
-
|
|
650
|
-
const issues: LayoutIssue[] = [
|
|
651
|
-
...checkOverflow(metrics),
|
|
652
|
-
...checkBalance(metrics),
|
|
653
|
-
...checkRhythm(metrics),
|
|
654
|
-
...complianceIssues,
|
|
655
|
-
// Attach novel_css_rule issues to slide 0 only
|
|
656
|
-
...(metrics.index === 0 ? novelCssIssues : []),
|
|
657
|
-
]
|
|
545
|
+
const issues: LayoutIssue[] = [...checkOverflow(metrics)]
|
|
658
546
|
|
|
659
547
|
slides.push({ index: metrics.index, title: metrics.title, issues })
|
|
660
548
|
}
|
|
@@ -709,12 +597,8 @@ export function formatReport(report: QAReport): string {
|
|
|
709
597
|
lines.push(
|
|
710
598
|
`### Action Required`,
|
|
711
599
|
``,
|
|
712
|
-
`Please fix the above
|
|
600
|
+
`Please fix the above hard-error issues in the HTML file. For each issue type:`,
|
|
713
601
|
`- **overflow**: reduce font size, padding, or content amount for the affected element.`,
|
|
714
|
-
`- **balance/centroid_offset**: redistribute content so the visual weight is centred — avoid concentrating everything in one corner or side.`,
|
|
715
|
-
`- **balance/bottom_gap**: expand content to fill the slide, use \`flex: 1\` on containers, add more content blocks, or reduce top padding.`,
|
|
716
|
-
`- **balance/sparse**: add more content components, increase font sizes, or use a layout with fewer columns.`,
|
|
717
|
-
`- **rhythm/gap_variance**: use consistent \`gap\` or \`margin\` values between stacked elements instead of mixing sizes.`,
|
|
718
602
|
`- **compliance/unknown_class**: an HTML element uses a CSS class not defined in the active design. Replace it with a class from the Component Index or Layout Index. Fetch the component/layout details with the \`revela-designs\` tool if needed.`,
|
|
719
603
|
`- **compliance/novel_css_rule**: \`<style>\` defines a CSS class that is not part of the active design. Remove the custom rule and use the design's existing component styles. For minor spacing/sizing adjustments, use inline \`style=""\` instead.`,
|
|
720
604
|
)
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { readFileSync } from "fs"
|
|
2
|
+
import type { DesignClassVocabulary } from "../design/designs"
|
|
3
|
+
import type { LayoutIssue, QAReport, SlideReport } from "./checks"
|
|
4
|
+
|
|
5
|
+
interface ClassUse {
|
|
6
|
+
className: string
|
|
7
|
+
selector: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SlideClassUses {
|
|
11
|
+
title: string
|
|
12
|
+
uses: ClassUse[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isExemptClass(cls: string, prefixExemptions: string[]): boolean {
|
|
16
|
+
return prefixExemptions.some((prefix) => cls.startsWith(prefix))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stripTags(value: string): string {
|
|
20
|
+
return value.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function extractTitle(html: string, index: number): string {
|
|
24
|
+
const match = /<(?:h1|h2|h3|title)\b[^>]*>([\s\S]*?)<\/(?:h1|h2|h3|title)>/i.exec(html)
|
|
25
|
+
const title = match ? stripTags(match[1]).slice(0, 80) : ""
|
|
26
|
+
return title || `Slide ${index + 1}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractClassUses(html: string): ClassUse[] {
|
|
30
|
+
const uses: ClassUse[] = []
|
|
31
|
+
const classAttrRe = /class\s*=\s*(["'])([\s\S]*?)\1/gi
|
|
32
|
+
let match: RegExpExecArray | null
|
|
33
|
+
|
|
34
|
+
while ((match = classAttrRe.exec(html)) !== null) {
|
|
35
|
+
const raw = match[2] || ""
|
|
36
|
+
for (const cls of raw.split(/\s+/).map((v) => v.trim()).filter(Boolean)) {
|
|
37
|
+
uses.push({ className: cls, selector: `.${cls}` })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return uses
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function extractSlideClassUses(html: string): SlideClassUses[] {
|
|
45
|
+
const slides: SlideClassUses[] = []
|
|
46
|
+
const sectionRe = /<section\b[\s\S]*?<\/section>/gi
|
|
47
|
+
let match: RegExpExecArray | null
|
|
48
|
+
let index = 0
|
|
49
|
+
|
|
50
|
+
while ((match = sectionRe.exec(html)) !== null) {
|
|
51
|
+
const chunk = match[0]
|
|
52
|
+
slides.push({ title: extractTitle(chunk, index), uses: extractClassUses(chunk) })
|
|
53
|
+
index++
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (slides.length === 0) {
|
|
57
|
+
slides.push({ title: extractTitle(html, 0), uses: extractClassUses(html) })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return slides
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractCssDefinedClasses(html: string): string[] {
|
|
64
|
+
const classes = new Set<string>()
|
|
65
|
+
const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi
|
|
66
|
+
const classRe = /\.([a-zA-Z_][\w-]*)/g
|
|
67
|
+
let styleMatch: RegExpExecArray | null
|
|
68
|
+
|
|
69
|
+
while ((styleMatch = styleRe.exec(html)) !== null) {
|
|
70
|
+
classRe.lastIndex = 0
|
|
71
|
+
let classMatch: RegExpExecArray | null
|
|
72
|
+
while ((classMatch = classRe.exec(styleMatch[1])) !== null) {
|
|
73
|
+
classes.add(classMatch[1])
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [...classes]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function summarize(filePath: string, slides: SlideReport[]): QAReport {
|
|
81
|
+
const totalIssues = slides.reduce((sum, slide) => sum + slide.issues.length, 0)
|
|
82
|
+
const errorCount = slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "error").length, 0)
|
|
83
|
+
const warningCount = slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "warning").length, 0)
|
|
84
|
+
const summary = totalIssues === 0
|
|
85
|
+
? "All slides passed layout QA."
|
|
86
|
+
: `Found ${totalIssues} issue(s): ${errorCount} error(s), ${warningCount} warning(s) across ${slides.filter((s) => s.issues.length > 0).length} slide(s).`
|
|
87
|
+
|
|
88
|
+
return { file: filePath, slides, totalIssues, errorCount, warningCount, summary }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function runComplianceQA(htmlFilePath: string, vocabulary?: DesignClassVocabulary): QAReport {
|
|
92
|
+
const html = readFileSync(htmlFilePath, "utf-8")
|
|
93
|
+
const slideUses = extractSlideClassUses(html)
|
|
94
|
+
const allowedClasses = vocabulary?.classes
|
|
95
|
+
const prefixExemptions = vocabulary?.prefixExemptions ?? []
|
|
96
|
+
|
|
97
|
+
const slides: SlideReport[] = slideUses.map((slide, index) => {
|
|
98
|
+
const issues: LayoutIssue[] = []
|
|
99
|
+
const reported = new Set<string>()
|
|
100
|
+
|
|
101
|
+
if (allowedClasses) {
|
|
102
|
+
for (const use of slide.uses) {
|
|
103
|
+
if (reported.has(use.className)) continue
|
|
104
|
+
if (allowedClasses.has(use.className)) continue
|
|
105
|
+
if (isExemptClass(use.className, prefixExemptions)) continue
|
|
106
|
+
|
|
107
|
+
reported.add(use.className)
|
|
108
|
+
issues.push({
|
|
109
|
+
type: "compliance",
|
|
110
|
+
sub: "unknown_class",
|
|
111
|
+
severity: "warning",
|
|
112
|
+
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 },
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { index, title: slide.title, issues }
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
if (allowedClasses && slides.length > 0) {
|
|
122
|
+
const first = slides[0]
|
|
123
|
+
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
|
|
128
|
+
|
|
129
|
+
reported.add(cls)
|
|
130
|
+
first.issues.push({
|
|
131
|
+
type: "compliance",
|
|
132
|
+
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 },
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return summarize(htmlFilePath, slides)
|
|
141
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { formatReport, runQA } from "./index"
|
|
2
|
+
|
|
3
|
+
export async function assertExportQAPassed(filePath: string): Promise<void> {
|
|
4
|
+
const report = await runQA(filePath)
|
|
5
|
+
if (report.totalIssues === 0) return
|
|
6
|
+
|
|
7
|
+
throw new Error(
|
|
8
|
+
"Export blocked because pre-export QA found issues. Fix them and export again.\n\n" +
|
|
9
|
+
formatReport(report)
|
|
10
|
+
)
|
|
11
|
+
}
|
package/lib/qa/index.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/index.ts
|
|
3
3
|
*
|
|
4
|
-
* Public entry point for
|
|
5
|
-
*
|
|
4
|
+
* Public entry point for hard-error slide QA.
|
|
5
|
+
* Runs overflow measurement only; design compliance is an automatic post-write
|
|
6
|
+
* hook concern, not part of manual/export QA.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
import { measureSlides } from "./measure"
|
|
9
10
|
import { runChecks, formatReport } from "./checks"
|
|
10
|
-
import type { QAReport
|
|
11
|
+
import type { QAReport } from "./checks"
|
|
11
12
|
import type { DesignClassVocabulary } from "../design/designs"
|
|
12
13
|
|
|
13
14
|
export type { QAReport, SlideReport, LayoutIssue, IssueSeverity } from "./checks"
|
|
14
15
|
export type { RunChecksOptions } from "./checks"
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Run
|
|
18
|
+
* Run hard-error QA on `htmlFilePath`.
|
|
18
19
|
*
|
|
19
20
|
* 1. Opens the file in headless Chrome (puppeteer-core)
|
|
20
21
|
* 2. Measures each .slide element's geometry + CSS class definitions
|
|
21
|
-
* 3. Runs
|
|
22
|
+
* 3. Runs hard-error overflow checks only
|
|
22
23
|
* 4. Returns a structured QAReport
|
|
23
24
|
*
|
|
24
25
|
* Pass `vocabulary` (from `extractDesignClasses()`) to enable compliance checks.
|
|
@@ -28,17 +29,10 @@ export type { RunChecksOptions } from "./checks"
|
|
|
28
29
|
*/
|
|
29
30
|
export async function runQA(
|
|
30
31
|
htmlFilePath: string,
|
|
31
|
-
|
|
32
|
+
_vocabulary?: DesignClassVocabulary,
|
|
32
33
|
): Promise<QAReport> {
|
|
33
34
|
const result = await measureSlides(htmlFilePath)
|
|
34
|
-
|
|
35
|
-
? {
|
|
36
|
-
allowedClasses: vocabulary.classes,
|
|
37
|
-
prefixExemptions: vocabulary.prefixExemptions,
|
|
38
|
-
cssDefinedClasses: result.cssDefinedClasses,
|
|
39
|
-
}
|
|
40
|
-
: undefined
|
|
41
|
-
return runChecks(htmlFilePath, result.slides, options)
|
|
35
|
+
return runChecks(htmlFilePath, result.slides)
|
|
42
36
|
}
|
|
43
37
|
|
|
44
38
|
/**
|
|
@@ -54,3 +48,4 @@ export async function runQAFormatted(
|
|
|
54
48
|
}
|
|
55
49
|
|
|
56
50
|
export { formatReport } from "./checks"
|
|
51
|
+
export { runComplianceQA } from "./compliance"
|
package/lib/qa/measure.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface SlideMetrics {
|
|
|
57
57
|
/** slide title extracted from the first h1/h2 inside the slide */
|
|
58
58
|
title: string
|
|
59
59
|
/**
|
|
60
|
-
* Whether this slide
|
|
60
|
+
* Whether this slide is marked as QA-relevant deck metadata.
|
|
61
61
|
* Read from the `slide-qa` attribute on `<section class="slide">`.
|
|
62
62
|
* Defaults to `false` when the attribute is absent.
|
|
63
63
|
* Content-heavy layouts set `slide-qa="true"`; structural/sparse slides omit or use `"false"`.
|
|
@@ -266,7 +266,7 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
266
266
|
const slide = document.querySelectorAll(".slide")[slideIdx]
|
|
267
267
|
if (!slide) return null
|
|
268
268
|
|
|
269
|
-
// Read the QA flag
|
|
269
|
+
// Read the QA flag for deck metadata; default checks do not branch on it.
|
|
270
270
|
const slideQa = (slide as HTMLElement).getAttribute("slide-qa") === "true"
|
|
271
271
|
|
|
272
272
|
const canvas = slide.querySelector(".slide-canvas") as HTMLElement | null
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* 5. experimental.chat.system.transform: inject three-layer prompt when enabled
|
|
12
12
|
* 6. chat.message: intercept @-referenced / pasted binary files → extract text → replace FilePart with TextPart
|
|
13
13
|
* 7. tool.execute.before: intercept read on DOCX/PPTX/XLSX → preRead()
|
|
14
|
-
* 8. tool.execute.after: intercept read on PDF/images → postRead()
|
|
14
|
+
* 8. tool.execute.after: intercept read on PDF/images → postRead(); run fast design compliance after deck writes/patches/edits
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { Plugin } from "@opencode-ai/plugin"
|
|
@@ -85,7 +85,7 @@ import pdfTool from "./tools/pdf"
|
|
|
85
85
|
import pptxTool from "./tools/pptx"
|
|
86
86
|
import createEditTool from "./tools/edit"
|
|
87
87
|
import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
|
|
88
|
-
import {
|
|
88
|
+
import { formatReport, runComplianceQA } from "./lib/qa"
|
|
89
89
|
import { extractDesignClasses } from "./lib/design/designs"
|
|
90
90
|
import { log, childLog } from "./lib/log"
|
|
91
91
|
|
|
@@ -97,6 +97,15 @@ const INTERNAL_AGENT_SIGNATURES = [
|
|
|
97
97
|
"Summarize what was done in this conversation",
|
|
98
98
|
]
|
|
99
99
|
|
|
100
|
+
function appendToolResult(output: any, text: string): void {
|
|
101
|
+
const existing = output.result ?? ""
|
|
102
|
+
output.result = (existing ? existing + "\n\n" : "") + text
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function extractEditFilePath(args: any): string {
|
|
106
|
+
return args?.filePath ?? args?.file_path ?? args?.path ?? args?.file ?? ""
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
101
110
|
|
|
102
111
|
/**
|
|
@@ -129,6 +138,33 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
129
138
|
const blockedDeckWrites = new Map<string, string>()
|
|
130
139
|
const blockedDeckPatches = new Map<string, string>()
|
|
131
140
|
|
|
141
|
+
async function appendComplianceReport(filePath: string, output: any): Promise<void> {
|
|
142
|
+
if (!isDeckHtmlPath(filePath)) return
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
let vocabulary
|
|
146
|
+
try {
|
|
147
|
+
vocabulary = extractDesignClasses()
|
|
148
|
+
} catch {
|
|
149
|
+
// Design may not be installed or may have no markers — skip compliance.
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const report = runComplianceQA(filePath, vocabulary)
|
|
153
|
+
if (report.totalIssues === 0) return
|
|
154
|
+
|
|
155
|
+
appendToolResult(
|
|
156
|
+
output,
|
|
157
|
+
"---\n\n**[revela design compliance]** Auto-check completed:\n\n" +
|
|
158
|
+
formatReport(report)
|
|
159
|
+
)
|
|
160
|
+
} catch (e) {
|
|
161
|
+
childLog("qa").warn("auto compliance failed", {
|
|
162
|
+
filePath,
|
|
163
|
+
error: e instanceof Error ? e.message : String(e),
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
132
168
|
// ── Startup: seed + build initial prompt ────────────────────────────────
|
|
133
169
|
try {
|
|
134
170
|
seedBuiltinDesigns()
|
|
@@ -577,7 +613,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
577
613
|
// Handles PDF and images — read tool succeeds with base64 attachment.
|
|
578
614
|
// PDF: extract text, remove base64. Images: jimp compress.
|
|
579
615
|
//
|
|
580
|
-
// Also handles:
|
|
616
|
+
// Also handles: fast design compliance after writing/patching/editing decks/*.html
|
|
581
617
|
"tool.execute.after": async (input, output) => {
|
|
582
618
|
if (!ctx.enabled) return
|
|
583
619
|
|
|
@@ -594,61 +630,47 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
594
630
|
return
|
|
595
631
|
}
|
|
596
632
|
|
|
597
|
-
// ──
|
|
633
|
+
// ── Fast design compliance after deck HTML writes/patches/edits ───
|
|
598
634
|
if (input.tool === "write") {
|
|
599
635
|
const filePath: string = input.args?.filePath ?? ""
|
|
600
636
|
const blockedReason = blockedDeckWrites.get(filePath)
|
|
601
637
|
if (blockedReason) {
|
|
602
638
|
blockedDeckWrites.delete(filePath)
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
(existing ? existing + "\n\n" : "") +
|
|
639
|
+
appendToolResult(
|
|
640
|
+
output,
|
|
606
641
|
"---\n\n**[revela state gate]** Write was blocked.\n\n" +
|
|
607
642
|
`${blockedReason}\n\n` +
|
|
608
643
|
"Use the `revela-decks` tool or complete the DECKS.json review workflow instead."
|
|
644
|
+
)
|
|
609
645
|
return
|
|
610
646
|
}
|
|
611
|
-
|
|
612
|
-
if (!isDeckHtmlPath(filePath)) return
|
|
613
|
-
|
|
614
|
-
try {
|
|
615
|
-
// Extract design's allowed class vocabulary for compliance checking
|
|
616
|
-
let vocabulary
|
|
617
|
-
try {
|
|
618
|
-
vocabulary = extractDesignClasses()
|
|
619
|
-
} catch {
|
|
620
|
-
// Design may not be installed or may have no markers — skip compliance
|
|
621
|
-
}
|
|
622
|
-
const report = await runQA(filePath, vocabulary)
|
|
623
|
-
// Only append QA report to tool output if there are issues
|
|
624
|
-
if (report.totalIssues > 0) {
|
|
625
|
-
const formatted = formatReport(report)
|
|
626
|
-
// Append to the write tool's output so the LLM sees it immediately
|
|
627
|
-
const existing = (output as any).result ?? ""
|
|
628
|
-
;(output as any).result =
|
|
629
|
-
(existing ? existing + "\n\n" : "") +
|
|
630
|
-
"---\n\n**[revela layout QA]** Auto-check completed:\n\n" +
|
|
631
|
-
formatted
|
|
632
|
-
}
|
|
633
|
-
} catch (e) {
|
|
634
|
-
childLog("qa").warn("auto QA failed", {
|
|
635
|
-
filePath,
|
|
636
|
-
error: e instanceof Error ? e.message : String(e),
|
|
637
|
-
})
|
|
638
|
-
// Don't surface errors to the LLM — fail silently
|
|
639
|
-
}
|
|
647
|
+
await appendComplianceReport(filePath, output)
|
|
640
648
|
return
|
|
641
649
|
}
|
|
642
650
|
|
|
643
651
|
if (input.tool === "apply_patch" && blockedDeckPatches.size > 0) {
|
|
644
652
|
const [blockedPath, blockedReason] = blockedDeckPatches.entries().next().value ?? []
|
|
645
653
|
if (blockedPath) blockedDeckPatches.delete(blockedPath)
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
(existing ? existing + "\n\n" : "") +
|
|
654
|
+
appendToolResult(
|
|
655
|
+
output,
|
|
649
656
|
"---\n\n**[revela prewrite gate]** Deck HTML patch was blocked.\n\n" +
|
|
650
657
|
`${blockedReason}\n\n` +
|
|
651
658
|
"Run `/revela review` or complete the same DECKS.json review workflow before patching the deck."
|
|
659
|
+
)
|
|
660
|
+
return
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (input.tool === "apply_patch") {
|
|
664
|
+
const patchText = extractPatchTextArg(input.args as Record<string, unknown>)
|
|
665
|
+
const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
|
|
666
|
+
for (const target of targets) {
|
|
667
|
+
await appendComplianceReport(target, output)
|
|
668
|
+
}
|
|
669
|
+
return
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (input.tool === "edit") {
|
|
673
|
+
await appendComplianceReport(extractEditFilePath(input.args), output)
|
|
652
674
|
return
|
|
653
675
|
}
|
|
654
676
|
},
|
package/skill/SKILL.md
CHANGED
|
@@ -232,8 +232,8 @@ layouts (cover, TOC, closing, quote, summary, etc.). When unsure, use `"false"`.
|
|
|
232
232
|
|
|
233
233
|
Example: `<section class="slide" slide-qa="true" data-index="0">`
|
|
234
234
|
|
|
235
|
-
The
|
|
236
|
-
|
|
235
|
+
The current QA path treats this as deck metadata. Automated checks focus on
|
|
236
|
+
design compliance and hard overflow errors, not subjective fill or spacing.
|
|
237
237
|
|
|
238
238
|
### Domain Context
|
|
239
239
|
|
|
@@ -382,11 +382,11 @@ exclusively for fine-tuning spacing and sizing (`margin`, `padding`, `gap`,
|
|
|
382
382
|
component — **NEVER adapt the component structure to fit content. NEVER create
|
|
383
383
|
a new component because the existing ones "don't quite fit".**
|
|
384
384
|
|
|
385
|
-
The
|
|
386
|
-
|
|
385
|
+
The automatic compliance check will flag any unrecognised CSS class. If the
|
|
386
|
+
QA report contains compliance issues after you write the file, you MUST
|
|
387
387
|
fix them immediately — remove the offending classes and replace them with the
|
|
388
388
|
closest component from the Component Index. Do not move on until all compliance
|
|
389
|
-
|
|
389
|
+
issues are resolved.
|
|
390
390
|
|
|
391
391
|
### Inline Editing
|
|
392
392
|
|
package/tools/decks.ts
CHANGED
|
@@ -57,7 +57,7 @@ export default tool({
|
|
|
57
57
|
title: tool.schema.string().describe("Slide title."),
|
|
58
58
|
purpose: tool.schema.string().optional().describe("Narrative purpose of this slide."),
|
|
59
59
|
layout: tool.schema.string().describe("Design layout name."),
|
|
60
|
-
qa: tool.schema.boolean().optional().describe("Whether the
|
|
60
|
+
qa: tool.schema.boolean().optional().describe("Whether the slide is marked QA-relevant deck metadata."),
|
|
61
61
|
components: tool.schema.array(tool.schema.string()).describe("Design components used by this slide."),
|
|
62
62
|
content: tool.schema.object({
|
|
63
63
|
headline: tool.schema.string().optional(),
|
package/tools/pdf.ts
CHANGED
|
@@ -8,11 +8,12 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
8
8
|
import { existsSync } from "fs"
|
|
9
9
|
import { resolve } from "path"
|
|
10
10
|
import { exportToPdf } from "../lib/pdf/export"
|
|
11
|
+
import { assertExportQAPassed } from "../lib/qa/export-gate"
|
|
11
12
|
|
|
12
13
|
export default tool({
|
|
13
14
|
description:
|
|
14
15
|
"Export a Revela-generated HTML slide deck to PDF. " +
|
|
15
|
-
"
|
|
16
|
+
"Runs pre-export QA before writing the PDF. " +
|
|
16
17
|
"Output is written beside the input file with the same basename and a .pdf extension.",
|
|
17
18
|
args: {
|
|
18
19
|
file: tool.schema
|
|
@@ -34,6 +35,7 @@ export default tool({
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
try {
|
|
38
|
+
await assertExportQAPassed(filePath)
|
|
37
39
|
const result = await exportToPdf(filePath)
|
|
38
40
|
return JSON.stringify({ ok: true, ...result }, null, 2)
|
|
39
41
|
} catch (e: any) {
|
package/tools/pptx.ts
CHANGED
|
@@ -8,11 +8,12 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
8
8
|
import { existsSync } from "fs"
|
|
9
9
|
import { resolve } from "path"
|
|
10
10
|
import { exportToPptx } from "../lib/pptx/export"
|
|
11
|
+
import { assertExportQAPassed } from "../lib/qa/export-gate"
|
|
11
12
|
|
|
12
13
|
export default tool({
|
|
13
14
|
description:
|
|
14
15
|
"Export a Revela-generated HTML slide deck to editable PPTX. " +
|
|
15
|
-
"
|
|
16
|
+
"Runs pre-export QA before writing the PPTX. " +
|
|
16
17
|
"Output is written beside the input file with the same basename and a .pptx extension.",
|
|
17
18
|
args: {
|
|
18
19
|
file: tool.schema
|
|
@@ -36,6 +37,7 @@ export default tool({
|
|
|
36
37
|
const progress: string[] = []
|
|
37
38
|
|
|
38
39
|
try {
|
|
40
|
+
await assertExportQAPassed(filePath)
|
|
39
41
|
const result = await exportToPptx(filePath, {
|
|
40
42
|
onProgress: (event) => {
|
|
41
43
|
progress.push(event.message)
|
package/tools/qa.ts
CHANGED
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* tools/qa.ts
|
|
3
3
|
*
|
|
4
|
-
* revela-qa —
|
|
4
|
+
* revela-qa — Hard-error quality assurance for generated slide HTML files.
|
|
5
5
|
*
|
|
6
|
-
* Exposed to the LLM so it can
|
|
7
|
-
* Also called automatically by the tool.execute.after hook in plugin.ts
|
|
8
|
-
* when the LLM writes a file matching decks/*.html.
|
|
6
|
+
* Exposed to the LLM so it can check overflow and design compliance.
|
|
9
7
|
*/
|
|
10
8
|
|
|
11
9
|
import { tool } from "@opencode-ai/plugin"
|
|
12
10
|
import { resolve } from "path"
|
|
13
11
|
import { existsSync } from "fs"
|
|
14
12
|
import { runQA, formatReport } from "../lib/qa"
|
|
15
|
-
import { extractDesignClasses } from "../lib/design/designs"
|
|
16
13
|
|
|
17
14
|
export default tool({
|
|
18
15
|
description:
|
|
19
|
-
"Run
|
|
16
|
+
"Run hard-error checks on a generated slide HTML file. " +
|
|
20
17
|
"Opens the file in a headless browser and measures actual rendered geometry. " +
|
|
21
|
-
"Checks for
|
|
22
|
-
"left-right column asymmetry, element overflow, and card height variance. " +
|
|
18
|
+
"Checks for element overflow. " +
|
|
23
19
|
"Returns a structured report with specific issues and fix instructions. " +
|
|
24
|
-
"Call this
|
|
20
|
+
"Call this when an edit may affect layout, or before delivery if export commands are not used.",
|
|
25
21
|
args: {
|
|
26
22
|
file: tool.schema
|
|
27
23
|
.string()
|
|
@@ -43,14 +39,7 @@ export default tool({
|
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
try {
|
|
46
|
-
|
|
47
|
-
let vocabulary
|
|
48
|
-
try {
|
|
49
|
-
vocabulary = extractDesignClasses()
|
|
50
|
-
} catch {
|
|
51
|
-
// Design may not be installed or may have no markers — skip compliance
|
|
52
|
-
}
|
|
53
|
-
const report = await runQA(filePath, vocabulary)
|
|
42
|
+
const report = await runQA(filePath)
|
|
54
43
|
const formatted = formatReport(report)
|
|
55
44
|
|
|
56
45
|
// Prepend a compact JSON summary for programmatic use if needed
|
|
@@ -63,7 +52,7 @@ export default tool({
|
|
|
63
52
|
|
|
64
53
|
return `<!-- QA Summary: ${jsonSummary} -->\n\n${formatted}`
|
|
65
54
|
} catch (err: any) {
|
|
66
|
-
return `Error running
|
|
55
|
+
return `Error running hard-error QA: ${err?.message ?? String(err)}\n\nMake sure Chrome is installed at /Applications/Google Chrome.app`
|
|
67
56
|
}
|
|
68
57
|
},
|
|
69
58
|
})
|