@cyber-dash-tech/revela 0.1.3 → 0.1.5
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 +2 -3
- package/README.zh-CN.md +2 -3
- package/lib/agents/research-prompt.ts +7 -3
- package/lib/design/designs.ts +158 -0
- package/lib/log.ts +3 -2
- package/lib/qa/checks.ts +132 -12
- package/lib/qa/index.ts +26 -8
- package/lib/qa/measure.ts +54 -4
- package/package.json +1 -1
- package/plugin.ts +24 -15
- package/skill/SKILL.md +106 -32
- package/tools/qa.ts +9 -1
- package/tools/workspace-scan.ts +17 -2
package/README.md
CHANGED
|
@@ -102,9 +102,8 @@ Three designs are bundled. Switch with `/revela designs <name>`.
|
|
|
102
102
|
|
|
103
103
|
| Name | Description | Preview |
|
|
104
104
|
|---|---|---|
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `editorial-ribbon` | Bold editorial layout — accent ribbons, strong headlines, high visual impact |  |
|
|
105
|
+
| `aurora` | Dark executive style — deep navy/slate, sharp typography, ECharts data visualization |  |
|
|
106
|
+
| `summit` | Editorial outdoor annual-report theme |  |
|
|
108
107
|
|
|
109
108
|
---
|
|
110
109
|
|
package/README.zh-CN.md
CHANGED
|
@@ -133,9 +133,8 @@ OPENCODE_ENABLE_EXA=1 opencode
|
|
|
133
133
|
|
|
134
134
|
| 名称 | 说明 | 预览 |
|
|
135
135
|
|---|---|---|
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `editorial-ribbon` | 大胆的编辑版式 —— 强调色横幅,醒目标题,高视觉冲击力 |  |
|
|
136
|
+
| `aurora` | 颜色主题 — 极光, 高饱和度, ECharts 数据可视化 |  |
|
|
137
|
+
| `summit` | 极简主义 - 户外,适合有丰富插图,Echart 数据可视化 |  |
|
|
139
138
|
|
|
140
139
|
---
|
|
141
140
|
|
|
@@ -53,9 +53,13 @@ Formulate **3–6 targeted search queries** for your specific axis, covering:
|
|
|
53
53
|
|
|
54
54
|
For Chinese topics: search in **both Chinese and English**.
|
|
55
55
|
|
|
56
|
-
Use
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
Use **\`websearch\`** for broad keyword queries to find relevant pages, reports,
|
|
57
|
+
and data. Then use **\`webfetch\`** to retrieve specific pages for depth.
|
|
58
|
+
|
|
59
|
+
Search strategy:
|
|
60
|
+
- Start with \`websearch\` to discover relevant URLs (market reports, company pages, news)
|
|
61
|
+
- Follow up with \`webfetch\` on the most promising URLs for full content
|
|
62
|
+
- For Chinese topics: run \`websearch\` queries in both Chinese and English
|
|
59
63
|
|
|
60
64
|
---
|
|
61
65
|
|
package/lib/design/designs.ts
CHANGED
|
@@ -436,6 +436,164 @@ function installFromPath(srcPath: string, name?: string): string {
|
|
|
436
436
|
return designName
|
|
437
437
|
}
|
|
438
438
|
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
// Design class vocabulary extraction
|
|
441
|
+
// ---------------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The set of CSS class names that are always allowed, regardless of design.
|
|
445
|
+
* These are structural/behavioural classes used by every presentation.
|
|
446
|
+
*/
|
|
447
|
+
const UNIVERSAL_CLASSES = new Set([
|
|
448
|
+
"slide",
|
|
449
|
+
"slide-canvas",
|
|
450
|
+
"visible",
|
|
451
|
+
"reveal",
|
|
452
|
+
"editable",
|
|
453
|
+
"page",
|
|
454
|
+
"bg",
|
|
455
|
+
"fg",
|
|
456
|
+
"overlay",
|
|
457
|
+
"alt",
|
|
458
|
+
"strong",
|
|
459
|
+
])
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* CSS class prefixes that are always exempt from compliance checks.
|
|
463
|
+
* Third-party libraries (icons, charts) generate classes with these prefixes.
|
|
464
|
+
*/
|
|
465
|
+
export const DEFAULT_PREFIX_EXEMPTIONS: string[] = ["lucide-", "echarts-", "editable-"]
|
|
466
|
+
|
|
467
|
+
export interface DesignClassVocabulary {
|
|
468
|
+
/** Complete set of allowed CSS class names. */
|
|
469
|
+
classes: Set<string>
|
|
470
|
+
/** Class name prefixes that bypass compliance checks. */
|
|
471
|
+
prefixExemptions: string[]
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Extract all CSS class names defined in a DESIGN.md and return a closed
|
|
476
|
+
* vocabulary of allowed class names for compliance checking.
|
|
477
|
+
*
|
|
478
|
+
* Extraction sources:
|
|
479
|
+
* - @design:foundation — parses CSS `.class-name` selectors in code blocks
|
|
480
|
+
* - @layout:xxx — parses HTML class="..." attributes and CSS selectors
|
|
481
|
+
* - @component:xxx — same as layouts
|
|
482
|
+
*
|
|
483
|
+
* UNIVERSAL_CLASSES and DEFAULT_PREFIX_EXEMPTIONS are always included.
|
|
484
|
+
*
|
|
485
|
+
* Falls back to UNIVERSAL_CLASSES-only when the design has no markers.
|
|
486
|
+
*/
|
|
487
|
+
export function extractDesignClasses(designName?: string): DesignClassVocabulary {
|
|
488
|
+
const name = designName || activeDesign()
|
|
489
|
+
const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
|
|
490
|
+
|
|
491
|
+
if (!existsSync(mdPath)) {
|
|
492
|
+
return { classes: new Set(UNIVERSAL_CLASSES), prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const raw = readFileSync(mdPath, "utf-8")
|
|
496
|
+
const { body } = parseFrontmatter(raw)
|
|
497
|
+
const { sections, layouts, components, hasMarkers } = parseDesignSections(body)
|
|
498
|
+
|
|
499
|
+
if (!hasMarkers) {
|
|
500
|
+
// No markers — can't extract a reliable vocabulary; return universal only
|
|
501
|
+
return { classes: new Set(UNIVERSAL_CLASSES), prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const classes = new Set(UNIVERSAL_CLASSES)
|
|
505
|
+
|
|
506
|
+
// Regex patterns for extraction (stateless — reset lastIndex before each use)
|
|
507
|
+
const htmlClassRe = /class="([^"]*)"/g
|
|
508
|
+
const cssClassRe = /\.([a-zA-Z_][\w-]*)/g
|
|
509
|
+
|
|
510
|
+
/** Extract CSS class names from a CSS string (selector context only).
|
|
511
|
+
* Strips url(...) and string literals before scanning to avoid false positives
|
|
512
|
+
* from inline SVG data URIs and other non-selector content.
|
|
513
|
+
*/
|
|
514
|
+
function extractFromCss(css: string): void {
|
|
515
|
+
// Remove url(...) values (may contain encoded paths like w3.org, data URIs, etc.)
|
|
516
|
+
const stripped = css
|
|
517
|
+
.replace(/url\([^)]*\)/gi, "url()")
|
|
518
|
+
// Remove quoted strings (single or double)
|
|
519
|
+
.replace(/"[^"]*"/g, '""')
|
|
520
|
+
.replace(/'[^']*'/g, "''")
|
|
521
|
+
cssClassRe.lastIndex = 0
|
|
522
|
+
let m: RegExpExecArray | null
|
|
523
|
+
while ((m = cssClassRe.exec(stripped)) !== null) {
|
|
524
|
+
if (m[1]) classes.add(m[1])
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Extract CSS class names from an HTML string (class="..." attributes only). */
|
|
529
|
+
function extractFromHtml(html: string): void {
|
|
530
|
+
htmlClassRe.lastIndex = 0
|
|
531
|
+
let m: RegExpExecArray | null
|
|
532
|
+
while ((m = htmlClassRe.exec(html)) !== null) {
|
|
533
|
+
for (const cls of m[1].split(/\s+/)) {
|
|
534
|
+
if (cls.trim()) classes.add(cls.trim())
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Also scan inline <style>...</style> blocks inside HTML snippets
|
|
538
|
+
const styleBlockRe = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
539
|
+
styleBlockRe.lastIndex = 0
|
|
540
|
+
while ((m = styleBlockRe.exec(html)) !== null) {
|
|
541
|
+
extractFromCss(m[1])
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Scan a DESIGN.md section body and extract CSS class names only from:
|
|
547
|
+
* - ```css ... ``` code blocks → CSS selector extraction
|
|
548
|
+
* - ```html ... ``` code blocks → HTML class="..." attribute extraction
|
|
549
|
+
* - <style>...</style> blocks → CSS selector extraction
|
|
550
|
+
*
|
|
551
|
+
* Skips ```javascript / ```js / ```ts code blocks entirely to avoid
|
|
552
|
+
* extracting JS method names (e.g. .classList, .forEach) as class names.
|
|
553
|
+
*/
|
|
554
|
+
function extractFromSection(text: string): void {
|
|
555
|
+
// Match fenced code blocks: ```<lang>\n...\n```
|
|
556
|
+
const fenceRe = /```(\w*)\n([\s\S]*?)```/g
|
|
557
|
+
let m: RegExpExecArray | null
|
|
558
|
+
fenceRe.lastIndex = 0
|
|
559
|
+
while ((m = fenceRe.exec(text)) !== null) {
|
|
560
|
+
const lang = m[1].toLowerCase()
|
|
561
|
+
const body = m[2]
|
|
562
|
+
if (lang === "css" || lang === "scss" || lang === "less") {
|
|
563
|
+
extractFromCss(body)
|
|
564
|
+
} else if (lang === "html" || lang === "xml" || lang === "") {
|
|
565
|
+
// Unknown-lang fences in DESIGN.md are usually HTML snippets
|
|
566
|
+
extractFromHtml(body)
|
|
567
|
+
}
|
|
568
|
+
// javascript / js / ts / typescript → skip entirely
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Also scan top-level <style>...</style> outside code blocks
|
|
572
|
+
const styleBlockRe = /<style[^>]*>([\s\S]*?)<\/style>/gi
|
|
573
|
+
styleBlockRe.lastIndex = 0
|
|
574
|
+
while ((m = styleBlockRe.exec(text)) !== null) {
|
|
575
|
+
extractFromCss(m[1])
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Extract from all sections (foundation, rules, etc.)
|
|
580
|
+
for (const content of Object.values(sections)) {
|
|
581
|
+
extractFromSection(content)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Extract from all layouts
|
|
585
|
+
for (const { content } of Object.values(layouts)) {
|
|
586
|
+
extractFromSection(content)
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Extract from all components
|
|
590
|
+
for (const content of Object.values(components)) {
|
|
591
|
+
extractFromSection(content)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
return { classes, prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
|
|
595
|
+
}
|
|
596
|
+
|
|
439
597
|
async function installFromUrl(url: string, name?: string): Promise<string> {
|
|
440
598
|
// Download zip to temp dir
|
|
441
599
|
const tmp = join(tmpdir(), `revela-design-${Date.now()}`)
|
package/lib/log.ts
CHANGED
|
@@ -17,8 +17,9 @@ export const log = new Logger({
|
|
|
17
17
|
type: "json",
|
|
18
18
|
hideLogPositionForProduction: true,
|
|
19
19
|
overwrite: {
|
|
20
|
-
transportJSON: (
|
|
21
|
-
|
|
20
|
+
transportJSON: (_logObj: unknown) => {
|
|
21
|
+
// Silenced: revela runs as an OpenCode plugin; writing to stderr
|
|
22
|
+
// pollutes the host terminal. Logs are intentionally suppressed.
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
})
|
package/lib/qa/checks.ts
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/checks.ts
|
|
3
3
|
*
|
|
4
|
-
* Geometry-based layout quality checks — four orthogonal visual dimensions
|
|
4
|
+
* Geometry-based layout quality checks — four orthogonal visual dimensions,
|
|
5
|
+
* plus a design-compliance dimension that verifies CSS class usage.
|
|
5
6
|
*
|
|
6
|
-
* Dimension 1: Overflow
|
|
7
|
-
* Dimension 2: Balance
|
|
8
|
-
* Dimension 3:
|
|
9
|
-
* Dimension 4:
|
|
7
|
+
* Dimension 1: Overflow — elements exceed canvas bounds (correctness)
|
|
8
|
+
* Dimension 2: Balance — content centroid & distribution (fill, sparsity)
|
|
9
|
+
* Dimension 3: Rhythm — spacing regularity & internal whitespace
|
|
10
|
+
* Dimension 4: Compliance — CSS classes match the active design's vocabulary
|
|
10
11
|
*
|
|
11
12
|
* All checks operate on SlideMetrics produced by measure.ts.
|
|
12
|
-
*
|
|
13
|
+
* Dimensions 1–4 are geometry-only (no CSS class-name assumptions).
|
|
14
|
+
* Dimension 5 requires an allowedClasses vocabulary from the design system.
|
|
13
15
|
*/
|
|
14
16
|
|
|
15
17
|
import type { SlideMetrics, ElementInfo, Rect } from "./measure"
|
|
@@ -20,11 +22,12 @@ import { CANVAS_W, CANVAS_H } from "./measure"
|
|
|
20
22
|
export type IssueSeverity = "error" | "warning" | "info"
|
|
21
23
|
|
|
22
24
|
export interface LayoutIssue {
|
|
23
|
-
type: "overflow" | "balance" | "symmetry" | "rhythm"
|
|
25
|
+
type: "overflow" | "balance" | "symmetry" | "rhythm" | "compliance"
|
|
24
26
|
/** Sub-category within the dimension */
|
|
25
27
|
sub?: "centroid_offset" | "bottom_gap" | "sparse"
|
|
26
28
|
| "height_mismatch" | "density_mismatch"
|
|
27
29
|
| "gap_variance"
|
|
30
|
+
| "unknown_class" | "novel_css_rule"
|
|
28
31
|
severity: IssueSeverity
|
|
29
32
|
/** Human-readable description for the LLM to act on */
|
|
30
33
|
detail: string
|
|
@@ -520,20 +523,137 @@ function checkRhythm(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
520
523
|
return issues
|
|
521
524
|
}
|
|
522
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
|
+
|
|
523
612
|
// ── Main export ───────────────────────────────────────────────────────────────
|
|
524
613
|
|
|
525
614
|
/**
|
|
526
|
-
*
|
|
615
|
+
* Options for runChecks(). All fields are optional — omitting them disables
|
|
616
|
+
* the corresponding checks (backward compatible).
|
|
617
|
+
*/
|
|
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
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Run all dimension checks on a set of slide metrics and produce a QA report.
|
|
527
629
|
*/
|
|
528
|
-
export function runChecks(
|
|
630
|
+
export function runChecks(
|
|
631
|
+
filePath: string,
|
|
632
|
+
allMetrics: SlideMetrics[],
|
|
633
|
+
options?: RunChecksOptions,
|
|
634
|
+
): QAReport {
|
|
529
635
|
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
|
+
: []
|
|
530
643
|
|
|
531
644
|
for (const metrics of allMetrics) {
|
|
645
|
+
const complianceIssues: LayoutIssue[] =
|
|
646
|
+
allowedClasses
|
|
647
|
+
? checkCompliance(metrics, allowedClasses, prefixExemptions)
|
|
648
|
+
: []
|
|
649
|
+
|
|
532
650
|
const issues: LayoutIssue[] = [
|
|
533
651
|
...checkOverflow(metrics),
|
|
534
652
|
...checkBalance(metrics),
|
|
535
|
-
...checkSymmetry(metrics),
|
|
536
653
|
...checkRhythm(metrics),
|
|
654
|
+
...complianceIssues,
|
|
655
|
+
// Attach novel_css_rule issues to slide 0 only
|
|
656
|
+
...(metrics.index === 0 ? novelCssIssues : []),
|
|
537
657
|
]
|
|
538
658
|
|
|
539
659
|
slides.push({ index: metrics.index, title: metrics.title, issues })
|
|
@@ -594,9 +714,9 @@ export function formatReport(report: QAReport): string {
|
|
|
594
714
|
`- **balance/centroid_offset**: redistribute content so the visual weight is centred — avoid concentrating everything in one corner or side.`,
|
|
595
715
|
`- **balance/bottom_gap**: expand content to fill the slide, use \`flex: 1\` on containers, add more content blocks, or reduce top padding.`,
|
|
596
716
|
`- **balance/sparse**: add more content components, increase font sizes, or use a layout with fewer columns.`,
|
|
597
|
-
`- **symmetry/height_mismatch**: equalise side-by-side column heights — use \`align-items: stretch\` or match content density.`,
|
|
598
|
-
`- **symmetry/density_mismatch**: balance content between columns — add items to the sparse column or reduce items in the dense one.`,
|
|
599
717
|
`- **rhythm/gap_variance**: use consistent \`gap\` or \`margin\` values between stacked elements instead of mixing sizes.`,
|
|
718
|
+
`- **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
|
+
`- **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.`,
|
|
600
720
|
)
|
|
601
721
|
|
|
602
722
|
return lines.join("\n")
|
package/lib/qa/index.ts
CHANGED
|
@@ -7,31 +7,49 @@
|
|
|
7
7
|
|
|
8
8
|
import { measureSlides } from "./measure"
|
|
9
9
|
import { runChecks, formatReport } from "./checks"
|
|
10
|
-
import type { QAReport } from "./checks"
|
|
10
|
+
import type { QAReport, RunChecksOptions } from "./checks"
|
|
11
|
+
import type { DesignClassVocabulary } from "../design/designs"
|
|
11
12
|
|
|
12
13
|
export type { QAReport, SlideReport, LayoutIssue, IssueSeverity } from "./checks"
|
|
14
|
+
export type { RunChecksOptions } from "./checks"
|
|
13
15
|
|
|
14
16
|
/**
|
|
15
17
|
* Run a full layout QA pass on `htmlFilePath`.
|
|
16
18
|
*
|
|
17
19
|
* 1. Opens the file in headless Chrome (puppeteer-core)
|
|
18
|
-
* 2. Measures each .slide element's geometry
|
|
19
|
-
* 3. Runs all checks (
|
|
20
|
+
* 2. Measures each .slide element's geometry + CSS class definitions
|
|
21
|
+
* 3. Runs all checks (overflow, balance, symmetry, rhythm, compliance)
|
|
20
22
|
* 4. Returns a structured QAReport
|
|
21
23
|
*
|
|
24
|
+
* Pass `vocabulary` (from `extractDesignClasses()`) to enable compliance checks.
|
|
25
|
+
* Omit it to run geometry-only checks (backward compatible).
|
|
26
|
+
*
|
|
22
27
|
* Throws if the file cannot be opened or Chrome is not found.
|
|
23
28
|
*/
|
|
24
|
-
export async function runQA(
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
export async function runQA(
|
|
30
|
+
htmlFilePath: string,
|
|
31
|
+
vocabulary?: DesignClassVocabulary,
|
|
32
|
+
): Promise<QAReport> {
|
|
33
|
+
const result = await measureSlides(htmlFilePath)
|
|
34
|
+
const options: RunChecksOptions | undefined = vocabulary
|
|
35
|
+
? {
|
|
36
|
+
allowedClasses: vocabulary.classes,
|
|
37
|
+
prefixExemptions: vocabulary.prefixExemptions,
|
|
38
|
+
cssDefinedClasses: result.cssDefinedClasses,
|
|
39
|
+
}
|
|
40
|
+
: undefined
|
|
41
|
+
return runChecks(htmlFilePath, result.slides, options)
|
|
27
42
|
}
|
|
28
43
|
|
|
29
44
|
/**
|
|
30
45
|
* Run QA and return a formatted markdown report string.
|
|
31
46
|
* Suitable for injecting into tool output or sending as a message to the LLM.
|
|
32
47
|
*/
|
|
33
|
-
export async function runQAFormatted(
|
|
34
|
-
|
|
48
|
+
export async function runQAFormatted(
|
|
49
|
+
htmlFilePath: string,
|
|
50
|
+
vocabulary?: DesignClassVocabulary,
|
|
51
|
+
): Promise<string> {
|
|
52
|
+
const report = await runQA(htmlFilePath, vocabulary)
|
|
35
53
|
return formatReport(report)
|
|
36
54
|
}
|
|
37
55
|
|
package/lib/qa/measure.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface ElementInfo {
|
|
|
47
47
|
visible: boolean
|
|
48
48
|
/** direct children that are also visible */
|
|
49
49
|
children: ElementInfo[]
|
|
50
|
+
/** all CSS class names on this element */
|
|
51
|
+
classList: string[]
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
export interface SlideMetrics {
|
|
@@ -69,6 +71,16 @@ export interface SlideMetrics {
|
|
|
69
71
|
contentRect: Rect
|
|
70
72
|
}
|
|
71
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Result returned by measureSlides().
|
|
76
|
+
* Contains per-slide geometry data and CSS class names defined in <style> blocks.
|
|
77
|
+
*/
|
|
78
|
+
export interface MeasurementResult {
|
|
79
|
+
slides: SlideMetrics[]
|
|
80
|
+
/** All CSS class names defined in <style> blocks of the HTML (deduplicated). */
|
|
81
|
+
cssDefinedClasses: string[]
|
|
82
|
+
}
|
|
83
|
+
|
|
72
84
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
73
85
|
|
|
74
86
|
function findChromePath(): string {
|
|
@@ -86,9 +98,9 @@ function findChromePath(): string {
|
|
|
86
98
|
|
|
87
99
|
/**
|
|
88
100
|
* Open `htmlFilePath` in a headless Chrome at 1920×1080, measure each slide,
|
|
89
|
-
* and return
|
|
101
|
+
* and return slide geometry + CSS class names defined in <style> blocks.
|
|
90
102
|
*/
|
|
91
|
-
export async function measureSlides(htmlFilePath: string): Promise<
|
|
103
|
+
export async function measureSlides(htmlFilePath: string): Promise<MeasurementResult> {
|
|
92
104
|
const executablePath = findChromePath()
|
|
93
105
|
const fileUrl = pathToFileURL(htmlFilePath).href
|
|
94
106
|
|
|
@@ -106,9 +118,22 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
|
|
|
106
118
|
try {
|
|
107
119
|
const page = await browser.newPage()
|
|
108
120
|
|
|
121
|
+
// Block all external (http/https) requests — fonts, CDN scripts, images.
|
|
122
|
+
// QA checks are purely geometry-based and do not require network resources.
|
|
123
|
+
// This makes measurement fast and reliable regardless of network conditions.
|
|
124
|
+
await page.setRequestInterception(true)
|
|
125
|
+
page.on("request", (req) => {
|
|
126
|
+
const url = req.url()
|
|
127
|
+
if (url.startsWith("https://") || url.startsWith("http://")) {
|
|
128
|
+
req.abort()
|
|
129
|
+
} else {
|
|
130
|
+
req.continue()
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
109
134
|
// Set viewport to exact canvas size so scale === 1 (no CSS transform needed).
|
|
110
135
|
await page.setViewport({ width: CANVAS_W, height: CANVAS_H })
|
|
111
|
-
await page.goto(fileUrl, { waitUntil: "
|
|
136
|
+
await page.goto(fileUrl, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
112
137
|
|
|
113
138
|
// Wait for any entrance animations / intersection observers to fire.
|
|
114
139
|
await new Promise((r) => setTimeout(r, 600))
|
|
@@ -181,6 +206,7 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
|
|
|
181
206
|
rect: ReturnType<typeof toRectRelative>
|
|
182
207
|
visible: boolean
|
|
183
208
|
children: EI[]
|
|
209
|
+
classList: string[]
|
|
184
210
|
}
|
|
185
211
|
|
|
186
212
|
function collectChildren(
|
|
@@ -208,6 +234,7 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
|
|
|
208
234
|
selector: selectorOf(child),
|
|
209
235
|
rect: relR,
|
|
210
236
|
visible: true,
|
|
237
|
+
classList: Array.from(child.classList),
|
|
211
238
|
children: collectChildren(child, offsetTop, offsetLeft, depth + 1),
|
|
212
239
|
})
|
|
213
240
|
}
|
|
@@ -281,7 +308,30 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
|
|
|
281
308
|
if (slideData) metrics.push(slideData as SlideMetrics)
|
|
282
309
|
}
|
|
283
310
|
|
|
284
|
-
|
|
311
|
+
// Extract all CSS class names defined in <style> blocks.
|
|
312
|
+
// Uses the browser's CSSStyleRule API for reliable selector parsing.
|
|
313
|
+
const cssDefinedClasses = await page.evaluate((): string[] => {
|
|
314
|
+
const classes: string[] = []
|
|
315
|
+
const classRe = /\.([a-zA-Z_][\w-]*)/g
|
|
316
|
+
for (const sheet of Array.from(document.styleSheets)) {
|
|
317
|
+
try {
|
|
318
|
+
for (const rule of Array.from(sheet.cssRules)) {
|
|
319
|
+
if (rule instanceof CSSStyleRule) {
|
|
320
|
+
let m: RegExpExecArray | null
|
|
321
|
+
classRe.lastIndex = 0
|
|
322
|
+
while ((m = classRe.exec(rule.selectorText)) !== null) {
|
|
323
|
+
classes.push(m[1])
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
} catch {
|
|
328
|
+
// Cross-origin or inaccessible sheets (e.g. external CDN) will throw
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return [...new Set(classes)]
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
return { slides: metrics, cssDefinedClasses }
|
|
285
335
|
} finally {
|
|
286
336
|
await browser.close()
|
|
287
337
|
}
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -48,6 +48,7 @@ import workspaceScanTool from "./tools/workspace-scan"
|
|
|
48
48
|
import qaTool from "./tools/qa"
|
|
49
49
|
import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
|
|
50
50
|
import { runQA, formatReport } from "./lib/qa"
|
|
51
|
+
import { extractDesignClasses } from "./lib/design/designs"
|
|
51
52
|
import { log, childLog } from "./lib/log"
|
|
52
53
|
|
|
53
54
|
// OpenCode internal agent signatures — used to skip system prompt injection
|
|
@@ -111,6 +112,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
111
112
|
// Permissions: read-only on edit/bash; write allowed to create researches/ files.
|
|
112
113
|
// No model override — inherits from the calling primary agent.
|
|
113
114
|
opencodeConfig.agent ??= {}
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
116
|
opencodeConfig.agent["revela-research"] = {
|
|
115
117
|
description: "Revela research agent — searches and collects raw materials for presentations",
|
|
116
118
|
mode: "subagent",
|
|
@@ -123,7 +125,19 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
123
125
|
"ls": "allow",
|
|
124
126
|
},
|
|
125
127
|
webfetch: "allow",
|
|
126
|
-
},
|
|
128
|
+
} as any,
|
|
129
|
+
}
|
|
130
|
+
// Give revela-research explicit websearch allow (overrides global deny below)
|
|
131
|
+
;(opencodeConfig.agent["revela-research"].permission as any).websearch = "allow"
|
|
132
|
+
|
|
133
|
+
// Block websearch for the primary agent globally.
|
|
134
|
+
// permission.ask hook is not triggered by OpenCode (no R.trigger call in binary).
|
|
135
|
+
// tool.execute.before throw is swallowed (trigger().catch(()=>{})).
|
|
136
|
+
// The only working mechanism is the config-level permission ruleset.
|
|
137
|
+
// revela-research agent overrides this with websearch: "allow" above.
|
|
138
|
+
opencodeConfig.permission ??= {}
|
|
139
|
+
if (!(opencodeConfig.permission as Record<string, unknown>)["websearch"]) {
|
|
140
|
+
;(opencodeConfig.permission as Record<string, unknown>)["websearch"] = "deny"
|
|
127
141
|
}
|
|
128
142
|
},
|
|
129
143
|
|
|
@@ -283,22 +297,10 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
283
297
|
// ── Pre-read: intercept binary files before read executes ──────────────
|
|
284
298
|
// Handles DOCX/PPTX/XLSX — read tool would Effect.fail on these.
|
|
285
299
|
// Extracts text → writes temp .txt → redirects args.filePath.
|
|
286
|
-
//
|
|
287
|
-
// Also blocks websearch for the primary agent — websearch must be delegated
|
|
288
|
-
// to the revela-research subagent. Use webfetch for specific URLs instead.
|
|
289
300
|
"tool.execute.before": async (input, output) => {
|
|
301
|
+
log.info("[hook] tool.execute.before fired", { tool: input.tool, enabled: ctx.enabled, isResearch: ctx.isResearchAgent })
|
|
290
302
|
if (!ctx.enabled) return
|
|
291
303
|
|
|
292
|
-
// ── Block websearch for primary agent ──────────────────────────────
|
|
293
|
-
if (input.tool === "websearch" && !ctx.isResearchAgent) {
|
|
294
|
-
throw new Error(
|
|
295
|
-
"[revela] websearch is not available for the primary agent. " +
|
|
296
|
-
"Delegate web research to the revela-research subagent via the Task tool — " +
|
|
297
|
-
"it searches systematically and saves structured findings for reuse across sessions. " +
|
|
298
|
-
"Use the webfetch tool if you need to read a specific URL directly.",
|
|
299
|
-
)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
304
|
if (input.tool !== "read") return
|
|
303
305
|
try {
|
|
304
306
|
await preRead(output.args)
|
|
@@ -338,7 +340,14 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
338
340
|
if (!filePath.match(/slides\/[^/]+\.html$/)) return
|
|
339
341
|
|
|
340
342
|
try {
|
|
341
|
-
|
|
343
|
+
// Extract design's allowed class vocabulary for compliance checking
|
|
344
|
+
let vocabulary
|
|
345
|
+
try {
|
|
346
|
+
vocabulary = extractDesignClasses()
|
|
347
|
+
} catch {
|
|
348
|
+
// Design may not be installed or may have no markers — skip compliance
|
|
349
|
+
}
|
|
350
|
+
const report = await runQA(filePath, vocabulary)
|
|
342
351
|
// Only append QA report to tool output if there are issues
|
|
343
352
|
if (report.totalIssues > 0) {
|
|
344
353
|
const formatted = formatReport(report)
|
package/skill/SKILL.md
CHANGED
|
@@ -34,9 +34,9 @@ Before writing any HTML, ask the user these questions **in a single message**
|
|
|
34
34
|
|
|
35
35
|
If the user's first message already answers most of these, skip what's clear and
|
|
36
36
|
only ask about what's missing. If the message is detailed enough, proceed directly
|
|
37
|
-
to Phase
|
|
37
|
+
to Phase 2.
|
|
38
38
|
|
|
39
|
-
### Phase
|
|
39
|
+
### Phase 2 — Select Design
|
|
40
40
|
|
|
41
41
|
Once you have the user's answers (especially topic, audience, and visual style),
|
|
42
42
|
pick the best-fit design before generating slides.
|
|
@@ -58,16 +58,16 @@ pick the best-fit design before generating slides.
|
|
|
58
58
|
|
|
59
59
|
4. Wait for the user's reply, then act:
|
|
60
60
|
- **Confirmed** (e.g. "yes", "sure", "go ahead") → activate the recommended
|
|
61
|
-
design and proceed to Phase
|
|
61
|
+
design and proceed to Phase 3:
|
|
62
62
|
Call the `designs` tool with action `"activate"` and name `"<name>"`.
|
|
63
|
-
- **User names a different design** → activate that one instead, then Phase
|
|
64
|
-
- **User says keep the current one** → skip the switch, proceed to Phase
|
|
63
|
+
- **User names a different design** → activate that one instead, then Phase 3.
|
|
64
|
+
- **User says keep the current one** → skip the switch, proceed to Phase 3.
|
|
65
65
|
|
|
66
|
-
Do not proceed to Phase
|
|
66
|
+
Do not proceed to Phase 3 until the user has replied to the design question.
|
|
67
67
|
|
|
68
68
|
---
|
|
69
69
|
|
|
70
|
-
### Phase
|
|
70
|
+
### Phase 3 — Research-First Protocol (自主调研)
|
|
71
71
|
|
|
72
72
|
**Always execute this phase — regardless of whether the user mentions reference
|
|
73
73
|
files.** Your job is to proactively gather all available information before
|
|
@@ -83,26 +83,26 @@ Research layers are **NOT** a sequential fallback chain where you stop once
|
|
|
83
83
|
│ LAUNCH TOGETHER (as your first action): │
|
|
84
84
|
│ │
|
|
85
85
|
│ ┌──────────────┐ ┌─────────────────────┐ │
|
|
86
|
-
│ │ Layer 1 │ │ Layer 2
|
|
86
|
+
│ │ Layer 1 │ │ Layer 2 │ │
|
|
87
87
|
│ │ Workspace │ │ Research agents │ │
|
|
88
88
|
│ │ scan │ │ (parallel per axis) │ │
|
|
89
89
|
│ └──────────────┘ └─────────────────────┘ │
|
|
90
90
|
│ │
|
|
91
91
|
│ After both complete: │
|
|
92
92
|
│ ┌──────────────┐ │
|
|
93
|
-
│ │ Layer
|
|
93
|
+
│ │ Layer 3 │ AI knowledge fills gaps │
|
|
94
94
|
│ └──────────────┘ │
|
|
95
95
|
│ │
|
|
96
96
|
│ Only if still missing: │
|
|
97
97
|
│ ┌──────────────┐ │
|
|
98
|
-
│ │ Layer
|
|
98
|
+
│ │ Layer 4 │ Ask the user │
|
|
99
99
|
│ └──────────────┘ │
|
|
100
100
|
└─────────────────────────────────────────────┘
|
|
101
101
|
```
|
|
102
102
|
|
|
103
|
-
**Layer 1 and Layer 2
|
|
104
|
-
Do not wait for Layer 1 results before launching Layer 2.
|
|
105
|
-
(AI knowledge) as an excuse to skip Layer 2.
|
|
103
|
+
**Layer 1 and Layer 2 launch in parallel as the FIRST action after Phase 2.**
|
|
104
|
+
Do not wait for Layer 1 results before launching Layer 2. Do not use Layer 3
|
|
105
|
+
(AI knowledge) as an excuse to skip Layer 2.
|
|
106
106
|
|
|
107
107
|
---
|
|
108
108
|
|
|
@@ -119,15 +119,16 @@ extracts text from binary formats (PDF, Excel, Word, PowerPoint) — just call
|
|
|
119
119
|
|
|
120
120
|
---
|
|
121
121
|
|
|
122
|
-
#### Layer 2
|
|
122
|
+
#### Layer 2 — Deep Research via Research Agents (MANDATORY)
|
|
123
123
|
|
|
124
124
|
**This layer is mandatory whenever the `@revela-research` subagent (Task tool
|
|
125
125
|
with `subagent_type: "revela-research"`) is available.** It is the primary
|
|
126
126
|
research workhorse — not an optional enhancement.
|
|
127
127
|
|
|
128
|
-
The research agent searches the web
|
|
129
|
-
|
|
130
|
-
file `researches/{topic-slug}/{axis-name}.md`
|
|
128
|
+
The research agent searches the web using `websearch` for broad discovery and
|
|
129
|
+
`webfetch` for depth on specific pages, reads workspace documents, and writes
|
|
130
|
+
structured findings to a single file `researches/{topic-slug}/{axis-name}.md`
|
|
131
|
+
in the workspace.
|
|
131
132
|
|
|
132
133
|
##### Parallelization Rule
|
|
133
134
|
|
|
@@ -160,7 +161,8 @@ List and read the findings files: `ls researches/{topic-slug}/`, then `read`
|
|
|
160
161
|
each `.md` file. Each file contains structured `## Data`, `## Cases`,
|
|
161
162
|
`## Images`, and `## Gaps` sections — use these directly as slide material.
|
|
162
163
|
Cross-reference agent findings with workspace documents (Layer 1). Flag any
|
|
163
|
-
contradictions.
|
|
164
|
+
contradictions. Once all findings are read, proceed to Phase 4 to present the
|
|
165
|
+
slide plan.
|
|
164
166
|
|
|
165
167
|
**Anti-pattern — NEVER do this:**
|
|
166
168
|
- Do NOT use `websearch` directly — it is blocked by the Revela plugin;
|
|
@@ -171,23 +173,23 @@ contradictions.
|
|
|
171
173
|
|
|
172
174
|
---
|
|
173
175
|
|
|
174
|
-
#### Layer
|
|
176
|
+
#### Layer 3 — AI Knowledge (Supplementary)
|
|
175
177
|
|
|
176
|
-
After Layer 1 and Layer 2
|
|
178
|
+
After Layer 1 and Layer 2 results are in, use your training data to fill
|
|
177
179
|
remaining gaps: industry context, historical background, technical explanations.
|
|
178
180
|
|
|
179
181
|
**Critical:** Always mark AI-sourced information with
|
|
180
182
|
`[Source: AI 公开知识,建议核实]`. Never present AI knowledge as verified fact.
|
|
181
183
|
|
|
182
184
|
This layer is supplementary — it adds context around the hard data from
|
|
183
|
-
Layers 1 and 2.
|
|
185
|
+
Layers 1 and 2. It must never be the primary source for quantitative claims
|
|
184
186
|
(market size, revenue, growth rates, etc.).
|
|
185
187
|
|
|
186
188
|
---
|
|
187
189
|
|
|
188
|
-
#### Layer
|
|
190
|
+
#### Layer 4 — Ask the User (Last Resort Only)
|
|
189
191
|
|
|
190
|
-
Only ask the user for information that Layers 1, 2, and
|
|
192
|
+
Only ask the user for information that Layers 1, 2, and 3 cannot cover.
|
|
191
193
|
When asking, first report what you already know:
|
|
192
194
|
|
|
193
195
|
> 我已从 workspace 文档和在线调研中获取了以下信息:
|
|
@@ -210,7 +212,6 @@ When asking, first report what you already know:
|
|
|
210
212
|
- **ALWAYS** decompose the topic into independent axes before launching agents
|
|
211
213
|
- **ALWAYS** read each `researches/{slug}/{axis}.md` after agents complete
|
|
212
214
|
- Use the `read` tool for all file types — binary formats are handled transparently
|
|
213
|
-
|
|
214
215
|
---
|
|
215
216
|
|
|
216
217
|
### Required Slide Structure
|
|
@@ -260,7 +261,41 @@ core rules and the visual design below.
|
|
|
260
261
|
|
|
261
262
|
---
|
|
262
263
|
|
|
263
|
-
### Phase
|
|
264
|
+
### Phase 4 — Presentation Plan
|
|
265
|
+
|
|
266
|
+
After all research is complete and findings have been read, present a detailed
|
|
267
|
+
slide plan to the user **before writing any HTML**.
|
|
268
|
+
|
|
269
|
+
Format the plan as a markdown table:
|
|
270
|
+
|
|
271
|
+
| # | Title | Content Summary | Layout | Components |
|
|
272
|
+
|---|-------|-----------------|--------|------------|
|
|
273
|
+
| 1 | Cover | Topic title, subtitle, presenter, date | `cover` | `gradient-text`, `deco-blob`, `accent-line` |
|
|
274
|
+
| 2 | Table of Contents | 5 chapter headings | `toc` | `toc-list` |
|
|
275
|
+
| 3 | Market Background | Key problem, 3 pain points, $4.2B TAM | `two-col` | `evidence-list`, `card` |
|
|
276
|
+
| 4 | Key Metrics | Growth 85%, TAM $12B, NPS 72 | `stats` | `stat-card ×3`, `gradient-text` |
|
|
277
|
+
|
|
278
|
+
Rules for filling the table:
|
|
279
|
+
- **Layout**: use the exact layout name from the Layout Index (e.g. `cover`, `two-col`, `card-grid`, `stats`)
|
|
280
|
+
- **Components**: list component names from the Component Index — no CSS details
|
|
281
|
+
(e.g. `card ×3`, `stat-card`, `evidence-list`, `step-flow`, `quote-block`)
|
|
282
|
+
- **Content Summary**: 1 sentence of actual content — specific numbers, key points, or
|
|
283
|
+
real data from research findings (not vague descriptions like "overview of topic")
|
|
284
|
+
|
|
285
|
+
After the table, add one sentence explaining any notable layout choices if non-obvious.
|
|
286
|
+
|
|
287
|
+
Then ask:
|
|
288
|
+
> "Does this plan look good? I'll generate the HTML once you confirm — or let me know
|
|
289
|
+
> if you'd like to adjust any slide."
|
|
290
|
+
|
|
291
|
+
**Do not write any HTML until the user replies with confirmation.**
|
|
292
|
+
|
|
293
|
+
- On confirmation → proceed to Phase 5
|
|
294
|
+
- On change request → update the table and ask again
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
### Phase 5 — Generate
|
|
264
299
|
|
|
265
300
|
Once you have enough information, generate the complete HTML file in one shot.
|
|
266
301
|
|
|
@@ -270,7 +305,7 @@ Once you have enough information, generate the complete HTML file in one shot.
|
|
|
270
305
|
(e.g. "AI Future" → `slides/ai-future.html`)
|
|
271
306
|
- The file must be completely self-contained (all CSS and JS inline)
|
|
272
307
|
|
|
273
|
-
### Phase
|
|
308
|
+
### Phase 6 — Iterate
|
|
274
309
|
|
|
275
310
|
After generating, briefly tell the user:
|
|
276
311
|
- The filename you wrote (e.g. `slides/ai-future.html`)
|
|
@@ -301,6 +336,42 @@ Follow these rules on every generation. They are non-negotiable.
|
|
|
301
336
|
Never use any other icon library (no Font Awesome, no Heroicons, no Material Icons).
|
|
302
337
|
- All JS methods must be **fully implemented** — no empty stubs, no `// TODO` comments.
|
|
303
338
|
|
|
339
|
+
### Design Compliance — Strict Mode
|
|
340
|
+
|
|
341
|
+
The active design defines a **closed vocabulary** of layouts and components.
|
|
342
|
+
You MUST use ONLY the layouts and components listed in the Layout Index and
|
|
343
|
+
Component Index injected into this prompt.
|
|
344
|
+
|
|
345
|
+
**Layouts:** Every `<section class="slide">` must use exactly one layout class
|
|
346
|
+
from the Layout Index. Do NOT invent custom grid or flex structures.
|
|
347
|
+
|
|
348
|
+
**Components:** Every content block must use a component class from the
|
|
349
|
+
Component Index. Do NOT create novel CSS classes for content elements.
|
|
350
|
+
|
|
351
|
+
**`<style>` block — no new class rules.** The design already provides all
|
|
352
|
+
necessary CSS (foundation, layouts, components). Your `<style>` block should
|
|
353
|
+
contain only CSS rules copied verbatim from the design's sections. Never define
|
|
354
|
+
a CSS class rule (`.my-custom-thing { ... }`) that is not in the design.
|
|
355
|
+
|
|
356
|
+
**Inline `style=""` — minor adjustments only.** Inline styles are permitted
|
|
357
|
+
for fine-tuning spacing and sizing (`margin`, `padding`, `gap`, `font-size`,
|
|
358
|
+
`max-width`, `min-height`, `width`, `height`). They must NOT be used to
|
|
359
|
+
define new visual effects — no custom `background-image`, `box-shadow`,
|
|
360
|
+
`border-radius`, `color`, or layout structures via inline style.
|
|
361
|
+
|
|
362
|
+
**CSS variables:** Use only `var(--xxx)` properties defined in
|
|
363
|
+
`@design:foundation`. Do NOT define new custom properties.
|
|
364
|
+
|
|
365
|
+
**Fetch before use:** Before generating any slide, call the `revela-designs`
|
|
366
|
+
tool to fetch the full HTML/CSS for each layout and component you plan to
|
|
367
|
+
use. Generate HTML that matches the fetched examples exactly.
|
|
368
|
+
|
|
369
|
+
**No suitable component?** Adapt the *content* to fit the closest available
|
|
370
|
+
component — never adapt the component structure to fit content.
|
|
371
|
+
|
|
372
|
+
The QA system will automatically flag any unrecognised CSS class as a
|
|
373
|
+
compliance warning after you write the file.
|
|
374
|
+
|
|
304
375
|
### Inline Editing
|
|
305
376
|
|
|
306
377
|
**Always include inline editing** in every generated presentation. The complete
|
|
@@ -314,8 +385,9 @@ element selector list, and `window.getEditedHTML()` definition.
|
|
|
314
385
|
- Always use the **original** file path in HTML `<img src>` for full-quality rendering
|
|
315
386
|
- Never repeat the same image on multiple slides (logos: title + closing only)
|
|
316
387
|
- Image compression is handled automatically by the server
|
|
317
|
-
- **Use the active design's image components**
|
|
318
|
-
|
|
388
|
+
- **Use the active design's image components** for displaying images — they
|
|
389
|
+
provide proper rounded corners and cropping. Use inline `style=""` only for
|
|
390
|
+
minor sizing adjustments; do not create custom image container classes.
|
|
319
391
|
|
|
320
392
|
### Accessibility
|
|
321
393
|
|
|
@@ -333,13 +405,15 @@ element selector list, and `window.getEditedHTML()` definition.
|
|
|
333
405
|
|
|
334
406
|
### Visual Quality Rules
|
|
335
407
|
|
|
336
|
-
**Layout Diversity** — choose
|
|
337
|
-
default to a bullet list. The active design's
|
|
338
|
-
which components work well for each content
|
|
408
|
+
**Layout Diversity** — choose from the design's defined layouts and components
|
|
409
|
+
based on content type, never default to a bullet list. The active design's
|
|
410
|
+
**Composition Guide** suggests which components work well for each content
|
|
411
|
+
pattern — consult it first.
|
|
339
412
|
|
|
340
413
|
The active design's **Component Library** defines the HTML/CSS for each
|
|
341
414
|
component, and **Layout Primitives** defines the grid/flex patterns for
|
|
342
|
-
arranging them. Combine
|
|
415
|
+
arranging them. Combine the design's defined layouts and components to serve
|
|
416
|
+
the content — never invent new ones.
|
|
343
417
|
|
|
344
418
|
**Visual Hierarchy** — every slide must have exactly 1 dominant visual focal point.
|
|
345
419
|
Forbidden: plain background + unstyled bullet list with zero decorative elements.
|
package/tools/qa.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
12
12
|
import { resolve } from "path"
|
|
13
13
|
import { existsSync } from "fs"
|
|
14
14
|
import { runQA, formatReport } from "../lib/qa"
|
|
15
|
+
import { extractDesignClasses } from "../lib/design/designs"
|
|
15
16
|
|
|
16
17
|
export default tool({
|
|
17
18
|
description:
|
|
@@ -42,7 +43,14 @@ export default tool({
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
+
// Extract design's allowed class vocabulary for compliance checking
|
|
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)
|
|
46
54
|
const formatted = formatReport(report)
|
|
47
55
|
|
|
48
56
|
// Prepend a compact JSON summary for programmatic use if needed
|
package/tools/workspace-scan.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin"
|
|
2
2
|
import { readdirSync, statSync, existsSync } from "fs"
|
|
3
|
-
import { join, relative, extname } from "path"
|
|
3
|
+
import { join, relative, extname, resolve, sep, isAbsolute } from "path"
|
|
4
4
|
|
|
5
5
|
const DOC_EXTENSIONS = new Set([
|
|
6
6
|
".pdf", ".docx", ".doc", ".xlsx", ".xls",
|
|
@@ -113,7 +113,22 @@ export default tool({
|
|
|
113
113
|
async execute(args, context) {
|
|
114
114
|
try {
|
|
115
115
|
const workspaceDir = context.directory ?? process.cwd()
|
|
116
|
-
|
|
116
|
+
|
|
117
|
+
// Validate and resolve scanRoot — must stay within workspaceDir
|
|
118
|
+
let scanRoot = workspaceDir
|
|
119
|
+
if (args.path) {
|
|
120
|
+
if (isAbsolute(args.path)) {
|
|
121
|
+
return JSON.stringify({ error: "path must be relative to workspace root" })
|
|
122
|
+
}
|
|
123
|
+
const candidate = join(workspaceDir, args.path)
|
|
124
|
+
const resolvedCandidate = resolve(candidate)
|
|
125
|
+
const resolvedWorkspace = resolve(workspaceDir)
|
|
126
|
+
if (resolvedCandidate !== resolvedWorkspace && !resolvedCandidate.startsWith(resolvedWorkspace + sep)) {
|
|
127
|
+
return JSON.stringify({ error: "path must be within workspace" })
|
|
128
|
+
}
|
|
129
|
+
scanRoot = candidate
|
|
130
|
+
}
|
|
131
|
+
|
|
117
132
|
const maxDepth = args.max_depth ?? 6
|
|
118
133
|
|
|
119
134
|
if (!existsSync(scanRoot)) {
|