@cyber-dash-tech/revela 0.17.16 → 0.17.18
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 +20 -8
- package/README.zh-CN.md +20 -8
- package/bin/revela.ts +5 -1
- package/designs/monet/DESIGN.md +1 -0
- package/designs/starter/DESIGN.md +1 -0
- package/designs/summit/DESIGN.md +1 -0
- package/lib/deck-html/contract.ts +88 -2
- package/lib/edit/prompt.ts +1 -1
- package/lib/html-export/deck-detect.ts +58 -32
- package/lib/pdf/export.ts +5 -6
- package/lib/qa/checks.ts +21 -1
- package/lib/qa/measure.ts +32 -2
- package/lib/runtime/index.ts +116 -3
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/hooks.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +79 -6
- package/plugins/revela/hooks/revela_post_write_notice.ts +82 -10
- package/plugins/revela/mcp/revela-server.ts +25 -1
- package/plugins/revela/skills/revela-design/SKILL.md +15 -1
- package/plugins/revela/skills/revela-export/SKILL.md +1 -1
- package/plugins/revela/skills/revela-make-deck/SKILL.md +2 -2
- package/plugins/revela/skills/revela-review-deck/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -123,49 +123,61 @@ revela, use consulting as the domain.
|
|
|
123
123
|
revela, use summit as the design.
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
3.
|
|
126
|
+
3. Create a custom design when you want a different visual direction.
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
revela, create a new design named neon-finance with a crisp financial-dashboard style: dark surfaces, precise grids, and bright green accents.
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Revela may ask for references or constraints, then creates and validates the design. When it is ready, switch to it:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
revela, use neon-finance as the design.
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
4. Initialize the narrative from local materials. Init grounds the narrative in the workspace and surfaces gaps; it does not replace the research step.
|
|
127
139
|
|
|
128
140
|
```text
|
|
129
141
|
revela, help me init this workspace from the local materials.
|
|
130
142
|
```
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
5. Research the gaps and bind only source-supported evidence into the narrative.
|
|
133
145
|
|
|
134
146
|
```text
|
|
135
147
|
revela, research the current gaps and bind only source-supported evidence.
|
|
136
148
|
```
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
6. Read Story before rendering to inspect the claim flow, evidence support, caveats, unsupported scope, and open gaps.
|
|
139
151
|
|
|
140
152
|
```text
|
|
141
153
|
revela, show me the Story before we make the deck.
|
|
142
154
|
```
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
7. Create or update the deck plan before generating HTML so slide order, chapter structure, evidence trace, caveats, and visual intent are explicit.
|
|
145
157
|
|
|
146
158
|
```text
|
|
147
159
|
revela, create or update the deck plan before generating HTML.
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
8. Make an HTML deck from the current deck plan and canonical narrative.
|
|
151
163
|
|
|
152
164
|
```text
|
|
153
165
|
revela, make the deck from the current deck plan and narrative.
|
|
154
166
|
```
|
|
155
167
|
|
|
156
|
-
|
|
168
|
+
9. Review the generated deck for traceability, diagnostics, and targeted edits.
|
|
157
169
|
|
|
158
170
|
```text
|
|
159
171
|
revela, review the generated deck.
|
|
160
172
|
```
|
|
161
173
|
|
|
162
|
-
|
|
174
|
+
10. Export a PDF after deck QA passes.
|
|
163
175
|
|
|
164
176
|
```text
|
|
165
177
|
revela, export the deck to PDF.
|
|
166
178
|
```
|
|
167
179
|
|
|
168
|
-
|
|
180
|
+
11. Export an editable PPTX after deck QA passes.
|
|
169
181
|
|
|
170
182
|
```text
|
|
171
183
|
revela, export the deck to PPTX.
|
package/README.zh-CN.md
CHANGED
|
@@ -123,49 +123,61 @@ revela,use consulting as domain.
|
|
|
123
123
|
revela,use summit as design.
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
3.
|
|
126
|
+
3. 如果需要不同的视觉方向,可以创建一个自定义 design。
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
revela,创建一个名为 neon-finance 的新 design:金融仪表盘风格,深色界面、精密网格、亮绿色重点色。
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Revela 可能会继续询问参考图、风格约束或禁忌项,然后创建并校验 design。创建完成后再切换使用:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
revela,使用 neon-finance 作为 design。
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
4. 从本地材料初始化 narrative。Init 负责基于 workspace 做 grounding 并暴露 gap;它不替代 research 步骤。
|
|
127
139
|
|
|
128
140
|
```text
|
|
129
141
|
revela,帮我 init 这个 workspace,先读本地材料。
|
|
130
142
|
```
|
|
131
143
|
|
|
132
|
-
|
|
144
|
+
5. 针对 gap 做 research,并且只把来源明确支持的 evidence 绑定回 narrative。
|
|
133
145
|
|
|
134
146
|
```text
|
|
135
147
|
revela,research 当前 gaps,只绑定 source-supported evidence。
|
|
136
148
|
```
|
|
137
149
|
|
|
138
|
-
|
|
150
|
+
6. 生成 deck 前先读 Story,检查 claim flow、证据支撑、caveats、unsupported scope 和 open gaps。
|
|
139
151
|
|
|
140
152
|
```text
|
|
141
153
|
revela,先给我看 Story,再 make deck。
|
|
142
154
|
```
|
|
143
155
|
|
|
144
|
-
|
|
156
|
+
7. 先创建或更新 deck plan,明确 slide 顺序、章节结构、evidence trace、caveats 和 visual intent,再生成 HTML。
|
|
145
157
|
|
|
146
158
|
```text
|
|
147
159
|
revela,生成 HTML 前先 create or update deck plan。
|
|
148
160
|
```
|
|
149
161
|
|
|
150
|
-
|
|
162
|
+
8. 基于当前 deck plan 和 canonical narrative 生成 HTML deck。
|
|
151
163
|
|
|
152
164
|
```text
|
|
153
165
|
revela,基于当前 deck plan 和 narrative make deck。
|
|
154
166
|
```
|
|
155
167
|
|
|
156
|
-
|
|
168
|
+
9. Review 生成后的 deck,检查 traceability、diagnostics,并做定向修改。
|
|
157
169
|
|
|
158
170
|
```text
|
|
159
171
|
revela,review 生成好的 deck。
|
|
160
172
|
```
|
|
161
173
|
|
|
162
|
-
|
|
174
|
+
10. QA 通过后导出 PDF。
|
|
163
175
|
|
|
164
176
|
```text
|
|
165
177
|
revela,把 deck export 成 PDF。
|
|
166
178
|
```
|
|
167
179
|
|
|
168
|
-
|
|
180
|
+
11. QA 通过后导出可编辑 PPTX。
|
|
169
181
|
|
|
170
182
|
```text
|
|
171
183
|
revela,把 deck export 成 PPTX。
|
package/bin/revela.ts
CHANGED
|
@@ -31,6 +31,8 @@ else {
|
|
|
31
31
|
else if (command === "design-list") result = runtime.designList()
|
|
32
32
|
else if (command === "design-read") result = runtime.designRead(options)
|
|
33
33
|
else if (command === "design-use") result = runtime.designActivate(required(options, ["name"]))
|
|
34
|
+
else if (command === "design-create") result = runtime.designCreate(required(options, ["name", "designMd", "previewHtml"]))
|
|
35
|
+
else if (command === "design-validate") result = runtime.designValidate(required(options, ["name"]))
|
|
34
36
|
else if (command === "domain-list") result = runtime.domainList()
|
|
35
37
|
else if (command === "domain-read") result = runtime.domainRead(options)
|
|
36
38
|
else if (command === "domain-use") result = runtime.domainActivate(required(options, ["name"]))
|
|
@@ -89,8 +91,10 @@ Usage:
|
|
|
89
91
|
revela export-pdf --file <path> [--workspaceRoot <path>] # deck PDF, or single-page PDF fallback for non-deck HTML
|
|
90
92
|
revela export-pptx --file <path> [--workspaceRoot <path>]
|
|
91
93
|
revela design-list
|
|
92
|
-
revela design-read [--name <design>]
|
|
94
|
+
revela design-read [--name <design>] [--section <rules|foundation|chart-rules>] [--workspaceRoot <path>]
|
|
93
95
|
revela design-use --name <design>
|
|
96
|
+
revela design-create --name <design> --designMd <text> --previewHtml <text> [--base <design>] [--overwrite true]
|
|
97
|
+
revela design-validate --name <design>
|
|
94
98
|
revela domain-list
|
|
95
99
|
revela domain-read [--name <domain>]
|
|
96
100
|
revela domain-use --name <domain>
|
package/designs/monet/DESIGN.md
CHANGED
|
@@ -483,6 +483,7 @@ These rules are mandatory for Monet.
|
|
|
483
483
|
- **Icon system is Lucide.** For ordinary UI, semantic, status, category, process, and navigation icons, use Lucide (`data-lucide`). Do not hand-write inline SVG for icons. SVG is allowed only for intentional decorative motifs, illustrations, or design-specific artwork. If any `data-lucide` icon is present, load Lucide via CDN and call `lucide.createIcons()` after `SlidePresentation`.
|
|
484
484
|
- **Chart system is ECharts.** Data charts default to ECharts inside `echart-panel`. Do not use hand-written SVG, div/CSS shapes, canvas mocks, or static faux charts as data-chart substitutes. SVG remains acceptable for decorative motifs, diagrams, or illustrations, not data charts. Before creating or changing a chart, fetch the `echart-panel` component and `section: "chart-rules"`; if chart rules or runtime are unavailable, report the gap instead of inventing a fake chart fallback.
|
|
485
485
|
- **Start from foundation.** New deck HTML starts from `@design:foundation`. Do not recreate foundation CSS, JavaScript, or the HTML skeleton from memory. Prefer a foundation helper when available; otherwise fetch `section: "foundation"` before writing a new deck shell. Existing deck edits preserve the current foundation unless the user asks for foundation repair or QA reports a foundation contract problem.
|
|
486
|
+
- **Canonical slide canvas.** Every slide must be `<section class="slide" slide-qa="..." data-slide-index="N">` with exactly one direct child `.slide-canvas`. `.slide-canvas` is the 1920px x 1080px export surface and must keep `padding: 0`, `position: relative`, and `overflow: hidden`; put `.page` or layout containers inside `.slide-canvas`, never slide content directly under `.slide`. Missing, nested, or duplicate `.slide-canvas` elements are invalid and fail Artifact QA.
|
|
486
487
|
|
|
487
488
|
### Common Mistakes
|
|
488
489
|
|
|
@@ -236,6 +236,7 @@ new SlidePresentation();
|
|
|
236
236
|
- **Icon system is Lucide.** For ordinary UI, semantic, status, category, process, and navigation icons, use Lucide (`data-lucide`). Do not hand-write inline SVG for icons. SVG is allowed only for intentional decorative motifs, illustrations, or design-specific artwork. If any `data-lucide` icon is present, load Lucide via CDN and call `lucide.createIcons()` after `SlidePresentation`.
|
|
237
237
|
- **Chart system is ECharts.** Data charts default to ECharts inside `echart-panel`. Do not use hand-written SVG, div/CSS shapes, canvas mocks, or static faux charts as data-chart substitutes. SVG remains acceptable for decorative motifs, diagrams, or illustrations, not data charts. Before creating or changing a chart, fetch the `echart-panel` component and `section: "chart-rules"`; if chart rules or runtime are unavailable, report the gap instead of inventing a fake chart fallback.
|
|
238
238
|
- **Start from foundation.** New deck HTML starts from `@design:foundation`. Do not recreate foundation CSS, JavaScript, or the HTML skeleton from memory. Prefer a foundation helper when available; otherwise fetch `section: "foundation"` before writing a new deck shell. Existing deck edits preserve the current foundation unless the user asks for foundation repair or QA reports a foundation contract problem.
|
|
239
|
+
- **Canonical slide canvas.** Every slide must be `<section class="slide" slide-qa="..." data-slide-index="N">` with exactly one direct child `.slide-canvas`. `.slide-canvas` is the 1920px x 1080px export surface and must keep `padding: 0`, `position: relative`, and `overflow: hidden`; put `.page` or layout containers inside `.slide-canvas`, never slide content directly under `.slide`. Missing, nested, or duplicate `.slide-canvas` elements are invalid and fail Artifact QA.
|
|
239
240
|
- **Images for photographic references.** Use image treatment rules rather than fake SVG when the reference is photographic, UI, webpage, or product imagery.
|
|
240
241
|
- **Content pages need a stable title block.** Except cover, TOC, closing, section divider, and full-bleed hero slides, every normal content slide should include a visible title block from the upper-left safe area. It should contain a compact chapter/section label plus a slide title written as the page's claim or takeaway.
|
|
241
242
|
- **Do not hide the page title inside a card.** Body components may have their own headings, but the slide-level title block should remain separate and easy to scan unless the chosen layout explicitly defines a compact side-title variant.
|
package/designs/summit/DESIGN.md
CHANGED
|
@@ -446,6 +446,7 @@ These rules are mandatory for Summit.
|
|
|
446
446
|
- **Icon system is Lucide.** For ordinary UI, semantic, status, category, process, and navigation icons, use Lucide (`data-lucide`). Do not hand-write inline SVG for icons. SVG is allowed only for intentional decorative motifs, illustrations, or design-specific artwork. If any `data-lucide` icon is present, load Lucide via CDN and call `lucide.createIcons()` after `SlidePresentation`.
|
|
447
447
|
- **Chart system is ECharts.** Data charts default to ECharts inside `echart-panel`. Do not use hand-written SVG, div/CSS shapes, canvas mocks, or static faux charts as data-chart substitutes. SVG remains acceptable for decorative motifs, diagrams, or illustrations, not data charts. Before creating or changing a chart, fetch the `echart-panel` component and `section: "chart-rules"`; if chart rules or runtime are unavailable, report the gap instead of inventing a fake chart fallback.
|
|
448
448
|
- **Start from foundation.** New deck HTML starts from `@design:foundation`. Do not recreate foundation CSS, JavaScript, or the HTML skeleton from memory. Prefer a foundation helper when available; otherwise fetch `section: "foundation"` before writing a new deck shell. Existing deck edits preserve the current foundation unless the user asks for foundation repair or QA reports a foundation contract problem.
|
|
449
|
+
- **Canonical slide canvas.** Every slide must be `<section class="slide" slide-qa="..." data-slide-index="N">` with exactly one direct child `.slide-canvas`. `.slide-canvas` is the 1920px x 1080px export surface and must keep `padding: 0`, `position: relative`, and `overflow: hidden`; put `.page` or layout containers inside `.slide-canvas`, never slide content directly under `.slide`. Missing, nested, or duplicate `.slide-canvas` elements are invalid and fail Artifact QA.
|
|
449
450
|
|
|
450
451
|
### Common Mistakes
|
|
451
452
|
|
|
@@ -16,6 +16,9 @@ export type DeckHtmlContractIssueType =
|
|
|
16
16
|
| "duplicate_data_slide_index"
|
|
17
17
|
| "slide_index_order"
|
|
18
18
|
| "legacy_data_index_noncanonical"
|
|
19
|
+
| "missing_slide_canvas"
|
|
20
|
+
| "multiple_slide_canvas"
|
|
21
|
+
| "slide_canvas_not_direct_child"
|
|
19
22
|
|
|
20
23
|
export interface DeckHtmlContractIssue {
|
|
21
24
|
type: DeckHtmlContractIssueType
|
|
@@ -43,6 +46,8 @@ interface SlideSectionAttrs {
|
|
|
43
46
|
position: number
|
|
44
47
|
dataSlideIndex?: string
|
|
45
48
|
dataIndex?: string
|
|
49
|
+
directSlideCanvasCount: number
|
|
50
|
+
descendantSlideCanvasCount: number
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export function validateDeckHtmlContract(workspaceRoot: string, filePath: string): DeckHtmlContractReport {
|
|
@@ -111,6 +116,26 @@ export function validateDeckHtmlContract(workspaceRoot: string, filePath: string
|
|
|
111
116
|
let previousIndex = 0
|
|
112
117
|
sections.forEach((section, offset) => {
|
|
113
118
|
const expectedIndex = base.expectedIndexes[offset]
|
|
119
|
+
if (section.directSlideCanvasCount === 0) {
|
|
120
|
+
base.issues.push({
|
|
121
|
+
type: section.descendantSlideCanvasCount > 0 ? "slide_canvas_not_direct_child" : "missing_slide_canvas",
|
|
122
|
+
severity: "error",
|
|
123
|
+
message: section.descendantSlideCanvasCount > 0
|
|
124
|
+
? `Slide ${section.position} has .slide-canvas, but it must be a direct child of the .slide section.`
|
|
125
|
+
: `Slide ${section.position} is missing a direct .slide-canvas child.`,
|
|
126
|
+
slidePosition: section.position,
|
|
127
|
+
expectedIndex,
|
|
128
|
+
})
|
|
129
|
+
} else if (section.directSlideCanvasCount > 1) {
|
|
130
|
+
base.issues.push({
|
|
131
|
+
type: "multiple_slide_canvas",
|
|
132
|
+
severity: "error",
|
|
133
|
+
message: `Slide ${section.position} has ${section.directSlideCanvasCount} direct .slide-canvas children; expected exactly one.`,
|
|
134
|
+
slidePosition: section.position,
|
|
135
|
+
expectedIndex,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
if (section.dataIndex !== undefined) {
|
|
115
140
|
base.warnings.push({
|
|
116
141
|
type: "legacy_data_index_noncanonical",
|
|
@@ -217,20 +242,62 @@ export function formatDeckHtmlContractReport(report: DeckHtmlContractReport): st
|
|
|
217
242
|
|
|
218
243
|
function extractSlideSections(html: string): SlideSectionAttrs[] {
|
|
219
244
|
const sections: SlideSectionAttrs[] = []
|
|
220
|
-
const
|
|
245
|
+
const sectionPattern = /<section\b([^>]*)>([\s\S]*?)<\/section>/gi
|
|
221
246
|
let match: RegExpExecArray | null
|
|
222
|
-
while ((match =
|
|
247
|
+
while ((match = sectionPattern.exec(html))) {
|
|
223
248
|
const attrs = match[1] ?? ""
|
|
224
249
|
if (!/\bclass\s*=\s*(["'])[^"']*\bslide\b[^"']*\1/i.test(attrs)) continue
|
|
250
|
+
const body = match[2] ?? ""
|
|
251
|
+
const directSlideCanvasCount = countDirectSlideCanvasChildren(body)
|
|
252
|
+
const descendantSlideCanvasCount = countSlideCanvasDescendants(body)
|
|
225
253
|
sections.push({
|
|
226
254
|
position: sections.length + 1,
|
|
227
255
|
dataSlideIndex: readAttr(attrs, "data-slide-index"),
|
|
228
256
|
dataIndex: readAttr(attrs, "data-index"),
|
|
257
|
+
directSlideCanvasCount,
|
|
258
|
+
descendantSlideCanvasCount,
|
|
229
259
|
})
|
|
230
260
|
}
|
|
231
261
|
return sections
|
|
232
262
|
}
|
|
233
263
|
|
|
264
|
+
function countSlideCanvasDescendants(html: string): number {
|
|
265
|
+
const pattern = /<([a-z][\w:-]*)\b([^>]*)>/gi
|
|
266
|
+
let count = 0
|
|
267
|
+
let match: RegExpExecArray | null
|
|
268
|
+
while ((match = pattern.exec(html))) {
|
|
269
|
+
const attrs = match[2] ?? ""
|
|
270
|
+
if (hasClass(attrs, "slide-canvas")) count++
|
|
271
|
+
}
|
|
272
|
+
return count
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function countDirectSlideCanvasChildren(html: string): number {
|
|
276
|
+
let depth = 0
|
|
277
|
+
let count = 0
|
|
278
|
+
const pattern = /<!--[\s\S]*?-->|<\/?([a-z][\w:-]*)\b([^>]*)>/gi
|
|
279
|
+
let match: RegExpExecArray | null
|
|
280
|
+
while ((match = pattern.exec(html))) {
|
|
281
|
+
const token = match[0]
|
|
282
|
+
if (token.startsWith("<!--")) continue
|
|
283
|
+
const tag = match[1]?.toLowerCase()
|
|
284
|
+
if (!tag) continue
|
|
285
|
+
if (token.startsWith("</")) {
|
|
286
|
+
depth = Math.max(0, depth - 1)
|
|
287
|
+
continue
|
|
288
|
+
}
|
|
289
|
+
const attrs = match[2] ?? ""
|
|
290
|
+
if (depth === 0 && hasClass(attrs, "slide-canvas")) count++
|
|
291
|
+
if (!isVoidTag(tag) && !/\/\s*>$/.test(token)) depth++
|
|
292
|
+
}
|
|
293
|
+
return count
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function hasClass(attrs: string, className: string): boolean {
|
|
297
|
+
const classAttr = readAttr(attrs, "class")
|
|
298
|
+
return classAttr?.split(/\s+/).includes(className) ?? false
|
|
299
|
+
}
|
|
300
|
+
|
|
234
301
|
function readAttr(attrs: string, name: string): string | undefined {
|
|
235
302
|
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*=\\s*(["'])(.*?)\\1`, "i")
|
|
236
303
|
return pattern.exec(attrs)?.[2]
|
|
@@ -266,3 +333,22 @@ function workspaceRelative(root: string, target: string): string {
|
|
|
266
333
|
function escapeRegExp(value: string): string {
|
|
267
334
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
268
335
|
}
|
|
336
|
+
|
|
337
|
+
function isVoidTag(tag: string): boolean {
|
|
338
|
+
return new Set([
|
|
339
|
+
"area",
|
|
340
|
+
"base",
|
|
341
|
+
"br",
|
|
342
|
+
"col",
|
|
343
|
+
"embed",
|
|
344
|
+
"hr",
|
|
345
|
+
"img",
|
|
346
|
+
"input",
|
|
347
|
+
"link",
|
|
348
|
+
"meta",
|
|
349
|
+
"param",
|
|
350
|
+
"source",
|
|
351
|
+
"track",
|
|
352
|
+
"wbr",
|
|
353
|
+
]).has(tag)
|
|
354
|
+
}
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -61,7 +61,7 @@ export function buildEditPrompt(payload: EditCommentPayload): string {
|
|
|
61
61
|
drop: payload.drop,
|
|
62
62
|
}
|
|
63
63
|
const qaInstruction = payload.suppressAutomaticArtifactQa
|
|
64
|
-
? `-
|
|
64
|
+
? `- The Review bridge may suppress host-side post-write QA for this specific Apply Fix request. Do not treat that as deck readiness; any reported QA failures still require the smallest targeted repair before review or export readiness.`
|
|
65
65
|
: `- Artifact QA runs automatically after deck writes/patches/edits. It checks deck HTML contract, design component compliance, exact 1920x1080 slide geometry, scrollbars, element overflow, text clipping, and claim/evidence content-density warnings.
|
|
66
66
|
- If the tool result reports hard QA errors, fix them with the smallest targeted patch and let the post-write QA run again. Refine opens automatically only after hard errors pass; warnings such as thin claim/evidence substance do not block opening.`
|
|
67
67
|
|
|
@@ -22,55 +22,81 @@ export async function detectDeckHtmlWithBrowser(browser: Browser, htmlFilePath:
|
|
|
22
22
|
try {
|
|
23
23
|
await page.goto(pathToFileURL(htmlFilePath).href, { waitUntil: "domcontentloaded", timeout: 15000 })
|
|
24
24
|
return await page.evaluate(() => {
|
|
25
|
+
const CANVAS_WIDTH = 1920
|
|
26
|
+
const CANVAS_HEIGHT = 1080
|
|
27
|
+
const DIMENSION_TOLERANCE = 2
|
|
28
|
+
const isDeckCanvasSize = (el: HTMLElement): boolean => {
|
|
29
|
+
const rect = el.getBoundingClientRect()
|
|
30
|
+
return (
|
|
31
|
+
Math.abs(rect.width - CANVAS_WIDTH) <= DIMENSION_TOLERANCE &&
|
|
32
|
+
Math.abs(rect.height - CANVAS_HEIGHT) <= DIMENSION_TOLERANCE
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
const slides = Array.from(document.querySelectorAll(".slide")) as HTMLElement[]
|
|
26
37
|
if (slides.length === 0) {
|
|
27
38
|
return { isDeck: false, slideCount: 0, reason: "no .slide elements found" }
|
|
28
39
|
}
|
|
29
40
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
const indexValues = slides.map((slide) => slide.getAttribute("data-slide-index"))
|
|
42
|
+
const seen = new Set<number>()
|
|
43
|
+
for (let i = 0; i < indexValues.length; i++) {
|
|
44
|
+
const raw = indexValues[i]
|
|
45
|
+
if (raw === null || raw.trim() === "") {
|
|
46
|
+
return {
|
|
47
|
+
isDeck: false,
|
|
48
|
+
slideCount: slides.length,
|
|
49
|
+
reason: `slide ${i + 1} is missing data-slide-index`,
|
|
50
|
+
}
|
|
36
51
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const indexValues = slides
|
|
40
|
-
.map((slide) => slide.getAttribute("data-slide-index"))
|
|
41
|
-
.filter((value): value is string => value !== null && value.trim() !== "")
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
if (
|
|
53
|
+
const parsed = Number(raw)
|
|
54
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
45
55
|
return {
|
|
46
56
|
isDeck: false,
|
|
47
57
|
slideCount: slides.length,
|
|
48
|
-
reason:
|
|
58
|
+
reason: `slide ${i + 1} has invalid data-slide-index "${raw}"`,
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
isDeck: false,
|
|
58
|
-
slideCount: slides.length,
|
|
59
|
-
reason: `slide ${i + 1} has invalid data-slide-index "${indexValues[i]}"`,
|
|
60
|
-
}
|
|
61
|
+
if (seen.has(parsed)) {
|
|
62
|
+
return {
|
|
63
|
+
isDeck: false,
|
|
64
|
+
slideCount: slides.length,
|
|
65
|
+
reason: `duplicate data-slide-index "${parsed}"`,
|
|
61
66
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
67
|
+
}
|
|
68
|
+
if (parsed !== i + 1) {
|
|
69
|
+
return {
|
|
70
|
+
isDeck: false,
|
|
71
|
+
slideCount: slides.length,
|
|
72
|
+
reason: `slide ${i + 1} has data-slide-index "${parsed}", expected "${i + 1}"`,
|
|
68
73
|
}
|
|
69
|
-
seen.add(parsed)
|
|
70
74
|
}
|
|
75
|
+
seen.add(parsed)
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
|
|
78
|
+
let usedSlideAsCanvas = false
|
|
79
|
+
for (let i = 0; i < slides.length; i++) {
|
|
80
|
+
const slide = slides[i]
|
|
81
|
+
if (slide.querySelector(".slide-canvas")) continue
|
|
82
|
+
if (isDeckCanvasSize(slide)) {
|
|
83
|
+
usedSlideAsCanvas = true
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
isDeck: false,
|
|
88
|
+
slideCount: slides.length,
|
|
89
|
+
reason: `.slide ${i + 1} has no .slide-canvas and is not 1920x1080`,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
isDeck: true,
|
|
95
|
+
slideCount: slides.length,
|
|
96
|
+
reason: usedSlideAsCanvas
|
|
97
|
+
? "valid deck contract: slide-as-canvas"
|
|
98
|
+
: "valid deck contract: slide-canvas",
|
|
99
|
+
}
|
|
74
100
|
})
|
|
75
101
|
} finally {
|
|
76
102
|
await page.close().catch(() => undefined)
|
package/lib/pdf/export.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* rewrite the HTML to use file:// local paths — avoids CDN/CORS/headless issues
|
|
10
10
|
* 3. Navigate to the patched HTML file
|
|
11
11
|
* 4. For each .slide: force .reveal.visible, wait 800ms, screenshot .slide-canvas
|
|
12
|
-
* using offsetParent-chain absolute coordinates
|
|
12
|
+
* or slide-as-canvas fallback using offsetParent-chain absolute coordinates
|
|
13
13
|
* 5. Assemble screenshots into a multi-page PDF (16:9, 1920×1080pt per page) via pdf-lib
|
|
14
14
|
* 6. Write PDF alongside the HTML file (same directory, .html → .pdf)
|
|
15
15
|
* 7. Clean up temp dir
|
|
@@ -335,24 +335,23 @@ export async function exportDeckToPdf(htmlFilePath: string): Promise<Omit<Export
|
|
|
335
335
|
// Wait for CSS transitions and JS rendering (ECharts animations, etc.)
|
|
336
336
|
await new Promise((r) => setTimeout(r, 800))
|
|
337
337
|
|
|
338
|
-
// Compute
|
|
338
|
+
// Compute screenshot target absolute position by walking the offsetParent chain.
|
|
339
339
|
// getBoundingClientRect() returns viewport-relative coords (always near 0,0) —
|
|
340
340
|
// unusable as screenshot clip coordinates without adding scrollY.
|
|
341
341
|
// offsetParent walk gives document-absolute coords that Puppeteer clip expects.
|
|
342
342
|
const clipRect = await page.evaluate((i: number) => {
|
|
343
343
|
const slide = document.querySelectorAll(".slide")[i] as HTMLElement | null
|
|
344
344
|
if (!slide) return null
|
|
345
|
-
const
|
|
346
|
-
if (!canvas) return null
|
|
345
|
+
const target = (slide.querySelector(".slide-canvas") as HTMLElement | null) ?? slide
|
|
347
346
|
let top = 0
|
|
348
347
|
let left = 0
|
|
349
|
-
let el: HTMLElement | null =
|
|
348
|
+
let el: HTMLElement | null = target
|
|
350
349
|
while (el) {
|
|
351
350
|
top += el.offsetTop
|
|
352
351
|
left += el.offsetLeft
|
|
353
352
|
el = el.offsetParent as HTMLElement | null
|
|
354
353
|
}
|
|
355
|
-
return { x: left, y: top, width:
|
|
354
|
+
return { x: left, y: top, width: target.offsetWidth, height: target.offsetHeight }
|
|
356
355
|
}, idx)
|
|
357
356
|
|
|
358
357
|
if (clipRect && clipRect.width > 0 && clipRect.height > 0) {
|
package/lib/qa/checks.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type IssueSeverity = "error" | "warning" | "info"
|
|
|
25
25
|
export interface LayoutIssue {
|
|
26
26
|
type: "canvas" | "scrollbar" | "navigation" | "overflow" | "text_overflow" | "overlap" | "density" | "balance" | "symmetry" | "rhythm" | "compliance" | "asset"
|
|
27
27
|
/** Sub-category within the dimension */
|
|
28
|
-
sub?: "size_mismatch" | "page_scroll" | "fixed_overlay_slides" | "hidden_paging" | "unreachable_slides" | "text_clipped" | "thin_content"
|
|
28
|
+
sub?: "size_mismatch" | "missing_slide_canvas" | "multiple_slide_canvas" | "page_scroll" | "fixed_overlay_slides" | "hidden_paging" | "unreachable_slides" | "text_clipped" | "thin_content"
|
|
29
29
|
| "element_collision" | "major_overlap" | "possible_overlay"
|
|
30
30
|
| "centroid_offset" | "bottom_gap" | "sparse"
|
|
31
31
|
| "height_mismatch" | "density_mismatch"
|
|
@@ -188,6 +188,26 @@ function collectLeaves(el: ElementInfo): ElementInfo[] {
|
|
|
188
188
|
function checkCanvas(metrics: SlideMetrics): LayoutIssue[] {
|
|
189
189
|
const issues: LayoutIssue[] = []
|
|
190
190
|
const tol = T.CANVAS_TOLERANCE
|
|
191
|
+
const directCanvasCount = metrics.directSlideCanvasCount ?? 1
|
|
192
|
+
if (directCanvasCount === 0) {
|
|
193
|
+
issues.push({
|
|
194
|
+
type: "canvas",
|
|
195
|
+
sub: "missing_slide_canvas",
|
|
196
|
+
severity: "error",
|
|
197
|
+
detail: "Each .slide must have a direct .slide-canvas child.",
|
|
198
|
+
})
|
|
199
|
+
return issues
|
|
200
|
+
}
|
|
201
|
+
if (directCanvasCount > 1) {
|
|
202
|
+
issues.push({
|
|
203
|
+
type: "canvas",
|
|
204
|
+
sub: "multiple_slide_canvas",
|
|
205
|
+
severity: "error",
|
|
206
|
+
detail: `Each .slide must have exactly one direct .slide-canvas child. Found ${directCanvasCount}.`,
|
|
207
|
+
data: { directSlideCanvasCount: directCanvasCount },
|
|
208
|
+
})
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
const canvasBad = Math.abs(metrics.canvasRect.width - CANVAS_W) > tol || Math.abs(metrics.canvasRect.height - CANVAS_H) > tol
|
|
192
212
|
const slideBad = Math.abs(metrics.slideRect.width - CANVAS_W) > tol || Math.abs(metrics.slideRect.height - CANVAS_H) > tol
|
|
193
213
|
|
package/lib/qa/measure.ts
CHANGED
|
@@ -89,6 +89,8 @@ export interface SlideMetrics {
|
|
|
89
89
|
* Content-heavy layouts set `slide-qa="true"`; structural/sparse slides omit or use `"false"`.
|
|
90
90
|
*/
|
|
91
91
|
slideQa: boolean
|
|
92
|
+
/** Number of direct .slide-canvas children on the .slide element. */
|
|
93
|
+
directSlideCanvasCount: number
|
|
92
94
|
/** bounding box of the slide-canvas element itself (post-scale) */
|
|
93
95
|
canvasRect: Rect
|
|
94
96
|
/** bounding box of the .slide element itself (post-scale) */
|
|
@@ -337,8 +339,35 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
337
339
|
// Read the QA flag for deck metadata; default checks do not branch on it.
|
|
338
340
|
const slideQa = (slide as HTMLElement).getAttribute("slide-qa") === "true"
|
|
339
341
|
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
+
const directCanvases = Array.from(slide.children).filter((child) => child.classList.contains("slide-canvas")) as HTMLElement[]
|
|
343
|
+
const canvas = directCanvases[0] ?? null
|
|
344
|
+
if (!canvas) {
|
|
345
|
+
const slideRaw = (slide as HTMLElement).getBoundingClientRect()
|
|
346
|
+
const titleEl = slide.querySelector("h1, h2")
|
|
347
|
+
const title = titleEl
|
|
348
|
+
? (titleEl.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80)
|
|
349
|
+
: `Slide ${slideIdx + 1}`
|
|
350
|
+
const emptyRect = { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }
|
|
351
|
+
return {
|
|
352
|
+
index: slideIdx,
|
|
353
|
+
title,
|
|
354
|
+
slideQa,
|
|
355
|
+
directSlideCanvasCount: directCanvases.length,
|
|
356
|
+
canvasRect: emptyRect,
|
|
357
|
+
slideRect: {
|
|
358
|
+
left: 0,
|
|
359
|
+
top: 0,
|
|
360
|
+
right: slideRaw.width,
|
|
361
|
+
bottom: slideRaw.height,
|
|
362
|
+
width: slideRaw.width,
|
|
363
|
+
height: slideRaw.height,
|
|
364
|
+
},
|
|
365
|
+
hasScrollbars: false,
|
|
366
|
+
elements: [],
|
|
367
|
+
contentRect: emptyRect,
|
|
368
|
+
contentStats: { bodyTextPoints: 0, contentUnits: 0, supportReferences: 0 },
|
|
369
|
+
}
|
|
370
|
+
}
|
|
342
371
|
|
|
343
372
|
const canvasRaw = canvas.getBoundingClientRect()
|
|
344
373
|
const slideRaw = (slide as HTMLElement).getBoundingClientRect()
|
|
@@ -407,6 +436,7 @@ export async function measureSlides(htmlFilePath: string): Promise<MeasurementRe
|
|
|
407
436
|
index: slideIdx,
|
|
408
437
|
title,
|
|
409
438
|
slideQa,
|
|
439
|
+
directSlideCanvasCount: directCanvases.length,
|
|
410
440
|
canvasRect,
|
|
411
441
|
slideRect,
|
|
412
442
|
hasScrollbars,
|
package/lib/runtime/index.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"
|
|
3
|
+
import { dirname, resolve } from "path"
|
|
4
|
+
import {
|
|
5
|
+
activeDesign,
|
|
6
|
+
activateDesign,
|
|
7
|
+
createDesignPackage,
|
|
8
|
+
getDesignSection,
|
|
9
|
+
getDesignSkillMd,
|
|
10
|
+
listDesigns,
|
|
11
|
+
seedBuiltinDesigns,
|
|
12
|
+
validateDesignPackage,
|
|
13
|
+
} from "../design/designs"
|
|
4
14
|
import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
|
|
5
15
|
import { activeDomain, activateDomain, getDomainSkillMd, listDomains, seedBuiltinDomains } from "../domain/domains"
|
|
6
16
|
import { computeNarrativeHash } from "../narrative-state/hash"
|
|
@@ -32,7 +42,17 @@ export interface RuntimeDeckFoundationInput extends RuntimeWorkspaceInput {
|
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
export interface RuntimeDesignReadInput {
|
|
45
|
+
workspaceRoot?: string
|
|
35
46
|
name?: string
|
|
47
|
+
section?: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RuntimeDesignCreateInput {
|
|
51
|
+
name: string
|
|
52
|
+
base?: string
|
|
53
|
+
designMd: string
|
|
54
|
+
previewHtml: string
|
|
55
|
+
overwrite?: boolean
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
export interface RuntimeNameInput {
|
|
@@ -166,6 +186,17 @@ export function designList() {
|
|
|
166
186
|
export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
167
187
|
seedBuiltinDesigns()
|
|
168
188
|
const name = input.name || activeDesign()
|
|
189
|
+
if (input.section) {
|
|
190
|
+
const markdown = getDesignSection(input.section, name)
|
|
191
|
+
const result = {
|
|
192
|
+
ok: true,
|
|
193
|
+
name,
|
|
194
|
+
section: input.section,
|
|
195
|
+
markdown,
|
|
196
|
+
}
|
|
197
|
+
if (input.section === "rules") recordDesignRulesRead(root(input.workspaceRoot), name, markdown)
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
169
200
|
return {
|
|
170
201
|
ok: true,
|
|
171
202
|
name,
|
|
@@ -173,6 +204,82 @@ export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
|
173
204
|
}
|
|
174
205
|
}
|
|
175
206
|
|
|
207
|
+
export function designCreate(input: RuntimeDesignCreateInput) {
|
|
208
|
+
seedBuiltinDesigns()
|
|
209
|
+
return createDesignPackage({
|
|
210
|
+
name: requiredString(input?.name, "design name"),
|
|
211
|
+
base: input.base,
|
|
212
|
+
designMd: requiredString(input?.designMd, "designMd"),
|
|
213
|
+
previewHtml: requiredString(input?.previewHtml, "previewHtml"),
|
|
214
|
+
overwrite: input.overwrite ?? false,
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function designValidate(input: RuntimeNameInput) {
|
|
219
|
+
seedBuiltinDesigns()
|
|
220
|
+
return validateDesignPackage(requiredName(input, "design"))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface DesignRulesReadinessResult {
|
|
224
|
+
ok: boolean
|
|
225
|
+
activeDesign: string
|
|
226
|
+
markerPath: string
|
|
227
|
+
reason?: string
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const DESIGN_RULES_MARKER_TTL_MS = 8 * 60 * 60 * 1000
|
|
231
|
+
|
|
232
|
+
export function checkDesignRulesReadiness(input: RuntimeWorkspaceInput = {}): DesignRulesReadinessResult {
|
|
233
|
+
seedBuiltinDesigns()
|
|
234
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
235
|
+
const design = activeDesign()
|
|
236
|
+
const rules = getDesignSection("rules", design)
|
|
237
|
+
const markerPath = designRulesMarkerPath(workspaceRoot)
|
|
238
|
+
if (!existsSync(markerPath)) {
|
|
239
|
+
return { ok: false, activeDesign: design, markerPath, reason: "Design rules have not been read for this workspace." }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let marker: any
|
|
243
|
+
try {
|
|
244
|
+
marker = JSON.parse(readFileSync(markerPath, "utf-8"))
|
|
245
|
+
} catch {
|
|
246
|
+
return { ok: false, activeDesign: design, markerPath, reason: "Design rules marker is unreadable." }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (marker.designName !== design) {
|
|
250
|
+
return { ok: false, activeDesign: design, markerPath, reason: `Design rules marker is for '${marker.designName ?? "unknown"}', but active design is '${design}'.` }
|
|
251
|
+
}
|
|
252
|
+
if (marker.rulesHash !== hashDesignRules(rules)) {
|
|
253
|
+
return { ok: false, activeDesign: design, markerPath, reason: "Design rules marker is stale for the current active design rules." }
|
|
254
|
+
}
|
|
255
|
+
if (typeof marker.readAt !== "string" || Number.isNaN(Date.parse(marker.readAt))) {
|
|
256
|
+
return { ok: false, activeDesign: design, markerPath, reason: "Design rules marker is missing a valid read timestamp." }
|
|
257
|
+
}
|
|
258
|
+
if (Date.now() - Date.parse(marker.readAt) > DESIGN_RULES_MARKER_TTL_MS) {
|
|
259
|
+
return { ok: false, activeDesign: design, markerPath, reason: "Design rules marker is older than 8 hours." }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { ok: true, activeDesign: design, markerPath }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function recordDesignRulesRead(workspaceRoot: string, designName: string, rules: string): void {
|
|
266
|
+
const markerPath = designRulesMarkerPath(workspaceRoot)
|
|
267
|
+
mkdirSync(dirname(markerPath), { recursive: true })
|
|
268
|
+
writeFileSync(markerPath, JSON.stringify({
|
|
269
|
+
designName,
|
|
270
|
+
rulesHash: hashDesignRules(rules),
|
|
271
|
+
readAt: new Date().toISOString(),
|
|
272
|
+
}, null, 2) + "\n", "utf-8")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function designRulesMarkerPath(workspaceRoot: string): string {
|
|
276
|
+
return resolve(workspaceRoot, ".opencode", "revela", "codex-hooks", "design-rules-read.json")
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function hashDesignRules(rules: string): string {
|
|
280
|
+
return createHash("sha256").update(rules).digest("hex")
|
|
281
|
+
}
|
|
282
|
+
|
|
176
283
|
export function designActivate(input: RuntimeNameInput) {
|
|
177
284
|
seedBuiltinDesigns()
|
|
178
285
|
activateDesign(requiredName(input, "design"))
|
|
@@ -232,3 +339,9 @@ function requiredName(input: RuntimeNameInput, label: string): string {
|
|
|
232
339
|
if (!name) throw new Error(`${label} name is required`)
|
|
233
340
|
return name
|
|
234
341
|
}
|
|
342
|
+
|
|
343
|
+
function requiredString(value: string | undefined, label: string): string {
|
|
344
|
+
const text = value?.trim()
|
|
345
|
+
if (!text) throw new Error(`${label} is required`)
|
|
346
|
+
return text
|
|
347
|
+
}
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -1,10 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
import { dirname, resolve } from "path"
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "url"
|
|
3
|
+
import { resolveRevelaRuntime } from "../mcp/runtime-resolver"
|
|
4
|
+
import { workspaceRootFromInput } from "./revela_post_write_notice"
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
interface HookResult {
|
|
7
|
+
ok: boolean
|
|
8
|
+
messages: string[]
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
const controlledStateFile = "DECKS" + ".json"
|
|
9
12
|
|
|
10
|
-
export {
|
|
13
|
+
export async function runPreWriteChecks(input: string): Promise<HookResult> {
|
|
14
|
+
const messages: string[] = []
|
|
15
|
+
|
|
16
|
+
if (input.includes(controlledStateFile)) {
|
|
17
|
+
messages.push(`Revela controls ${controlledStateFile}. Use Revela MCP/runtime tools or file-native narrative files instead of direct ${controlledStateFile} patches.`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const deckTargets = extractDeckHtmlPatchTargets(input)
|
|
21
|
+
if (deckTargets.length > 0) {
|
|
22
|
+
const pluginRoot = resolve(process.env.PLUGIN_ROOT || dirname(dirname(fileURLToPath(import.meta.url))))
|
|
23
|
+
const runtime = resolveRevelaRuntime({ pluginRoot })
|
|
24
|
+
if (!runtime.ok || !runtime.runtimePath) {
|
|
25
|
+
messages.push([
|
|
26
|
+
"Revela deck HTML patch blocked because Codex could not locate the Revela runtime to verify active design rules.",
|
|
27
|
+
...runtime.diagnostics.map((item) => `- ${item}`),
|
|
28
|
+
].join("\n"))
|
|
29
|
+
} else {
|
|
30
|
+
const workspaceRoot = workspaceRootFromInput(input)
|
|
31
|
+
const runtimeModule = await import(pathToFileURL(runtime.runtimePath).href)
|
|
32
|
+
const result = runtimeModule.checkDesignRulesReadiness({ workspaceRoot })
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
messages.push([
|
|
35
|
+
"Revela deck HTML patch blocked: active design rules must be loaded before patching `decks/*.html`.",
|
|
36
|
+
`Reason: ${result.reason ?? "Design rules marker is missing or stale."}`,
|
|
37
|
+
`Active design: ${result.activeDesign ?? "unknown"}`,
|
|
38
|
+
"Next step: call `revela_design_read` with `section: \"rules\"` for this workspace, then retry the patch.",
|
|
39
|
+
"Deck slides must use `<section class=\"slide\" ...><div class=\"slide-canvas\">...</div></section>` with exactly one direct `.slide-canvas` child.",
|
|
40
|
+
].join("\n"))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { ok: messages.length === 0, messages }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function extractDeckHtmlPatchTargets(input: string): string[] {
|
|
49
|
+
const targets = new Set<string>()
|
|
50
|
+
for (const patch of patchPayloads(input)) {
|
|
51
|
+
const pattern = /(?:^\*\*\* Update File: |^\*\*\* Add File: )([^\r\n]*decks\/[^\r\n]+\.html)\s*$/gm
|
|
52
|
+
let match: RegExpExecArray | null
|
|
53
|
+
while ((match = pattern.exec(patch))) targets.add(match[1].trim())
|
|
54
|
+
}
|
|
55
|
+
return [...targets].sort((a, b) => a.localeCompare(b))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function patchPayloads(input: string): string[] {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(input)
|
|
61
|
+
return [
|
|
62
|
+
parsed.patch,
|
|
63
|
+
parsed.args?.patch,
|
|
64
|
+
parsed.tool_input?.patch,
|
|
65
|
+
parsed.toolInput?.patch,
|
|
66
|
+
].filter((item): item is string => typeof item === "string")
|
|
67
|
+
} catch {
|
|
68
|
+
return [input]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (import.meta.main) {
|
|
73
|
+
const input = await new Response(Bun.stdin.stream()).text()
|
|
74
|
+
try {
|
|
75
|
+
const result = await runPreWriteChecks(input)
|
|
76
|
+
if (result.messages.length > 0) console.error(result.messages.join("\n\n---\n\n"))
|
|
77
|
+
process.exit(result.ok ? 0 : 2)
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error("Revela pre-write hook failed to run.")
|
|
80
|
+
console.error(e instanceof Error ? e.message : String(e))
|
|
81
|
+
process.exit(2)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -1,18 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { dirname, resolve } from "path"
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "url"
|
|
3
|
+
import { resolveRevelaRuntime } from "../mcp/runtime-resolver"
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
interface HookResult {
|
|
6
|
+
ok: boolean
|
|
7
|
+
messages: string[]
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
export function extractDeckHtmlTargets(input: string): string[] {
|
|
11
|
+
const targets = new Set<string>()
|
|
12
|
+
const patterns = [
|
|
13
|
+
/\bdecks\/[^\s"'`<>]+\.html\b/g,
|
|
14
|
+
/(?:^\*\*\* Update File: |^\*\*\* Add File: )([^\r\n]+decks\/[^\r\n]+\.html)\s*$/gm,
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
for (const pattern of patterns) {
|
|
18
|
+
let match: RegExpExecArray | null
|
|
19
|
+
while ((match = pattern.exec(input))) {
|
|
20
|
+
targets.add((match[1] ?? match[0]).trim())
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [...targets].sort((a, b) => a.localeCompare(b))
|
|
10
25
|
}
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
export function workspaceRootFromInput(input: string): string {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(input)
|
|
30
|
+
const candidates = [
|
|
31
|
+
parsed.workspaceRoot,
|
|
32
|
+
parsed.cwd,
|
|
33
|
+
parsed.root,
|
|
34
|
+
parsed.tool_input?.workspaceRoot,
|
|
35
|
+
parsed.tool_input?.cwd,
|
|
36
|
+
parsed.toolInput?.workspaceRoot,
|
|
37
|
+
parsed.toolInput?.cwd,
|
|
38
|
+
]
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
if (typeof candidate === "string" && candidate.trim()) return resolve(candidate)
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// Hook payloads are not guaranteed to be JSON across Codex versions.
|
|
44
|
+
}
|
|
45
|
+
return resolve(process.env.CODEX_WORKSPACE_ROOT || process.env.PWD || process.cwd())
|
|
14
46
|
}
|
|
15
47
|
|
|
16
|
-
|
|
48
|
+
export async function runPostWriteChecks(input: string): Promise<HookResult> {
|
|
49
|
+
const messages: string[] = []
|
|
50
|
+
if (/revela-narrative\/.*\.md/.test(input)) {
|
|
51
|
+
messages.push("Revela narrative Markdown changed. Run `revela_markdown_qa` and `revela_compile_narrative` before treating the graph as usable.")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const deckTargets = extractDeckHtmlTargets(input)
|
|
55
|
+
if (deckTargets.length === 0) return { ok: true, messages }
|
|
56
|
+
|
|
57
|
+
const pluginRoot = resolve(process.env.PLUGIN_ROOT || dirname(dirname(fileURLToPath(import.meta.url))))
|
|
58
|
+
const runtime = resolveRevelaRuntime({ pluginRoot })
|
|
59
|
+
if (!runtime.ok || !runtime.runtimePath) {
|
|
60
|
+
messages.push([
|
|
61
|
+
"Revela deck HTML changed, but Codex hook could not locate the Revela runtime to run Artifact QA.",
|
|
62
|
+
...runtime.diagnostics.map((item) => `- ${item}`),
|
|
63
|
+
].join("\n"))
|
|
64
|
+
return { ok: false, messages }
|
|
65
|
+
}
|
|
17
66
|
|
|
18
|
-
|
|
67
|
+
const workspaceRoot = workspaceRootFromInput(input)
|
|
68
|
+
const runtimeModule = await import(pathToFileURL(runtime.runtimePath).href)
|
|
69
|
+
let ok = true
|
|
70
|
+
for (const target of deckTargets) {
|
|
71
|
+
const result = await runtimeModule.runDeckQa({ workspaceRoot, file: target })
|
|
72
|
+
messages.push(result.markdown ?? JSON.stringify(result, null, 2))
|
|
73
|
+
if (!result.ok) ok = false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { ok, messages }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (import.meta.main) {
|
|
80
|
+
const input = await new Response(Bun.stdin.stream()).text()
|
|
81
|
+
try {
|
|
82
|
+
const result = await runPostWriteChecks(input)
|
|
83
|
+
if (result.messages.length > 0) console.error(result.messages.join("\n\n---\n\n"))
|
|
84
|
+
process.exit(result.ok ? 0 : 2)
|
|
85
|
+
} catch (e) {
|
|
86
|
+
console.error("Revela post-write Artifact QA failed to run.")
|
|
87
|
+
console.error(e instanceof Error ? e.message : String(e))
|
|
88
|
+
process.exit(2)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -20,6 +20,8 @@ type RuntimeModule = {
|
|
|
20
20
|
designList(): any
|
|
21
21
|
designRead(input?: any): any
|
|
22
22
|
designActivate(input: any): any
|
|
23
|
+
designCreate(input: any): any
|
|
24
|
+
designValidate(input: any): any
|
|
23
25
|
domainList(): any
|
|
24
26
|
domainRead(input?: any): any
|
|
25
27
|
domainActivate(input: any): any
|
|
@@ -105,13 +107,33 @@ const tools = [
|
|
|
105
107
|
{
|
|
106
108
|
name: "revela_design_read",
|
|
107
109
|
description: "Read Revela design instructions for the active or requested design.",
|
|
108
|
-
inputSchema: objectSchema({
|
|
110
|
+
inputSchema: objectSchema({
|
|
111
|
+
workspaceRoot: stringProp("Optional workspace root. Used to record deck-write hook context when section is rules."),
|
|
112
|
+
name: stringProp("Optional design name."),
|
|
113
|
+
section: stringProp("Optional design section, such as rules, foundation, or chart-rules."),
|
|
114
|
+
}),
|
|
109
115
|
},
|
|
110
116
|
{
|
|
111
117
|
name: "revela_design_activate",
|
|
112
118
|
description: "Activate a Revela design for future deck planning and artifact generation.",
|
|
113
119
|
inputSchema: objectSchema({ name: requiredStringProp("Design name to activate.") }, ["name"]),
|
|
114
120
|
},
|
|
121
|
+
{
|
|
122
|
+
name: "revela_design_create",
|
|
123
|
+
description: "Create and validate a local Revela design package from complete DESIGN.md and preview.html content.",
|
|
124
|
+
inputSchema: objectSchema({
|
|
125
|
+
name: requiredStringProp("Design name in kebab-case."),
|
|
126
|
+
base: stringProp("Optional base design used as structural scaffold."),
|
|
127
|
+
designMd: requiredStringProp("Complete DESIGN.md content."),
|
|
128
|
+
previewHtml: requiredStringProp("Complete preview.html content."),
|
|
129
|
+
overwrite: booleanProp("Whether to replace an existing local design package. Defaults to false."),
|
|
130
|
+
}, ["name", "designMd", "previewHtml"]),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: "revela_design_validate",
|
|
134
|
+
description: "Validate a local Revela design package.",
|
|
135
|
+
inputSchema: objectSchema({ name: requiredStringProp("Design name to validate.") }, ["name"]),
|
|
136
|
+
},
|
|
115
137
|
{
|
|
116
138
|
name: "revela_domain_list",
|
|
117
139
|
description: "List installed Revela narrative domains and the active domain.",
|
|
@@ -254,6 +276,8 @@ async function callTool(name: string, args: any): Promise<any> {
|
|
|
254
276
|
if (name === "revela_design_list") return r.designList()
|
|
255
277
|
if (name === "revela_design_read") return r.designRead(args)
|
|
256
278
|
if (name === "revela_design_activate") return r.designActivate(args)
|
|
279
|
+
if (name === "revela_design_create") return r.designCreate(args)
|
|
280
|
+
if (name === "revela_design_validate") return r.designValidate(args)
|
|
257
281
|
if (name === "revela_domain_list") return r.domainList()
|
|
258
282
|
if (name === "revela_domain_read") return r.domainRead(args)
|
|
259
283
|
if (name === "revela_domain_activate") return r.domainActivate(args)
|
|
@@ -10,11 +10,25 @@ Use this skill when the user asks about Revela designs or when generating deck H
|
|
|
10
10
|
## Workflow
|
|
11
11
|
|
|
12
12
|
1. Call `revela_design_list` to inspect installed designs.
|
|
13
|
-
2. Call `revela_design_read`
|
|
13
|
+
2. Call `revela_design_read` with `section: "rules"` before writing or patching `decks/*.html`; this records the Codex hook context required for deck writes.
|
|
14
14
|
3. When the user asks to switch designs for future work, call `revela_design_activate` with the requested design name, then read the active design again.
|
|
15
15
|
4. For one-off deck generation with a requested design, read that design by name and pass `designName` to `revela_create_deck_foundation` without changing active design unless the user asked to switch.
|
|
16
16
|
5. Use the current simplified built-in design grammar: `box`, `text-panel`, `media`, `echart-panel`, `data-table`, `steps`, `roadmap-horizontal`, `roadmap-vertical`, `hero`, `stat-card`, `quote`, `toc`, `page-number`, and `brand-watermark`.
|
|
17
17
|
6. Fetch chart/design guidance before creating ECharts or complex layouts.
|
|
18
18
|
7. Do not invent unsupported component names.
|
|
19
19
|
|
|
20
|
+
Deck HTML must keep exactly one direct `.slide-canvas` child inside every `<section class="slide" ...>`; place `.page` or layout containers inside `.slide-canvas`, not directly under `.slide`.
|
|
21
|
+
|
|
20
22
|
Design changes are visual/artifact-level unless they change claim meaning, evidence boundaries, decision, or recommendation.
|
|
23
|
+
|
|
24
|
+
## Creating Or Editing Designs
|
|
25
|
+
|
|
26
|
+
When the user asks to create a new design, use `starter` as the default base design unless they specify another base. Interview the user before saving anything: collect visual references such as images, webpages, brands, decks, or text descriptions, plus must-have and must-avoid constraints. Summarize the design brief and visual schema, then wait for the user to confirm before creating files.
|
|
27
|
+
|
|
28
|
+
After confirmation, read the base design with `revela_design_read`. Generate complete `DESIGN.md` and complete `preview.html` content, then call `revela_design_create`. For edits to an existing design, read the existing design first, preserve useful layout/component coverage, and call `revela_design_create` with `overwrite: true` only after the user confirms the edit brief. Always call `revela_design_validate` after creation or overwrite.
|
|
29
|
+
|
|
30
|
+
`DESIGN.md` must include frontmatter with `name`, `description`, `author`, and `version`, plus valid marker blocks for `@design:foundation`, `@design:rules`, at least one `@layout`, and at least one `@component`.
|
|
31
|
+
|
|
32
|
+
`preview.html` must be self-contained and directly openable in a browser. Every `<section class="slide">` must include `slide-qa` and exactly one direct `.slide-canvas` child. Include a cover slide with `data-slide-role="cover"`, a closing slide with `data-slide-role="closing"`, and a visible sample for every `@component:*` using `data-preview-component="<component-name>"`.
|
|
33
|
+
|
|
34
|
+
Do not automatically activate a newly created design. Report the saved path and tell the user they can activate it with `revela_design_activate`.
|
|
@@ -16,6 +16,6 @@ Use this skill when the user asks to export a Revela deck.
|
|
|
16
16
|
|
|
17
17
|
`revela_run_deck_qa`, `revela_export_pdf`, and `revela_export_pptx` may launch a browser. In sandboxed Codex sessions, request user-approved command escalation when the browser cannot start inside the default sandbox.
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Post-write hooks and explicit QA tools surface Artifact QA failures. If the latest visible QA result has hard errors, repair them before treating the deck as export-ready.
|
|
20
20
|
|
|
21
21
|
Do not treat narrative gaps as export blockers unless they affect technical artifact validity or data safety.
|
|
@@ -23,8 +23,8 @@ Use this skill when the user asks to make, generate, or update a Revela deck.
|
|
|
23
23
|
5. Report deck-plan diagnostics before artifact generation, including stale narrative hashes, missing slide projections, missing evidence trace, caveats, or malformed plan files.
|
|
24
24
|
6. Do not start HTML generation from narrative alone unless the user explicitly asks for a throwaway diagnostic smoke deck.
|
|
25
25
|
7. For new HTML files, call `revela_create_deck_foundation`.
|
|
26
|
-
8. Read active design guidance with `revela_design_list` and `revela_design_read`
|
|
27
|
-
9. Patch slides into the foundation between Revela slide markers. Preserve positive 1-based `data-slide-index` values.
|
|
26
|
+
8. Read active design guidance with `revela_design_list` and `revela_design_read` using `section: "rules"` before writing `decks/*.html`; fetch layouts/components/chart rules as needed. If the user asks to switch designs persistently, call `revela_design_activate`; if they ask for a one-off design, read that design by name and pass `designName` to `revela_create_deck_foundation`.
|
|
27
|
+
9. Patch slides into the foundation between Revela slide markers. Preserve positive 1-based `data-slide-index` values. Every slide must use `<section class="slide" ...>` with exactly one direct `.slide-canvas` child.
|
|
28
28
|
10. Generate chapter by chapter. Keep the HTML valid after each write.
|
|
29
29
|
11. After every HTML write, call `revela_run_deck_qa` and repair hard errors before review or export.
|
|
30
30
|
|
|
@@ -17,7 +17,7 @@ Use this skill when the user asks to review, inspect, diagnose, or refine a gene
|
|
|
17
17
|
6. Do not call `revela_run_deck_qa`, `revela_compile_narrative`, or `revela_read_deck_plan` separately for a normal Review UI open.
|
|
18
18
|
7. Call `revela_run_deck_qa` separately only for focused low-level artifact QA, after a repair, or when the user explicitly asks for QA detail.
|
|
19
19
|
8. Separate technical blockers from narrative/evidence diagnostics.
|
|
20
|
-
9. Pure visual/layout/export fixes may patch artifacts directly when the user asks for a change
|
|
20
|
+
9. Pure visual/layout/export fixes may patch artifacts directly when the user asks for a change, but read active design rules first with `revela_design_read` using `section: "rules"`. Meaning changes must update `revela-narrative/` first.
|
|
21
21
|
|
|
22
22
|
## Generated Visual Assets
|
|
23
23
|
|
|
@@ -34,6 +34,7 @@ Use this skill when the user asks to review, inspect, diagnose, or refine a gene
|
|
|
34
34
|
- `revela_review_deck_open` opens the local Review server from the MCP process and uses the Codex `codex-exec` bridge for Insight and Comment/Apply Fix. It returns URL/token/open state and basic file metadata, not aggregate diagnostics.
|
|
35
35
|
- `revela_run_deck_qa` may need browser-launch permission in Codex sandboxed sessions.
|
|
36
36
|
- Repair hard QA errors before treating a deck as review-ready.
|
|
37
|
+
- Deck slides must use `<section class="slide" ...>` with exactly one direct `.slide-canvas` child; missing, nested, or duplicate slide canvases are hard artifact failures.
|
|
37
38
|
- Text clipping should usually be fixed with typography and spacing changes, not by deleting evidence or changing claim meaning.
|
|
38
39
|
- A warning that a smoke/development artifact is not the active legacy deck target is non-blocking when the requested file passes hard artifact checks.
|
|
39
40
|
|