@cyber-dash-tech/revela 0.1.4 → 0.1.6
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/commands/designs.ts +18 -1
- package/lib/commands/domains.ts +18 -1
- package/lib/commands/help.ts +3 -1
- package/lib/design/designs.ts +158 -0
- 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 +19 -1
- package/skill/SKILL.md +102 -29
- package/tools/qa.ts +9 -1
package/lib/commands/designs.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* /revela designs — list installed designs
|
|
6
6
|
* /revela designs <name> — activate a design
|
|
7
7
|
* /revela designs-add <url> — install a design from URL / github:user/repo / local path
|
|
8
|
+
* /revela designs-rm <name> — remove an installed design
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import { listDesigns, activeDesign, activateDesign, installDesign } from "../design/designs"
|
|
11
|
+
import { listDesigns, activeDesign, activateDesign, installDesign, removeDesign } from "../design/designs"
|
|
11
12
|
import { buildPrompt } from "../prompt-builder"
|
|
12
13
|
|
|
13
14
|
export async function handleDesignsList(
|
|
@@ -57,3 +58,19 @@ export async function handleDesignsAdd(
|
|
|
57
58
|
await send(`**Install failed:** ${e.message}`)
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
export async function handleDesignsRemove(
|
|
63
|
+
name: string,
|
|
64
|
+
send: (text: string) => Promise<void>,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (!name) {
|
|
67
|
+
await send(`**Usage:** \`/revela designs-rm <name>\``)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
removeDesign(name)
|
|
72
|
+
await send(`**Design removed:** \`${name}\``)
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
await send(`**Error:** ${e.message}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
package/lib/commands/domains.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* /revela domains — list installed domains
|
|
6
6
|
* /revela domains <name> — activate a domain
|
|
7
7
|
* /revela domains-add <url> — install a domain from URL / github:user/repo / local path
|
|
8
|
+
* /revela domains-rm <name> — remove an installed domain
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
|
-
import { listDomains, activeDomain, activateDomain, installDomain } from "../domain/domains"
|
|
11
|
+
import { listDomains, activeDomain, activateDomain, installDomain, removeDomain } from "../domain/domains"
|
|
11
12
|
import { buildPrompt } from "../prompt-builder"
|
|
12
13
|
|
|
13
14
|
export async function handleDomainsList(
|
|
@@ -57,3 +58,19 @@ export async function handleDomainsAdd(
|
|
|
57
58
|
await send(`**Install failed:** ${e.message}`)
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
|
|
62
|
+
export async function handleDomainsRemove(
|
|
63
|
+
name: string,
|
|
64
|
+
send: (text: string) => Promise<void>,
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (!name) {
|
|
67
|
+
await send(`**Usage:** \`/revela domains-rm <name>\``)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
removeDomain(name)
|
|
72
|
+
await send(`**Domain removed:** \`${name}\``)
|
|
73
|
+
} catch (e: any) {
|
|
74
|
+
await send(`**Error:** ${e.message}`)
|
|
75
|
+
}
|
|
76
|
+
}
|
package/lib/commands/help.ts
CHANGED
|
@@ -30,6 +30,8 @@ export async function handleHelp(
|
|
|
30
30
|
`\`/revela domains\` — list installed domains\n` +
|
|
31
31
|
`\`/revela domains <name>\` — activate a domain\n` +
|
|
32
32
|
`\`/revela designs-add <url>\` — install a design from URL / github:user/repo\n` +
|
|
33
|
-
`\`/revela domains-add <url>\` — install a domain from URL / github:user/repo`
|
|
33
|
+
`\`/revela domains-add <url>\` — install a domain from URL / github:user/repo\n` +
|
|
34
|
+
`\`/revela designs-rm <name>\` — remove an installed design\n` +
|
|
35
|
+
`\`/revela domains-rm <name>\` — remove an installed domain`
|
|
34
36
|
)
|
|
35
37
|
}
|
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/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
|
@@ -35,11 +35,13 @@ import {
|
|
|
35
35
|
handleDesignsList,
|
|
36
36
|
handleDesignsActivate,
|
|
37
37
|
handleDesignsAdd,
|
|
38
|
+
handleDesignsRemove,
|
|
38
39
|
} from "./lib/commands/designs"
|
|
39
40
|
import {
|
|
40
41
|
handleDomainsList,
|
|
41
42
|
handleDomainsActivate,
|
|
42
43
|
handleDomainsAdd,
|
|
44
|
+
handleDomainsRemove,
|
|
43
45
|
} from "./lib/commands/domains"
|
|
44
46
|
import designsTool from "./tools/designs"
|
|
45
47
|
import domainsTool from "./tools/domains"
|
|
@@ -48,6 +50,7 @@ import workspaceScanTool from "./tools/workspace-scan"
|
|
|
48
50
|
import qaTool from "./tools/qa"
|
|
49
51
|
import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
|
|
50
52
|
import { runQA, formatReport } from "./lib/qa"
|
|
53
|
+
import { extractDesignClasses } from "./lib/design/designs"
|
|
51
54
|
import { log, childLog } from "./lib/log"
|
|
52
55
|
|
|
53
56
|
// OpenCode internal agent signatures — used to skip system prompt injection
|
|
@@ -187,6 +190,14 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
187
190
|
await handleDomainsAdd(param, send)
|
|
188
191
|
throw new Error("__REVELA_DOMAINS_ADD_HANDLED__")
|
|
189
192
|
}
|
|
193
|
+
if (sub === "designs-rm") {
|
|
194
|
+
await handleDesignsRemove(param, send)
|
|
195
|
+
throw new Error("__REVELA_DESIGNS_RM_HANDLED__")
|
|
196
|
+
}
|
|
197
|
+
if (sub === "domains-rm") {
|
|
198
|
+
await handleDomainsRemove(param, send)
|
|
199
|
+
throw new Error("__REVELA_DOMAINS_RM_HANDLED__")
|
|
200
|
+
}
|
|
190
201
|
|
|
191
202
|
await send(`**Unknown sub-command:** \`${sub}\`\nRun \`/revela\` to see available commands.`)
|
|
192
203
|
throw new Error("__REVELA_UNKNOWN_HANDLED__")
|
|
@@ -339,7 +350,14 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
339
350
|
if (!filePath.match(/slides\/[^/]+\.html$/)) return
|
|
340
351
|
|
|
341
352
|
try {
|
|
342
|
-
|
|
353
|
+
// Extract design's allowed class vocabulary for compliance checking
|
|
354
|
+
let vocabulary
|
|
355
|
+
try {
|
|
356
|
+
vocabulary = extractDesignClasses()
|
|
357
|
+
} catch {
|
|
358
|
+
// Design may not be installed or may have no markers — skip compliance
|
|
359
|
+
}
|
|
360
|
+
const report = await runQA(filePath, vocabulary)
|
|
343
361
|
// Only append QA report to tool output if there are issues
|
|
344
362
|
if (report.totalIssues > 0) {
|
|
345
363
|
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,7 +119,7 @@ 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
|
|
@@ -161,7 +161,8 @@ List and read the findings files: `ls researches/{topic-slug}/`, then `read`
|
|
|
161
161
|
each `.md` file. Each file contains structured `## Data`, `## Cases`,
|
|
162
162
|
`## Images`, and `## Gaps` sections — use these directly as slide material.
|
|
163
163
|
Cross-reference agent findings with workspace documents (Layer 1). Flag any
|
|
164
|
-
contradictions.
|
|
164
|
+
contradictions. Once all findings are read, proceed to Phase 4 to present the
|
|
165
|
+
slide plan.
|
|
165
166
|
|
|
166
167
|
**Anti-pattern — NEVER do this:**
|
|
167
168
|
- Do NOT use `websearch` directly — it is blocked by the Revela plugin;
|
|
@@ -172,23 +173,23 @@ contradictions.
|
|
|
172
173
|
|
|
173
174
|
---
|
|
174
175
|
|
|
175
|
-
#### Layer
|
|
176
|
+
#### Layer 3 — AI Knowledge (Supplementary)
|
|
176
177
|
|
|
177
|
-
After Layer 1 and Layer 2
|
|
178
|
+
After Layer 1 and Layer 2 results are in, use your training data to fill
|
|
178
179
|
remaining gaps: industry context, historical background, technical explanations.
|
|
179
180
|
|
|
180
181
|
**Critical:** Always mark AI-sourced information with
|
|
181
182
|
`[Source: AI 公开知识,建议核实]`. Never present AI knowledge as verified fact.
|
|
182
183
|
|
|
183
184
|
This layer is supplementary — it adds context around the hard data from
|
|
184
|
-
Layers 1 and 2.
|
|
185
|
+
Layers 1 and 2. It must never be the primary source for quantitative claims
|
|
185
186
|
(market size, revenue, growth rates, etc.).
|
|
186
187
|
|
|
187
188
|
---
|
|
188
189
|
|
|
189
|
-
#### Layer
|
|
190
|
+
#### Layer 4 — Ask the User (Last Resort Only)
|
|
190
191
|
|
|
191
|
-
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.
|
|
192
193
|
When asking, first report what you already know:
|
|
193
194
|
|
|
194
195
|
> 我已从 workspace 文档和在线调研中获取了以下信息:
|
|
@@ -211,7 +212,6 @@ When asking, first report what you already know:
|
|
|
211
212
|
- **ALWAYS** decompose the topic into independent axes before launching agents
|
|
212
213
|
- **ALWAYS** read each `researches/{slug}/{axis}.md` after agents complete
|
|
213
214
|
- Use the `read` tool for all file types — binary formats are handled transparently
|
|
214
|
-
|
|
215
215
|
---
|
|
216
216
|
|
|
217
217
|
### Required Slide Structure
|
|
@@ -261,7 +261,41 @@ core rules and the visual design below.
|
|
|
261
261
|
|
|
262
262
|
---
|
|
263
263
|
|
|
264
|
-
### 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
|
|
265
299
|
|
|
266
300
|
Once you have enough information, generate the complete HTML file in one shot.
|
|
267
301
|
|
|
@@ -271,7 +305,7 @@ Once you have enough information, generate the complete HTML file in one shot.
|
|
|
271
305
|
(e.g. "AI Future" → `slides/ai-future.html`)
|
|
272
306
|
- The file must be completely self-contained (all CSS and JS inline)
|
|
273
307
|
|
|
274
|
-
### Phase
|
|
308
|
+
### Phase 6 — Iterate
|
|
275
309
|
|
|
276
310
|
After generating, briefly tell the user:
|
|
277
311
|
- The filename you wrote (e.g. `slides/ai-future.html`)
|
|
@@ -302,6 +336,42 @@ Follow these rules on every generation. They are non-negotiable.
|
|
|
302
336
|
Never use any other icon library (no Font Awesome, no Heroicons, no Material Icons).
|
|
303
337
|
- All JS methods must be **fully implemented** — no empty stubs, no `// TODO` comments.
|
|
304
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
|
+
|
|
305
375
|
### Inline Editing
|
|
306
376
|
|
|
307
377
|
**Always include inline editing** in every generated presentation. The complete
|
|
@@ -315,8 +385,9 @@ element selector list, and `window.getEditedHTML()` definition.
|
|
|
315
385
|
- Always use the **original** file path in HTML `<img src>` for full-quality rendering
|
|
316
386
|
- Never repeat the same image on multiple slides (logos: title + closing only)
|
|
317
387
|
- Image compression is handled automatically by the server
|
|
318
|
-
- **Use the active design's image components**
|
|
319
|
-
|
|
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.
|
|
320
391
|
|
|
321
392
|
### Accessibility
|
|
322
393
|
|
|
@@ -334,13 +405,15 @@ element selector list, and `window.getEditedHTML()` definition.
|
|
|
334
405
|
|
|
335
406
|
### Visual Quality Rules
|
|
336
407
|
|
|
337
|
-
**Layout Diversity** — choose
|
|
338
|
-
default to a bullet list. The active design's
|
|
339
|
-
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.
|
|
340
412
|
|
|
341
413
|
The active design's **Component Library** defines the HTML/CSS for each
|
|
342
414
|
component, and **Layout Primitives** defines the grid/flex patterns for
|
|
343
|
-
arranging them. Combine
|
|
415
|
+
arranging them. Combine the design's defined layouts and components to serve
|
|
416
|
+
the content — never invent new ones.
|
|
344
417
|
|
|
345
418
|
**Visual Hierarchy** — every slide must have exactly 1 dominant visual focal point.
|
|
346
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
|