@cyber-dash-tech/revela 0.1.2 → 0.1.3

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 CHANGED
@@ -148,7 +148,7 @@ Checks performed on every slide:
148
148
  | **Density imbalance** | Columns where CSS `align-items: stretch` hides content imbalance |
149
149
  | **Sparse** | Slides with too few visible elements |
150
150
 
151
- Cover, TOC, divider, summary, and closing slides are automatically exempted from fill/spacing checks via the `data-slide-type` attribute.
151
+ Structural slides (cover, TOC, quote, summary, closing) set `slide-qa="false"` and are automatically exempted from fill/spacing checks. Content-heavy slides set `slide-qa="true"` to opt in.
152
152
 
153
153
  You can also invoke QA manually: ask the AI to "run QA on slides/my-deck.html" or use the `revela-qa` tool directly.
154
154
 
package/README.zh-CN.md CHANGED
@@ -179,7 +179,7 @@ OPENCODE_ENABLE_EXA=1 opencode
179
179
  | **密度失衡** | CSS `align-items: stretch` 列布局中隐藏的内容不平衡 |
180
180
  | **稀疏** | 可见元素过少的幻灯片 |
181
181
 
182
- 封面、目录、分隔、总结、结语幻灯片通过 `data-slide-type` 属性自动豁免填充/间距检查。
182
+ 结构性幻灯片(封面、目录、引言、总结、结语)设置 `slide-qa="false"`,自动豁免填充/间距检查。内容型幻灯片设置 `slide-qa="true"` 启用 QA 检查。
183
183
 
184
184
  也可以手动触发:让 AI "对 slides/my-deck.html 运行 QA",或直接使用 `revela-qa` 工具。
185
185
 
package/lib/config.ts CHANGED
@@ -28,7 +28,7 @@ export const CONFIG_FILE = join(CONFIG_DIR, "config.json")
28
28
  export const ACTIVE_PROMPT_FILE = join(CONFIG_DIR, "_active-prompt.md")
29
29
 
30
30
  /** Default design name. */
31
- export const DEFAULT_DESIGN = "default"
31
+ export const DEFAULT_DESIGN = "aurora"
32
32
 
33
33
  /** Default domain name. */
34
34
  export const DEFAULT_DOMAIN = "general"
