@cyber-dash-tech/revela 0.1.2 → 0.1.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 +3 -4
- package/README.zh-CN.md +3 -4
- package/lib/agents/research-prompt.ts +7 -3
- package/lib/config.ts +1 -1
- package/lib/design/designs.ts +97 -7
- package/lib/log.ts +3 -2
- package/lib/prompt-builder.ts +29 -50
- package/lib/qa/checks.ts +6 -49
- package/lib/qa/measure.ts +8 -7
- package/package.json +1 -1
- package/plugin.ts +15 -14
- package/skill/SKILL.md +23 -198
- package/tools/designs.ts +21 -5
- package/tools/workspace-scan.ts +17 -2
- package/designs/default/DESIGN.md +0 -1100
- package/designs/editorial-ribbon/DESIGN.md +0 -1092
- package/designs/minimal/DESIGN.md +0 -1079
package/README.md
CHANGED
|
@@ -102,9 +102,8 @@ Three designs are bundled. Switch with `/revela designs <name>`.
|
|
|
102
102
|
|
|
103
103
|
| Name | Description | Preview |
|
|
104
104
|
|---|---|---|
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `editorial-ribbon` | Bold editorial layout — accent ribbons, strong headlines, high visual impact |  |
|
|
105
|
+
| `aurora` | Dark executive style — deep navy/slate, sharp typography, ECharts data visualization |  |
|
|
106
|
+
| `summit` | Editorial outdoor annual-report theme |  |
|
|
108
107
|
|
|
109
108
|
---
|
|
110
109
|
|
|
@@ -148,7 +147,7 @@ Checks performed on every slide:
|
|
|
148
147
|
| **Density imbalance** | Columns where CSS `align-items: stretch` hides content imbalance |
|
|
149
148
|
| **Sparse** | Slides with too few visible elements |
|
|
150
149
|
|
|
151
|
-
|
|
150
|
+
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
151
|
|
|
153
152
|
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
153
|
|
package/README.zh-CN.md
CHANGED
|
@@ -133,9 +133,8 @@ OPENCODE_ENABLE_EXA=1 opencode
|
|
|
133
133
|
|
|
134
134
|
| 名称 | 说明 | 预览 |
|
|
135
135
|
|---|---|---|
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `editorial-ribbon` | 大胆的编辑版式 —— 强调色横幅,醒目标题,高视觉冲击力 |  |
|
|
136
|
+
| `aurora` | 颜色主题 — 极光, 高饱和度, ECharts 数据可视化 |  |
|
|
137
|
+
| `summit` | 极简主义 - 户外,适合有丰富插图,Echart 数据可视化 |  |
|
|
139
138
|
|
|
140
139
|
---
|
|
141
140
|
|
|
@@ -179,7 +178,7 @@ OPENCODE_ENABLE_EXA=1 opencode
|
|
|
179
178
|
| **密度失衡** | CSS `align-items: stretch` 列布局中隐藏的内容不平衡 |
|
|
180
179
|
| **稀疏** | 可见元素过少的幻灯片 |
|
|
181
180
|
|
|
182
|
-
|
|
181
|
+
结构性幻灯片(封面、目录、引言、总结、结语)设置 `slide-qa="false"`,自动豁免填充/间距检查。内容型幻灯片设置 `slide-qa="true"` 启用 QA 检查。
|
|
183
182
|
|
|
184
183
|
也可以手动触发:让 AI "对 slides/my-deck.html 运行 QA",或直接使用 `revela-qa` 工具。
|
|
185
184
|
|
|
@@ -53,9 +53,13 @@ Formulate **3–6 targeted search queries** for your specific axis, covering:
|
|
|
53
53
|
|
|
54
54
|
For Chinese topics: search in **both Chinese and English**.
|
|
55
55
|
|
|
56
|
-
Use
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
Use **\`websearch\`** for broad keyword queries to find relevant pages, reports,
|
|
57
|
+
and data. Then use **\`webfetch\`** to retrieve specific pages for depth.
|
|
58
|
+
|
|
59
|
+
Search strategy:
|
|
60
|
+
- Start with \`websearch\` to discover relevant URLs (market reports, company pages, news)
|
|
61
|
+
- Follow up with \`webfetch\` on the most promising URLs for full content
|
|
62
|
+
- For Chinese topics: run \`websearch\` queries in both Chinese and English
|
|
59
63
|
|
|
60
64
|
---
|
|
61
65
|
|
package/lib/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 = "aurora"
|
|
32
32
|
|
|
33
33
|
/** Default domain name. */
|
|
34
34
|
export const DEFAULT_DOMAIN = "general"
|
package/lib/design/designs.ts
CHANGED
|
@@ -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
|
|
156
|
+
/** Map of @design:<name> section → extracted content (without marker lines). */
|
|
150
157
|
sections: Record<string, string>
|
|
151
|
-
/** Map of
|
|
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
|
|
160
|
-
* <!-- @
|
|
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
|
|
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.
|
package/lib/log.ts
CHANGED
|
@@ -17,8 +17,9 @@ export const log = new Logger({
|
|
|
17
17
|
type: "json",
|
|
18
18
|
hideLogPositionForProduction: true,
|
|
19
19
|
overwrite: {
|
|
20
|
-
transportJSON: (
|
|
21
|
-
|
|
20
|
+
transportJSON: (_logObj: unknown) => {
|
|
21
|
+
// Silenced: revela runs as an OpenCode plugin; writing to stderr
|
|
22
|
+
// pollutes the host terminal. Logs are intentionally suppressed.
|
|
22
23
|
},
|
|
23
24
|
},
|
|
24
25
|
})
|
package/lib/prompt-builder.ts
CHANGED
|
@@ -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
|
|
70
|
-
|
|
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 @
|
|
125
|
-
* - Always include @
|
|
126
|
-
* -
|
|
127
|
-
* -
|
|
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.
|
|
150
|
-
if (sections["
|
|
151
|
-
layerParts.push(sections["
|
|
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
|
-
//
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
layerParts.push(
|
|
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
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
150
|
+
// 4. Component Index — compact catalog
|
|
151
|
+
const componentIndex = generateComponentIndex(components)
|
|
152
|
+
if (componentIndex) {
|
|
153
|
+
layerParts.push(componentIndex)
|
|
163
154
|
}
|
|
164
155
|
|
|
165
|
-
//
|
|
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: \"
|
|
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
|
-
*
|
|
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
|
-
//
|
|
267
|
-
if (metrics.
|
|
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
|
-
//
|
|
504
|
-
if (metrics.
|
|
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
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
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
|
-
|
|
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
|
|
242
|
-
const
|
|
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
|
-
|
|
272
|
+
slideQa,
|
|
272
273
|
canvasRect,
|
|
273
274
|
elements,
|
|
274
275
|
contentRect: unionRect(elements),
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -111,6 +111,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
111
111
|
// Permissions: read-only on edit/bash; write allowed to create researches/ files.
|
|
112
112
|
// No model override — inherits from the calling primary agent.
|
|
113
113
|
opencodeConfig.agent ??= {}
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
115
|
opencodeConfig.agent["revela-research"] = {
|
|
115
116
|
description: "Revela research agent — searches and collects raw materials for presentations",
|
|
116
117
|
mode: "subagent",
|
|
@@ -123,7 +124,19 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
123
124
|
"ls": "allow",
|
|
124
125
|
},
|
|
125
126
|
webfetch: "allow",
|
|
126
|
-
},
|
|
127
|
+
} as any,
|
|
128
|
+
}
|
|
129
|
+
// Give revela-research explicit websearch allow (overrides global deny below)
|
|
130
|
+
;(opencodeConfig.agent["revela-research"].permission as any).websearch = "allow"
|
|
131
|
+
|
|
132
|
+
// Block websearch for the primary agent globally.
|
|
133
|
+
// permission.ask hook is not triggered by OpenCode (no R.trigger call in binary).
|
|
134
|
+
// tool.execute.before throw is swallowed (trigger().catch(()=>{})).
|
|
135
|
+
// The only working mechanism is the config-level permission ruleset.
|
|
136
|
+
// revela-research agent overrides this with websearch: "allow" above.
|
|
137
|
+
opencodeConfig.permission ??= {}
|
|
138
|
+
if (!(opencodeConfig.permission as Record<string, unknown>)["websearch"]) {
|
|
139
|
+
;(opencodeConfig.permission as Record<string, unknown>)["websearch"] = "deny"
|
|
127
140
|
}
|
|
128
141
|
},
|
|
129
142
|
|
|
@@ -283,22 +296,10 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
283
296
|
// ── Pre-read: intercept binary files before read executes ──────────────
|
|
284
297
|
// Handles DOCX/PPTX/XLSX — read tool would Effect.fail on these.
|
|
285
298
|
// Extracts text → writes temp .txt → redirects args.filePath.
|
|
286
|
-
//
|
|
287
|
-
// Also blocks websearch for the primary agent — websearch must be delegated
|
|
288
|
-
// to the revela-research subagent. Use webfetch for specific URLs instead.
|
|
289
299
|
"tool.execute.before": async (input, output) => {
|
|
300
|
+
log.info("[hook] tool.execute.before fired", { tool: input.tool, enabled: ctx.enabled, isResearch: ctx.isResearchAgent })
|
|
290
301
|
if (!ctx.enabled) return
|
|
291
302
|
|
|
292
|
-
// ── Block websearch for primary agent ──────────────────────────────
|
|
293
|
-
if (input.tool === "websearch" && !ctx.isResearchAgent) {
|
|
294
|
-
throw new Error(
|
|
295
|
-
"[revela] websearch is not available for the primary agent. " +
|
|
296
|
-
"Delegate web research to the revela-research subagent via the Task tool — " +
|
|
297
|
-
"it searches systematically and saves structured findings for reuse across sessions. " +
|
|
298
|
-
"Use the webfetch tool if you need to read a specific URL directly.",
|
|
299
|
-
)
|
|
300
|
-
}
|
|
301
|
-
|
|
302
303
|
if (input.tool !== "read") return
|
|
303
304
|
try {
|
|
304
305
|
await preRead(output.args)
|