@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 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. Initialize the narrative from local materials. Init grounds the narrative in the workspace and surfaces gaps; it does not replace the research step.
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
- 4. Research the gaps and bind only source-supported evidence into the narrative.
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
- 5. Read Story before rendering to inspect the claim flow, evidence support, caveats, unsupported scope, and open gaps.
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
- 6. Create or update the deck plan before generating HTML so slide order, chapter structure, evidence trace, caveats, and visual intent are explicit.
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
- 7. Make an HTML deck from the current deck plan and canonical narrative.
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
- 8. Review the generated deck for traceability, diagnostics, and targeted edits.
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
- 9. Export a PDF after deck QA passes.
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
- 10. Export an editable PPTX after deck QA passes.
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. 从本地材料初始化 narrativeInit 负责基于 workspace 做 grounding 并暴露 gap;它不替代 research 步骤。
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
- 4. 针对 gap 做 research,并且只把来源明确支持的 evidence 绑定回 narrative。
144
+ 5. 针对 gap 做 research,并且只把来源明确支持的 evidence 绑定回 narrative。
133
145
 
134
146
  ```text
135
147
  revela,research 当前 gaps,只绑定 source-supported evidence。
136
148
  ```
137
149
 
138
- 5. 生成 deck 前先读 Story,检查 claim flow、证据支撑、caveats、unsupported scope 和 open gaps。
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
- 6. 先创建或更新 deck plan,明确 slide 顺序、章节结构、evidence trace、caveats 和 visual intent,再生成 HTML。
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
- 7. 基于当前 deck plan 和 canonical narrative 生成 HTML deck。
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
- 8. Review 生成后的 deck,检查 traceability、diagnostics,并做定向修改。
168
+ 9. Review 生成后的 deck,检查 traceability、diagnostics,并做定向修改。
157
169
 
158
170
  ```text
159
171
  revela,review 生成好的 deck。
160
172
  ```
161
173
 
162
- 9. QA 通过后导出 PDF。
174
+ 10. QA 通过后导出 PDF。
163
175
 
164
176
  ```text
165
177
  revela,把 deck export 成 PDF。
166
178
  ```
167
179
 
168
- 10. QA 通过后导出可编辑 PPTX。
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>
@@ -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.
@@ -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 sectionTagPattern = /<section\b([^>]*)>/gi
245
+ const sectionPattern = /<section\b([^>]*)>([\s\S]*?)<\/section>/gi
221
246
  let match: RegExpExecArray | null
222
- while ((match = sectionTagPattern.exec(html))) {
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
+ }
@@ -61,7 +61,7 @@ export function buildEditPrompt(payload: EditCommentPayload): string {
61
61
  drop: payload.drop,
62
62
  }
63
63
  const qaInstruction = payload.suppressAutomaticArtifactQa
64
- ? `- Do not run artifact QA after this edit and do not keep editing just to satisfy post-write QA. The Review UI will refresh from the deck file version change; QA can be run later through an explicit Review, QA, or export workflow.`
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 missingCanvas = slides.findIndex((slide) => !slide.querySelector(".slide-canvas"))
31
- if (missingCanvas >= 0) {
32
- return {
33
- isDeck: false,
34
- slideCount: slides.length,
35
- reason: `.slide ${missingCanvas + 1} has no .slide-canvas`,
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
- if (indexValues.length > 0) {
44
- if (indexValues.length !== slides.length) {
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: "some slides have data-slide-index and some do not",
58
+ reason: `slide ${i + 1} has invalid data-slide-index "${raw}"`,
49
59
  }
50
60
  }
51
-
52
- const seen = new Set<number>()
53
- for (let i = 0; i < indexValues.length; i++) {
54
- const parsed = Number(indexValues[i])
55
- if (!Number.isInteger(parsed) || parsed < 1) {
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
- if (seen.has(parsed)) {
63
- return {
64
- isDeck: false,
65
- slideCount: slides.length,
66
- reason: `duplicate data-slide-index "${parsed}"`,
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
- return { isDeck: true, slideCount: slides.length, reason: "valid deck contract" }
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 .slide-canvas absolute position by walking the offsetParent chain.
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 canvas = slide.querySelector(".slide-canvas") as HTMLElement | null
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 = canvas
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: canvas.offsetWidth, height: canvas.offsetHeight }
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 canvas = slide.querySelector(".slide-canvas") as HTMLElement | null
341
- if (!canvas) return null
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,
@@ -1,6 +1,16 @@
1
- import { existsSync } from "fs"
2
- import { resolve } from "path"
3
- import { activeDesign, activateDesign, getDesignSkillMd, listDesigns, seedBuiltinDesigns } from "../design/designs"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.17.16",
3
+ "version": "0.17.18",
4
4
  "description": "OpenCode plugin for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -2,7 +2,7 @@
2
2
  "mcpServers": {
3
3
  "revela": {
4
4
  "command": "npx",
5
- "args": ["-y", "@cyber-dash-tech/revela@0.17.16", "mcp"]
5
+ "args": ["-y", "@cyber-dash-tech/revela@0.17.18", "mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -19,7 +19,7 @@
19
19
  {
20
20
  "type": "command",
21
21
  "command": "bun ${PLUGIN_ROOT}/hooks/revela_post_write_notice.ts",
22
- "statusMessage": "Checking Revela QA reminders"
22
+ "statusMessage": "Running Revela post-write QA"
23
23
  }
24
24
  ]
25
25
  }
@@ -1,10 +1,83 @@
1
- const input = await new Response(Bun.stdin.stream()).text()
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
- if (input.includes("DECKS.json")) {
4
- console.error("Revela controls DECKS.json. Use Revela MCP/runtime tools or file-native narrative files instead of direct DECKS.json patches.")
5
- process.exit(2)
6
+ interface HookResult {
7
+ ok: boolean
8
+ messages: string[]
6
9
  }
7
10
 
8
- process.exit(0)
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
- const input = await new Response(Bun.stdin.stream()).text()
2
- const notices: string[] = []
1
+ import { dirname, resolve } from "path"
2
+ import { fileURLToPath, pathToFileURL } from "url"
3
+ import { resolveRevelaRuntime } from "../mcp/runtime-resolver"
3
4
 
4
- if (/revela-narrative\/.*\.md/.test(input)) {
5
- notices.push("Revela narrative Markdown changed. Run `revela_markdown_qa` and `revela_compile_narrative` before treating the graph as usable.")
5
+ interface HookResult {
6
+ ok: boolean
7
+ messages: string[]
6
8
  }
7
9
 
8
- if (/decks\/.*\.html/.test(input)) {
9
- notices.push("Revela deck HTML changed. Run `revela_run_deck_qa` before review or export.")
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
- if (notices.length > 0) {
13
- console.error(notices.join("\n"))
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
- process.exit(0)
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
- export {}
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({ name: stringProp("Optional design name.") }),
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` for the active or requested design.
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
- Deck writes run post-write QA automatically. Do not run artifact QA as a pre-export blocker unless the user explicitly asks for diagnostics.
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` when choosing layouts/components. 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.
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. Meaning changes must update `revela-narrative/` first.
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