@@ -145,28 +145,41 @@ export function getDesignSkillMd(name?: string): string {
145
145
  // Marker-based section / component parsing
146
146
  // ---------------------------------------------------------------------------
147
147
 
148
+ export interface LayoutInfo {
149
+ /** Full text content of the layout block (without marker lines). */
150
+ content: string
151
+ /** Whether this layout type should be QA-checked for balance/rhythm. */
152
+ qa: boolean
153
+ }
154
+
148
155
  export interface DesignSections {
149
- /** Map of section name → extracted content (without marker lines). */
156
+ /** Map of @design:<name> section → extracted content (without marker lines). */
150
157
  sections: Record<string, string>
151
- /** Map of component name → extracted content (without marker lines). */
158
+ /** Map of @layout:<name>LayoutInfo with content + qa flag. */
159
+ layouts: Record<string, LayoutInfo>
160
+ /** Map of @component:<name> → extracted content (without marker lines). */
152
161
  components: Record<string, string>
153
162
  /** Whether the DESIGN.md has any markers at all. */
154
163
  hasMarkers: boolean
155
164
  }
156
165
 
157
166
  /**
158
- * Parse a DESIGN.md body (no frontmatter) into sections and components
159
- * using the two-layer HTML comment marker convention:
160
- * <!-- @section:<name>:start --> … <!-- @section:<name>:end -->
167
+ * Parse a DESIGN.md body (no frontmatter) into sections, layouts, and components
168
+ * using the three-layer HTML comment marker convention:
169
+ * <!-- @design:<name>:start --> … <!-- @design:<name>:end -->
170
+ * <!-- @layout:<name>:start qa=true|false --> … <!-- @layout:<name>:end -->
161
171
  * <!-- @component:<name>:start --> … <!-- @component:<name>:end -->
162
172
  *
173
+ * The `qa` attribute on layout markers defaults to `true` when omitted.
163
174
  * Returns an object with empty maps and hasMarkers=false when no markers found.
164
175
  */
165
176
  export function parseDesignSections(body: string): DesignSections {
166
177
  const sections: Record<string, string> = {}
178
+ const layouts: Record<string, LayoutInfo> = {}
167
179
  const components: Record<string, string> = {}
168
180
 
169
- const sectionRe = /<!--\s*@section:(\w[\w-]*):start\s*-->([\s\S]*?)<!--\s*@section:\1:end\s*-->/g
181
+ const sectionRe = /<!--\s*@design:(\w[\w-]*):start\s*-->([\s\S]*?)<!--\s*@design:\1:end\s*-->/g
182
+ const layoutRe = /<!--\s*@layout:(\w[\w-]*):start(?:\s+qa=(true|false))?\s*-->([\s\S]*?)<!--\s*@layout:\1:end\s*-->/g
170
183
  const componentRe = /<!--\s*@component:(\w[\w-]*):start\s*-->([\s\S]*?)<!--\s*@component:\1:end\s*-->/g
171
184
 
172
185
  let hasMarkers = false
@@ -177,12 +190,20 @@ export function parseDesignSections(body: string): DesignSections {
177
190
  sections[match[1]] = match[2].trim()
178
191
  }
179
192
 
193
+ while ((match = layoutRe.exec(body)) !== null) {
194
+ hasMarkers = true
195
+ const qaAttr = match[2]
196
+ // qa defaults to true when attribute is omitted
197
+ const qa = qaAttr === "false" ? false : true
198
+ layouts[match[1]] = { content: match[3].trim(), qa }
199
+ }
200
+
180
201
  while ((match = componentRe.exec(body)) !== null) {
181
202
  hasMarkers = true
182
203
  components[match[1]] = match[2].trim()
183
204
  }
184
205
 
185
- return { sections, components, hasMarkers }
206
+ return { sections, layouts, components, hasMarkers }
186
207
  }
187
208
 
188
209
  /**
@@ -219,6 +240,75 @@ export function generateComponentIndex(components: Record<string, string>): stri
219
240
  ].join("\n")
220
241
  }
221
242
 
243
+ /**
244
+ * Generate a compact Layout Index table from parsed layouts.
245
+ * Lists each layout name with the QA flag and a one-line description.
246
+ */
247
+ export function generateLayoutIndex(layouts: Record<string, LayoutInfo>): string {
248
+ const names = Object.keys(layouts)
249
+ if (names.length === 0) return ""
250
+
251
+ const rows = names.map((name) => {
252
+ const { content, qa } = layouts[name]
253
+ const firstLine = content
254
+ .split("\n")
255
+ .map((l) => l.trim())
256
+ .find((l) => l && !l.startsWith("<!--") && !l.startsWith("```"))
257
+ const desc = firstLine
258
+ ? firstLine.replace(/^#+\s*/, "").replace(/\(.*?\)/, "").trim()
259
+ : ""
260
+ const qaIcon = qa ? "✓" : "—"
261
+ return `| \`${name}\` | ${qaIcon} | ${desc} |`
262
+ })
263
+
264
+ return [
265
+ "### Layout Index",
266
+ "",
267
+ "| Layout | QA | Description |",
268
+ "|---|---|---|",
269
+ ...rows,
270
+ "",
271
+ "_Use `revela-designs` tool with `action: \"read\"` and `layout: \"<name>\"` to get full HTML/CSS for any layout._",
272
+ ].join("\n")
273
+ }
274
+
275
+ /**
276
+ * Get the raw text of one or more named layouts from a DESIGN.md.
277
+ * @param layoutNames - Comma-separated layout names or an array.
278
+ * @param designName - Design to read from (defaults to active).
279
+ */
280
+ export function getDesignLayout(
281
+ layoutNames: string | string[],
282
+ designName?: string,
283
+ ): string {
284
+ const name = designName || activeDesign()
285
+ const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
286
+ if (!existsSync(mdPath)) {
287
+ throw new Error(`Design '${name}' is not installed`)
288
+ }
289
+ const text = readFileSync(mdPath, "utf-8")
290
+ const { body } = parseFrontmatter(text)
291
+ const { layouts, hasMarkers } = parseDesignSections(body)
292
+
293
+ if (!hasMarkers) {
294
+ throw new Error(`Design '${name}' has no markers — use getDesignSkillMd() for full text`)
295
+ }
296
+
297
+ const names = Array.isArray(layoutNames)
298
+ ? layoutNames
299
+ : layoutNames.split(",").map((s) => s.trim())
300
+
301
+ const parts: string[] = []
302
+ for (const layoutName of names) {
303
+ if (!(layoutName in layouts)) {
304
+ throw new Error(`Layout '${layoutName}' not found in design '${name}'`)
305
+ }
306
+ const { content, qa } = layouts[layoutName]
307
+ parts.push(`### Layout: ${layoutName} (qa=${qa})\n\n${content}`)
308
+ }
309
+ return parts.join("\n\n---\n\n")
310
+ }
311
+
222
312
  /**
223
313
  * Get the raw text of a named section from a DESIGN.md.
224
314
  * Throws if the design is not installed or the section doesn't exist.
@@ -29,10 +29,10 @@ import {
29
29
  getDesignSkillMd,
30
30
  parseDesignSections,
31
31
  generateComponentIndex,
32
+ generateLayoutIndex,
32
33
  } from "./design/designs"
33
34
  import { activeDomain, getDomainSkillMd } from "./domain/domains"
34
35
  import { parseFrontmatter } from "./frontmatter"
35
- import { SLIDE_TYPES, type SlideType } from "./qa/checks"
36
36
  import { childLog } from "./log"
37
37
 
38
38
  const promptLog = childLog("prompt-builder")
@@ -40,21 +40,6 @@ const promptLog = childLog("prompt-builder")
40
40
  /** Path to SKILL.md shipped with this package. */
41
41
  const SKILL_MD_PATH = resolve(__dirname, "..", "skill", "SKILL.md")
42
42
 
43
- /**
44
- * Human-readable descriptions for each slide type.
45
- * Used to generate the data-slide-type table injected into SKILL.md.
46
- * Kept here (not in checks.ts) — these are prose for the AI, not QA logic.
47
- */
48
- const SLIDE_TYPE_DESCRIPTIONS: Record<SlideType, string> = {
49
- cover: "Title / opening slide",
50
- toc: "Table of contents",
51
- content: "Regular content slides (default)",
52
- summary: "Key takeaways slide",
53
- closing: "Thank-you / Q&A / contact slide",
54
- divider: "Section-break / transition slide",
55
- "thank-you": "Alias for `closing`",
56
- }
57
-
58
43
  /**
59
44
  * Build the combined system prompt and write it to _active-prompt.md.
60
45
  *
@@ -66,9 +51,8 @@ export function buildPrompt(designName?: string, domainName?: string): string {
66
51
  const design = designName || activeDesign()
67
52
  const domain = domainName || activeDomain()
68
53
 
69
- // Layer 1 — SKILL.md (with dynamic slide-type table injected)
70
- let coreSkill = readFileSync(SKILL_MD_PATH, "utf-8")
71
- coreSkill = injectSlideTypes(coreSkill)
54
+ // Layer 1 — SKILL.md
55
+ const coreSkill = readFileSync(SKILL_MD_PATH, "utf-8")
72
56
 
73
57
  // Check for preview.html
74
58
  const designDir = join(DESIGNS_DIR, design)
@@ -121,13 +105,14 @@ export function buildPrompt(designName?: string, domainName?: string): string {
121
105
  * Build the design layer text.
122
106
  *
123
107
  * If the DESIGN.md has markers:
124
- * - Always include @section:global
125
- * - Always include @section:layouts (layout primitives — always needed)
126
- * - Include a generated Component Index table (lightweight catalog)
127
- * - Omit @section:components detail, @section:charts, @section:guide
108
+ * - Always include @design:foundation (colors, fonts, CSS, JS, HTML skeleton)
109
+ * - Always include @design:rules (composition rules, do/don't — always resident)
110
+ * - Always include generated Layout Index (with QA column)
111
+ * - Always include generated Component Index
112
+ * - Omit individual layout details, component details, @design:chart-rules
128
113
  * (available on demand via revela-designs tool)
129
114
  *
130
- * If no markers: return the full DESIGN.md body unchanged.
115
+ * If no markers: return the full DESIGN.md body unchanged (backward compat).
131
116
  */
132
117
  function buildDesignLayer(designName: string): string {
133
118
  const mdPath = join(DESIGNS_DIR, designName, "DESIGN.md")
@@ -137,7 +122,7 @@ function buildDesignLayer(designName: string): string {
137
122
 
138
123
  const raw = readFileSync(mdPath, "utf-8")
139
124
  const { body } = parseFrontmatter(raw)
140
- const { sections, components, hasMarkers } = parseDesignSections(body)
125
+ const { sections, layouts, components, hasMarkers } = parseDesignSections(body)
141
126
 
142
127
  if (!hasMarkers) {
143
128
  // Backward-compatible: full text injection
@@ -146,23 +131,29 @@ function buildDesignLayer(designName: string): string {
146
131
 
147
132
  const layerParts: string[] = []
148
133
 
149
- // 1. Global section (colors, typography, CSS, JS class, HTML structure)
150
- if (sections["global"]) {
151
- layerParts.push(sections["global"])
134
+ // 1. Foundation section (colors, typography, CSS vars, JS, HTML skeleton)
135
+ if (sections["foundation"]) {
136
+ layerParts.push(sections["foundation"])
137
+ }
138
+
139
+ // 2. Rules section (composition rules, do/don't — always resident)
140
+ if (sections["rules"]) {
141
+ layerParts.push(sections["rules"])
152
142
  }
153
143
 
154
- // 2. Component Index — compact catalog
155
- const index = generateComponentIndex(components)
156
- if (index) {
157
- layerParts.push(index)
144
+ // 3. Layout Index — compact catalog with QA column
145
+ const layoutIndex = generateLayoutIndex(layouts)
146
+ if (layoutIndex) {
147
+ layerParts.push(layoutIndex)
158
148
  }
159
149
 
160
- // 3. Layouts sectionalways resident (needed for every slide)
161
- if (sections["layouts"]) {
162
- layerParts.push(sections["layouts"])
150
+ // 4. Component Indexcompact catalog
151
+ const componentIndex = generateComponentIndex(components)
152
+ if (componentIndex) {
153
+ layerParts.push(componentIndex)
163
154
  }
164
155
 
165
- // 4. On-demand note
156
+ // 5. On-demand note
166
157
  layerParts.push(
167
158
  [
168
159
  "### On-Demand Design Sections",
@@ -172,23 +163,11 @@ function buildDesignLayer(designName: string): string {
172
163
  "",
173
164
  "| Section | Fetch with |",
174
165
  "|---|---|",
166
+ "| Layout HTML/CSS details | `layout: \"<name>\"` (see Layout Index above) |",
175
167
  "| Component CSS/HTML details | `component: \"<name>\"` (see Component Index above) |",
176
- "| Data Visualization (ECharts) | `section: \"charts\"` |",
177
- "| Composition Guide & Do/Don't | `section: \"guide\"` |",
168
+ "| Data Visualization (ECharts) | `section: \"chart-rules\"` |",
178
169
  ].join("\n"),
179
170
  )
180
171
 
181
172
  return layerParts.join("\n\n---\n\n")
182
173
  }
183
-
184
- /**
185
- * Replace the <!-- @slide-types --> placeholder in SKILL.md with a generated
186
- * markdown table built from SLIDE_TYPES (single source of truth in checks.ts).
187
- */
188
- function injectSlideTypes(skillMd: string): string {
189
- const rows = SLIDE_TYPES.map(
190
- (t) => `| \`${t}\` | ${SLIDE_TYPE_DESCRIPTIONS[t]} |`,
191
- )
192
- const table = ["| Value | Use for |", "|-------|---------|", ...rows].join("\n")
193
- return skillMd.replace("<!-- @slide-types -->", table)
194
- }
package/lib/qa/checks.ts CHANGED
@@ -47,41 +47,6 @@ export interface QAReport {
47
47
  summary: string
48
48
  }
49
49
 
50
- // ── Slide type registry — single source of truth ──────────────────────────────
51
-
52
- /**
53
- * All valid values for the `data-slide-type` attribute on `<section class="slide">`.
54
- *
55
- * Single source of truth consumed by:
56
- * - QA checks (EXEMPT_TYPES below)
57
- * - prompt-builder.ts (injected into SKILL.md via <!-- @slide-types --> placeholder)
58
- */
59
- export const SLIDE_TYPES = [
60
- "cover",
61
- "toc",
62
- "content",
63
- "summary",
64
- "closing",
65
- "divider",
66
- "thank-you",
67
- ] as const
68
-
69
- export type SlideType = (typeof SLIDE_TYPES)[number]
70
-
71
- /**
72
- * Slide types that are intentionally sparse, centred, or structurally
73
- * different from "content" slides. Balance and rhythm checks are skipped
74
- * for these types (overflow and symmetry still apply).
75
- */
76
- export const EXEMPT_TYPES: ReadonlySet<string> = new Set<SlideType>([
77
- "cover",
78
- "toc",
79
- "closing",
80
- "divider",
81
- "summary",
82
- "thank-you",
83
- ])
84
-
85
50
  // ── Thresholds ────────────────────────────────────────────────────────────────
86
51
 
87
52
  const T = {
@@ -241,7 +206,8 @@ function checkOverflow(metrics: SlideMetrics): LayoutIssue[] {
241
206
 
242
207
  /**
243
208
  * Check 2: Balance — content centroid, bottom gap, sparsity.
244
- * Skipped for EXEMPT_TYPES (cover, closing, etc.).
209
+ * Only runs when `metrics.slideQa` is true (content-heavy layouts).
210
+ * Structural/sparse slides (cover, closing, etc.) set slide-qa="false" and are skipped.
245
211
  *
246
212
  * Sub-checks:
247
213
  * - centroid_offset: weighted centroid deviates too far from canvas centre
@@ -263,17 +229,8 @@ function checkBalance(metrics: SlideMetrics): LayoutIssue[] {
263
229
  return issues
264
230
  }
265
231
 
266
- // Exempt structural slides
267
- if (metrics.slideType && EXEMPT_TYPES.has(metrics.slideType)) return []
268
-
269
- // Geometry fallback for old HTML without data-slide-type:
270
- // detect cover-like slides (single centred column)
271
- const contentCenterX = (contentRect.left + contentRect.right) / 2
272
- const canvasCenterX = canvasRect.width / 2
273
- const centerOffsetFrac = Math.abs(contentCenterX - canvasCenterX) / canvasRect.width
274
- const maxElemWidth = Math.max(...elements.map((e) => e.rect.width))
275
- const isCoverLike = centerOffsetFrac < 0.15 && maxElemWidth < canvasRect.width * 0.65
276
- if (isCoverLike) return []
232
+ // Skip balance checks for structural/sparse slides (slide-qa="false")
233
+ if (!metrics.slideQa) return []
277
234
 
278
235
  // ── Sub-check: sparse ────────────────────────────────────────────────────
279
236
  const visibleCount = elements.filter((e) => e.visible).length
@@ -500,8 +457,8 @@ function cv(values: number[]): number {
500
457
  function checkRhythm(metrics: SlideMetrics): LayoutIssue[] {
501
458
  const issues: LayoutIssue[] = []
502
459
 
503
- // Exempt structural slides
504
- if (metrics.slideType && EXEMPT_TYPES.has(metrics.slideType)) return []
460
+ // Skip rhythm checks for structural/sparse slides (slide-qa="false")
461
+ if (!metrics.slideQa) return []
505
462
 
506
463
  function checkContainer(els: ElementInfo[], containerSelector?: string) {
507
464
  if (els.length < 2) return
package/lib/qa/measure.ts CHANGED
@@ -55,11 +55,12 @@ export interface SlideMetrics {
55
55
  /** slide title extracted from the first h1/h2 inside the slide */
56
56
  title: string
57
57
  /**
58
- * Structural role from the slide's `data-slide-type` attribute.
59
- * Valid values: "cover", "toc", "content", "closing", "divider", "summary".
60
- * Undefined when the attribute is absent (old/third-party HTML).
58
+ * Whether this slide should be included in layout QA checks.
59
+ * Read from the `slide-qa` attribute on `<section class="slide">`.
60
+ * Defaults to `false` when the attribute is absent.
61
+ * Content-heavy layouts set `slide-qa="true"`; structural/sparse slides omit or use `"false"`.
61
62
  */
62
- slideType?: string
63
+ slideQa: boolean
63
64
  /** bounding box of the slide-canvas element itself (post-scale) */
64
65
  canvasRect: Rect
65
66
  /** top-level visible children of .slide-canvas */
@@ -238,8 +239,8 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
238
239
  const slide = document.querySelectorAll(".slide")[slideIdx]
239
240
  if (!slide) return null
240
241
 
241
- // Read the semantic slide type if the author provided it
242
- const slideType = (slide as HTMLElement).dataset.slideType || slide.getAttribute("data-slide-type") || undefined
242
+ // Read the QA flag true means this slide gets balance/rhythm checks
243
+ const slideQa = (slide as HTMLElement).getAttribute("slide-qa") === "true"
243
244
 
244
245
  const canvas = slide.querySelector(".slide-canvas") as HTMLElement | null
245
246
  if (!canvas) return null
@@ -268,7 +269,7 @@ export async function measureSlides(htmlFilePath: string): Promise<SlideMetrics[
268
269
  return {
269
270
  index: slideIdx,
270
271
  title,
271
- slideType,
272
+ slideQa,
272
273
  canvasRect,
273
274
  elements,
274
275
  contentRect: unionRect(elements),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",