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