@cyber-dash-tech/revela 0.17.22 → 0.17.23

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.
@@ -1,11 +1,9 @@
1
1
  /**
2
2
  * DesignManager — manage revela visual design templates.
3
3
  *
4
- * Designs are stored in ~/.config/revela/designs/<name>/.
4
+ * User designs are stored in ~/.config/revela/designs/<name>/.
5
+ * Built-in designs are shipped read-only with this package under designs/<name>/.
5
6
  * Each design directory contains DESIGN.md (required) and optionally preview.html.
6
- *
7
- * Built-in designs are shipped with the npm package under designs/ and seeded
8
- * to the config directory on first run.
9
7
  */
10
8
 
11
9
  import {
@@ -152,21 +150,46 @@ export function parseDesignFile(filePath: string): DesignInfo | null {
152
150
  // Public API
153
151
  // ---------------------------------------------------------------------------
154
152
 
155
- /** List installed designs, sorted by name. Internal designs are hidden by default. */
153
+ function designDirHasPackage(dir: string): boolean {
154
+ return existsSync(dir) && statSync(dir).isDirectory() && existsSync(join(dir, "DESIGN.md"))
155
+ }
156
+
157
+ function resolveDesignDir(nameInput?: string): string | null {
158
+ const name = normalizeDesignName(nameInput || activeDesign())
159
+ const userDir = join(DESIGNS_DIR, name)
160
+ if (designDirHasPackage(userDir)) return userDir
161
+
162
+ const bundledDir = join(SEED_DIR, name)
163
+ if (designDirHasPackage(bundledDir)) return bundledDir
164
+
165
+ return null
166
+ }
167
+
168
+ function readDesignsFromDir(root: string): Map<string, DesignInfo> {
169
+ const designs = new Map<string, DesignInfo>()
170
+ if (!existsSync(root)) return designs
171
+
172
+ for (const entry of readdirSync(root).sort()) {
173
+ const dir = join(root, entry)
174
+ if (!designDirHasPackage(dir)) continue
175
+ const info = parseDesignFile(join(dir, "DESIGN.md"))
176
+ if (info) designs.set(entry, info)
177
+ }
178
+ return designs
179
+ }
180
+
181
+ /** List available designs, sorted by name. User designs override bundled designs with the same name. Internal designs are hidden by default. */
156
182
  export function listDesigns(options: ListDesignsOptions = {}): DesignInfo[] {
157
- if (!existsSync(DESIGNS_DIR)) return []
158
- const results: DesignInfo[] = []
159
183
  const includeInternal = options.includeInternal ?? false
184
+ const available = readDesignsFromDir(SEED_DIR)
160
185
 
161
- for (const entry of readdirSync(DESIGNS_DIR).sort()) {
162
- const dir = join(DESIGNS_DIR, entry)
163
- if (!statSync(dir).isDirectory()) continue
164
- const mdPath = join(dir, "DESIGN.md")
165
- if (!existsSync(mdPath)) continue
166
- const info = parseDesignFile(mdPath)
167
- if (info && (includeInternal || !info.internal)) results.push(info)
186
+ for (const [entry, info] of readDesignsFromDir(DESIGNS_DIR)) {
187
+ available.set(entry, info)
168
188
  }
169
- return results
189
+
190
+ return [...available.values()]
191
+ .filter((info) => includeInternal || !info.internal)
192
+ .sort((a, b) => a.name.localeCompare(b.name))
170
193
  }
171
194
 
172
195
  /** Get the name of the currently active design. */
@@ -187,11 +210,12 @@ export function activateDesign(name: string): void {
187
210
 
188
211
  /** Get the skill text body from a design's DESIGN.md. */
189
212
  export function getDesignSkillMd(name?: string): string {
190
- const designName = name || activeDesign()
191
- const mdPath = join(DESIGNS_DIR, designName, "DESIGN.md")
192
- if (!existsSync(mdPath)) {
213
+ const designName = normalizeDesignName(name || activeDesign())
214
+ const designDir = resolveDesignDir(designName)
215
+ if (!designDir) {
193
216
  throw new Error(`Design '${designName}' is not installed`)
194
217
  }
218
+ const mdPath = join(designDir, "DESIGN.md")
195
219
  const info = parseDesignFile(mdPath)
196
220
  if (!info) {
197
221
  throw new Error(`Failed to parse DESIGN.md for '${designName}'`)
@@ -202,9 +226,8 @@ export function getDesignSkillMd(name?: string): string {
202
226
  /** Resolve a design's preview.html path. Throws if the design is not installed. */
203
227
  export function resolveDesignPreview(name?: string): DesignPreviewInfo {
204
228
  const designName = normalizeDesignName(name || activeDesign())
205
- const designDir = join(DESIGNS_DIR, designName)
206
- const mdPath = join(designDir, "DESIGN.md")
207
- if (!existsSync(designDir) || !existsSync(mdPath)) {
229
+ const designDir = resolveDesignDir(designName)
230
+ if (!designDir) {
208
231
  throw new Error(`Design '${designName}' is not installed`)
209
232
  }
210
233
 
@@ -388,12 +411,15 @@ function hasFixedSizeCssRule(html: string, className: "slide-canvas"): boolean {
388
411
  /** Validate a local design package for the minimum Revela design contract. */
389
412
  export function validateDesignPackage(nameInput: string): ValidateDesignPackageResult {
390
413
  let name = nameInput
414
+ let hasValidName = true
391
415
  try {
392
416
  name = normalizeDesignName(nameInput)
393
417
  } catch {
418
+ hasValidName = false
394
419
  // validateDesignPackageAt records the invalid-name error.
395
420
  }
396
- return validateDesignPackageAt(nameInput, join(DESIGNS_DIR, name))
421
+ const dir = hasValidName ? resolveDesignDir(name) || join(DESIGNS_DIR, name) : join(DESIGNS_DIR, name)
422
+ return validateDesignPackageAt(nameInput, dir)
397
423
  }
398
424
 
399
425
  function validateDesignPackageAt(nameInput: string, dir: string): ValidateDesignPackageResult {
@@ -493,6 +519,25 @@ export interface DesignSections {
493
519
  hasMarkers: boolean
494
520
  }
495
521
 
522
+ export interface DesignInventoryLayout {
523
+ name: string
524
+ qa: boolean
525
+ description: string
526
+ }
527
+
528
+ export interface DesignInventoryComponent {
529
+ name: string
530
+ description: string
531
+ }
532
+
533
+ export interface DesignInventory {
534
+ name: string
535
+ sections: string[]
536
+ layouts: DesignInventoryLayout[]
537
+ components: DesignInventoryComponent[]
538
+ hasMarkers: boolean
539
+ }
540
+
496
541
  /**
497
542
  * Parse a DESIGN.md body (no frontmatter) into sections, layouts, and components
498
543
  * using the three-layer HTML comment marker convention:
@@ -566,7 +611,7 @@ export function generateComponentIndex(components: Record<string, string>): stri
566
611
  "|---|---|",
567
612
  ...rows,
568
613
  "",
569
- "_Use `revela-designs` tool with `action: \"read\"` and `component: \"<name>\"` to get full CSS/HTML for any component._",
614
+ "_Use `revela_design_read_component` with `component: \"<name>\"` to get full CSS/HTML for any component._",
570
615
  ].join("\n")
571
616
  }
572
617
 
@@ -598,10 +643,47 @@ export function generateLayoutIndex(layouts: Record<string, LayoutInfo>): string
598
643
  "|---|---|---|",
599
644
  ...rows,
600
645
  "",
601
- "_Use `revela-designs` tool with `action: \"read\"` and `layout: \"<name>\"` to get full HTML/CSS for any layout._",
646
+ "_Use `revela_design_read_layout` with `layout: \"<name>\"` to get full HTML/CSS for any layout._",
602
647
  ].join("\n")
603
648
  }
604
649
 
650
+ export function getDesignInventory(designName?: string): DesignInventory {
651
+ const name = normalizeDesignName(designName || activeDesign())
652
+ const designDir = resolveDesignDir(name)
653
+ if (!designDir) {
654
+ throw new Error(`Design '${name}' is not installed`)
655
+ }
656
+ const mdPath = join(designDir, "DESIGN.md")
657
+ const text = readFileSync(mdPath, "utf-8")
658
+ const { body } = parseFrontmatter(text)
659
+ const { sections, layouts, components, hasMarkers } = parseDesignSections(body)
660
+
661
+ return {
662
+ name,
663
+ sections: Object.keys(sections),
664
+ layouts: Object.entries(layouts).map(([layoutName, layout]) => ({
665
+ name: layoutName,
666
+ qa: layout.qa,
667
+ description: designBlockDescription(layout.content),
668
+ })),
669
+ components: Object.entries(components).map(([componentName, content]) => ({
670
+ name: componentName,
671
+ description: designBlockDescription(content),
672
+ })),
673
+ hasMarkers,
674
+ }
675
+ }
676
+
677
+ function designBlockDescription(body: string): string {
678
+ const firstLine = body
679
+ .split("\n")
680
+ .map((line) => line.trim())
681
+ .find((line) => line && !line.startsWith("<!--") && !line.startsWith("```"))
682
+ return firstLine
683
+ ? firstLine.replace(/^#+\s*/, "").replace(/\(.*?\)/, "").trim()
684
+ : ""
685
+ }
686
+
605
687
  /**
606
688
  * Get the raw text of one or more named layouts from a DESIGN.md.
607
689
  * @param layoutNames - Comma-separated layout names or an array.
@@ -611,11 +693,12 @@ export function getDesignLayout(
611
693
  layoutNames: string | string[],
612
694
  designName?: string,
613
695
  ): string {
614
- const name = designName || activeDesign()
615
- const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
616
- if (!existsSync(mdPath)) {
696
+ const name = normalizeDesignName(designName || activeDesign())
697
+ const designDir = resolveDesignDir(name)
698
+ if (!designDir) {
617
699
  throw new Error(`Design '${name}' is not installed`)
618
700
  }
701
+ const mdPath = join(designDir, "DESIGN.md")
619
702
  const text = readFileSync(mdPath, "utf-8")
620
703
  const { body } = parseFrontmatter(text)
621
704
  const { layouts, hasMarkers } = parseDesignSections(body)
@@ -644,11 +727,12 @@ export function getDesignLayout(
644
727
  * Throws if the design is not installed or the section doesn't exist.
645
728
  */
646
729
  export function getDesignSection(sectionName: string, designName?: string): string {
647
- const name = designName || activeDesign()
648
- const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
649
- if (!existsSync(mdPath)) {
730
+ const name = normalizeDesignName(designName || activeDesign())
731
+ const designDir = resolveDesignDir(name)
732
+ if (!designDir) {
650
733
  throw new Error(`Design '${name}' is not installed`)
651
734
  }
735
+ const mdPath = join(designDir, "DESIGN.md")
652
736
  const text = readFileSync(mdPath, "utf-8")
653
737
  const { body } = parseFrontmatter(text)
654
738
  const { sections, hasMarkers } = parseDesignSections(body)
@@ -671,11 +755,12 @@ export function getDesignComponent(
671
755
  componentNames: string | string[],
672
756
  designName?: string,
673
757
  ): string {
674
- const name = designName || activeDesign()
675
- const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
676
- if (!existsSync(mdPath)) {
758
+ const name = normalizeDesignName(designName || activeDesign())
759
+ const designDir = resolveDesignDir(name)
760
+ if (!designDir) {
677
761
  throw new Error(`Design '${name}' is not installed`)
678
762
  }
763
+ const mdPath = join(designDir, "DESIGN.md")
679
764
  const text = readFileSync(mdPath, "utf-8")
680
765
  const { body } = parseFrontmatter(text)
681
766
  const { components, hasMarkers } = parseDesignSections(body)
@@ -742,8 +827,7 @@ export async function installDesign(
742
827
  // ---------------------------------------------------------------------------
743
828
 
744
829
  function designExists(name: string): boolean {
745
- const dir = join(DESIGNS_DIR, name)
746
- return existsSync(dir) && existsSync(join(dir, "DESIGN.md"))
830
+ return resolveDesignDir(name) !== null
747
831
  }
748
832
 
749
833
  function installFromPath(srcPath: string, name?: string): string {
@@ -814,13 +898,13 @@ export interface DesignClassVocabulary {
814
898
  * Falls back to UNIVERSAL_CLASSES-only when the design has no markers.
815
899
  */
816
900
  export function extractDesignClasses(designName?: string): DesignClassVocabulary {
817
- const name = designName || activeDesign()
818
- const mdPath = join(DESIGNS_DIR, name, "DESIGN.md")
819
-
820
- if (!existsSync(mdPath)) {
901
+ const name = normalizeDesignName(designName || activeDesign())
902
+ const designDir = resolveDesignDir(name)
903
+ if (!designDir) {
821
904
  return { classes: new Set(UNIVERSAL_CLASSES), prefixExemptions: DEFAULT_PREFIX_EXEMPTIONS }
822
905
  }
823
906
 
907
+ const mdPath = join(designDir, "DESIGN.md")
824
908
  const raw = readFileSync(mdPath, "utf-8")
825
909
  const { body } = parseFrontmatter(raw)
826
910
  const { sections, layouts, components, hasMarkers } = parseDesignSections(body)
@@ -6,6 +6,9 @@ import {
6
6
  activateDesign,
7
7
  createDesignDraftPackage,
8
8
  createDesignPackage,
9
+ getDesignComponent,
10
+ getDesignInventory,
11
+ getDesignLayout,
9
12
  getDesignSection,
10
13
  getDesignSkillMd,
11
14
  installDesignDraftPackage,
@@ -59,6 +62,20 @@ export interface RuntimeDesignReadInput {
59
62
  section?: string
60
63
  }
61
64
 
65
+ export interface RuntimeDesignInventoryInput {
66
+ name?: string
67
+ }
68
+
69
+ export interface RuntimeDesignLayoutReadInput {
70
+ name?: string
71
+ layout: string | string[]
72
+ }
73
+
74
+ export interface RuntimeDesignComponentReadInput {
75
+ name?: string
76
+ component: string | string[]
77
+ }
78
+
62
79
  export interface RuntimeDesignCreateInput {
63
80
  name: string
64
81
  base?: string
@@ -212,7 +229,6 @@ export async function reviewDeckOpen(input: ReviewDeckOpenInput) {
212
229
  }
213
230
 
214
231
  export function designList() {
215
- seedBuiltinDesigns()
216
232
  return {
217
233
  ok: true,
218
234
  activeDesign: activeDesign(),
@@ -226,7 +242,6 @@ export function designList() {
226
242
  }
227
243
 
228
244
  export function designRead(input: RuntimeDesignReadInput = {}) {
229
- seedBuiltinDesigns()
230
245
  const name = input.name || activeDesign()
231
246
  if (input.section) {
232
247
  const markdown = getDesignSection(input.section, name)
@@ -246,6 +261,33 @@ export function designRead(input: RuntimeDesignReadInput = {}) {
246
261
  }
247
262
  }
248
263
 
264
+ export function designInventory(input: RuntimeDesignInventoryInput = {}) {
265
+ return {
266
+ ok: true,
267
+ ...getDesignInventory(input.name || activeDesign()),
268
+ }
269
+ }
270
+
271
+ export function designReadLayout(input: RuntimeDesignLayoutReadInput) {
272
+ const name = input.name || activeDesign()
273
+ return {
274
+ ok: true,
275
+ name,
276
+ layout: input.layout,
277
+ markdown: getDesignLayout(input.layout, name),
278
+ }
279
+ }
280
+
281
+ export function designReadComponent(input: RuntimeDesignComponentReadInput) {
282
+ const name = input.name || activeDesign()
283
+ return {
284
+ ok: true,
285
+ name,
286
+ component: input.component,
287
+ markdown: getDesignComponent(input.component, name),
288
+ }
289
+ }
290
+
249
291
  export function designCreate(input: RuntimeDesignCreateInput) {
250
292
  seedBuiltinDesigns()
251
293
  return createDesignPackage({
@@ -258,7 +300,6 @@ export function designCreate(input: RuntimeDesignCreateInput) {
258
300
  }
259
301
 
260
302
  export function designValidate(input: RuntimeNameInput) {
261
- seedBuiltinDesigns()
262
303
  return validateDesignPackage(requiredName(input, "design"))
263
304
  }
264
305
 
@@ -296,7 +337,6 @@ export interface DesignRulesReadinessResult {
296
337
  const DESIGN_RULES_MARKER_TTL_MS = 8 * 60 * 60 * 1000
297
338
 
298
339
  export function checkDesignRulesReadiness(input: RuntimeWorkspaceInput = {}): DesignRulesReadinessResult {
299
- seedBuiltinDesigns()
300
340
  const workspaceRoot = root(input.workspaceRoot)
301
341
  const design = activeDesign()
302
342
  const rules = getDesignSection("rules", design)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.17.22",
3
+ "version": "0.17.23",
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.22", "mcp"]
5
+ "args": ["-y", "@cyber-dash-tech/revela@0.17.23", "mcp"]
6
6
  }
7
7
  }
8
8
  }
@@ -19,6 +19,9 @@ type RuntimeModule = {
19
19
  exportPptx(input: any): Promise<any>
20
20
  designList(): any
21
21
  designRead(input?: any): any
22
+ designInventory(input?: any): any
23
+ designReadLayout(input: any): any
24
+ designReadComponent(input: any): any
22
25
  designActivate(input: any): any
23
26
  designCreate(input: any): any
24
27
  designValidate(input: any): any
@@ -125,6 +128,29 @@ const tools = [
125
128
  section: stringProp("Optional design section, such as rules, foundation, or chart-rules."),
126
129
  }),
127
130
  },
131
+ {
132
+ name: "revela_design_inventory",
133
+ description: "List the active or requested Revela design sections, layouts, and components.",
134
+ inputSchema: objectSchema({
135
+ name: stringProp("Optional design name."),
136
+ }),
137
+ },
138
+ {
139
+ name: "revela_design_read_layout",
140
+ description: "Read one or more layout blocks from the active or requested Revela design.",
141
+ inputSchema: objectSchema({
142
+ name: stringProp("Optional design name."),
143
+ layout: stringOrArrayProp("Layout name, comma-separated names, or an array of layout names."),
144
+ }, ["layout"]),
145
+ },
146
+ {
147
+ name: "revela_design_read_component",
148
+ description: "Read one or more component blocks from the active or requested Revela design.",
149
+ inputSchema: objectSchema({
150
+ name: stringProp("Optional design name."),
151
+ component: stringOrArrayProp("Component name, comma-separated names, or an array of component names."),
152
+ }, ["component"]),
153
+ },
128
154
  {
129
155
  name: "revela_design_activate",
130
156
  description: "Activate a Revela design for future deck planning and artifact generation.",
@@ -394,6 +420,9 @@ async function callTool(name: string, args: any): Promise<any> {
394
420
  if (name === "revela_export_pptx") return r.exportPptx(args)
395
421
  if (name === "revela_design_list") return r.designList()
396
422
  if (name === "revela_design_read") return r.designRead(args)
423
+ if (name === "revela_design_inventory") return r.designInventory(args)
424
+ if (name === "revela_design_read_layout") return r.designReadLayout(args)
425
+ if (name === "revela_design_read_component") return r.designReadComponent(args)
397
426
  if (name === "revela_design_activate") return r.designActivate(args)
398
427
  if (name === "revela_design_create") return r.designCreate(args)
399
428
  if (name === "revela_design_validate") return r.designValidate(args)
@@ -458,6 +487,16 @@ function arrayProp(description: string) {
458
487
  return { type: "array", items: { type: "string" }, description }
459
488
  }
460
489
 
490
+ function stringOrArrayProp(description: string) {
491
+ return {
492
+ anyOf: [
493
+ { type: "string" },
494
+ { type: "array", items: { type: "string" } },
495
+ ],
496
+ description,
497
+ }
498
+ }
499
+
461
500
  function arrayObjectProp(description: string) {
462
501
  return {
463
502
  type: "array",
@@ -11,11 +11,13 @@ Use this skill when the user asks about Revela designs or when generating deck H
11
11
 
12
12
  1. Call `revela_design_list` to inspect installed designs.
13
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
- 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
- 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
- 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
- 6. Fetch chart/design guidance before creating ECharts or complex layouts.
18
- 7. Do not invent unsupported component names.
14
+ 3. Call `revela_design_inventory` before authoring or repairing `deck-plan/` so planned layout/component names come from the active design.
15
+ 4. Read required details with `revela_design_read_layout` and `revela_design_read_component` before writing slide HTML that uses those layouts/components.
16
+ 5. 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.
17
+ 6. For one-off deck generation with a requested design, read that design by name, call `revela_design_inventory` with that name, and pass `designName` to `revela_create_deck_foundation` without changing active design unless the user asked to switch.
18
+ 7. 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`.
19
+ 8. Fetch chart/design guidance before creating ECharts or complex layouts.
20
+ 9. Do not invent unsupported component names.
19
21
 
20
22
  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
23
 
@@ -18,15 +18,16 @@ Use this skill when the user asks to make, generate, or update a Revela deck.
18
18
 
19
19
  1. Call `revela_compile_narrative` and `revela_markdown_qa`.
20
20
  2. Report narrative and Markdown diagnostics, but treat only malformed/unsafe files and technical artifact validity as hard blockers.
21
- 3. Call `revela_read_deck_plan` as the required deck-plan preflight before any HTML generation.
22
- 4. If `deck-plan/` is missing or incomplete, author or repair `deck-plan/index.md` and `deck-plan/slides/*.md` before calling `revela_create_deck_foundation`.
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
- 6. Do not start HTML generation from narrative alone unless the user explicitly asks for a throwaway diagnostic smoke deck.
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` 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
- 10. Generate chapter by chapter. Keep the HTML valid after each write.
29
- 11. After every HTML write, call `revela_run_deck_qa` and repair hard errors before review or export.
21
+ 3. Call `revela_design_list`, `revela_design_read` using `section: "rules"`, and `revela_design_inventory` before authoring or repairing `deck-plan/`; deck-plan layout/component names must come from the selected design inventory.
22
+ 4. Call `revela_read_deck_plan` as the required deck-plan preflight before any HTML generation.
23
+ 5. If `deck-plan/` is missing or incomplete, author or repair `deck-plan/index.md` and `deck-plan/slides/*.md` before calling `revela_create_deck_foundation`; use only inventory-listed layouts/components in slide frontmatter and visual intent.
24
+ 6. Report deck-plan diagnostics before artifact generation, including stale narrative hashes, missing slide projections, missing evidence trace, caveats, malformed plan files, or layout/component names outside the active design inventory.
25
+ 7. Do not start HTML generation from narrative alone unless the user explicitly asks for a throwaway diagnostic smoke deck.
26
+ 8. For new HTML files, call `revela_create_deck_foundation`.
27
+ 9. Before patching slide HTML, read the specific layouts/components used by the deck plan with `revela_design_read_layout` and `revela_design_read_component`; fetch `section: "chart-rules"` and the `echart-panel` component before creating or changing ECharts. 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, call `revela_design_inventory` with that name, and pass `designName` to `revela_create_deck_foundation`.
28
+ 10. 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.
29
+ 11. Generate chapter by chapter. Keep the HTML valid after each write.
30
+ 12. After every HTML write, call `revela_run_deck_qa` and repair hard errors before review or export.
30
31
 
31
32
  ## Generated Visual Assets
32
33