@cyber-dash-tech/revela 0.17.22 → 0.17.24
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/lib/design/designs.ts +123 -39
- package/lib/narrative-state/deck-plan-artifact.ts +426 -1
- package/lib/runtime/index.ts +84 -6
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +121 -0
- package/plugins/revela/skills/revela-design/SKILL.md +13 -5
- package/plugins/revela/skills/revela-make-deck/SKILL.md +18 -10
package/lib/design/designs.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* DesignManager — manage revela visual design templates.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
|
|
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
|
|
162
|
-
|
|
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
|
-
|
|
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
|
|
192
|
-
if (!
|
|
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 =
|
|
206
|
-
|
|
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
|
-
|
|
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 `
|
|
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 `
|
|
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
|
|
616
|
-
if (!
|
|
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
|
|
649
|
-
if (!
|
|
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
|
|
676
|
-
if (!
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs"
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "fs"
|
|
2
2
|
import { dirname, join, relative } from "path"
|
|
3
3
|
import { createHash } from "crypto"
|
|
4
4
|
import type { DeckSpec, SlideSpec } from "../decks-state"
|
|
@@ -81,6 +81,7 @@ export interface DeckPlanSlideProjection {
|
|
|
81
81
|
chapter: string
|
|
82
82
|
layout: string
|
|
83
83
|
components: string[]
|
|
84
|
+
componentPlan: DeckPlanSlideComponentPlan[]
|
|
84
85
|
structural: boolean
|
|
85
86
|
narrativeRole: string
|
|
86
87
|
markdown: string
|
|
@@ -89,6 +90,66 @@ export interface DeckPlanSlideProjection {
|
|
|
89
90
|
links: DeckPlanNarrativeLink[]
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export interface DeckPlanSlideComponentPlan {
|
|
94
|
+
name: string
|
|
95
|
+
slot: string
|
|
96
|
+
position: string
|
|
97
|
+
purpose: string
|
|
98
|
+
content: string
|
|
99
|
+
claimIds: string[]
|
|
100
|
+
evidenceIds: string[]
|
|
101
|
+
sourceNotes: string[]
|
|
102
|
+
renderNotes: string[]
|
|
103
|
+
placementNote?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface DeckPlanSlideUpsertComponentInput {
|
|
107
|
+
name: string
|
|
108
|
+
slot: string
|
|
109
|
+
position: string
|
|
110
|
+
purpose: string
|
|
111
|
+
content: string
|
|
112
|
+
claimIds?: string[]
|
|
113
|
+
evidenceIds?: string[]
|
|
114
|
+
sourceNotes?: string[]
|
|
115
|
+
renderNotes?: string[]
|
|
116
|
+
placementNote?: string
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface DeckPlanSlideUpsertInput {
|
|
120
|
+
slideIndex: number
|
|
121
|
+
id?: string
|
|
122
|
+
title: string
|
|
123
|
+
chapter: string
|
|
124
|
+
narrativeRole: string
|
|
125
|
+
structural?: boolean
|
|
126
|
+
layout: string
|
|
127
|
+
components: DeckPlanSlideUpsertComponentInput[]
|
|
128
|
+
visualIntent: {
|
|
129
|
+
kind?: string
|
|
130
|
+
component?: string
|
|
131
|
+
rationale?: string
|
|
132
|
+
brief?: string
|
|
133
|
+
} | string
|
|
134
|
+
narrativeLinks: {
|
|
135
|
+
claimIds?: string[]
|
|
136
|
+
evidenceIds?: string[]
|
|
137
|
+
riskIds?: string[]
|
|
138
|
+
objectionIds?: string[]
|
|
139
|
+
gapIds?: string[]
|
|
140
|
+
}
|
|
141
|
+
caveats?: string[]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DeckPlanSlideUpsertResult {
|
|
145
|
+
ok: boolean
|
|
146
|
+
path?: string
|
|
147
|
+
absolutePath?: string
|
|
148
|
+
updated?: boolean
|
|
149
|
+
slide?: DeckPlanSlideProjection
|
|
150
|
+
diagnostics: DeckPlanProjectionDiagnostic[]
|
|
151
|
+
}
|
|
152
|
+
|
|
92
153
|
export interface DeckPlanNarrativeLink {
|
|
93
154
|
id: string
|
|
94
155
|
relation: "uses_claim" | "uses_evidence" | "addresses_risk" | "answers_objection" | "mentions_gap"
|
|
@@ -247,6 +308,41 @@ export function deckPlanBodyHash(markdown: string): string {
|
|
|
247
308
|
return createHash("sha1").update(stripApprovalSection(markdown).trim()).digest("hex")
|
|
248
309
|
}
|
|
249
310
|
|
|
311
|
+
export function upsertDeckPlanSlideArtifact(
|
|
312
|
+
workspaceRoot: string,
|
|
313
|
+
input: DeckPlanSlideUpsertInput,
|
|
314
|
+
options: { narrativeHash?: string; knownNodeIds?: Set<string>; designLayouts: string[]; designComponents: string[] },
|
|
315
|
+
): DeckPlanSlideUpsertResult {
|
|
316
|
+
const diagnostics = validateDeckPlanSlideUpsert(input, options)
|
|
317
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return { ok: false, diagnostics }
|
|
318
|
+
|
|
319
|
+
mkdirSync(join(workspaceRoot, DECK_PLAN_SLIDES_DIR), { recursive: true })
|
|
320
|
+
ensureDeckPlanIndex(workspaceRoot, options.narrativeHash)
|
|
321
|
+
|
|
322
|
+
const existing = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
323
|
+
const existingSlide = existing?.slides.find((slide) => slide.slideIndex === input.slideIndex)
|
|
324
|
+
const id = input.id?.trim() || existingSlide?.id || `slide-${slugify(input.title)}`
|
|
325
|
+
const filename = `${String(input.slideIndex).padStart(3, "0")}-${slugify(input.title)}.md`
|
|
326
|
+
const path = `${DECK_PLAN_SLIDES_DIR}/${filename}`
|
|
327
|
+
const absolutePath = join(workspaceRoot, path)
|
|
328
|
+
const markdown = renderDeckPlanSlideMarkdown({ ...input, id })
|
|
329
|
+
|
|
330
|
+
writeFileSync(absolutePath, markdown, "utf-8")
|
|
331
|
+
if (existingSlide && existingSlide.absolutePath !== absolutePath && existsSync(existingSlide.absolutePath)) {
|
|
332
|
+
try {
|
|
333
|
+
rmSync(existingSlide.absolutePath)
|
|
334
|
+
} catch {
|
|
335
|
+
// Empty stale files are ignored by readers only when removed; if removal fails,
|
|
336
|
+
// duplicate slideIndex diagnostics will surface on the next read.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
updateDeckPlanIndex(workspaceRoot, options.narrativeHash)
|
|
341
|
+
const projection = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
342
|
+
const slide = projection?.slides.find((item) => item.slideIndex === input.slideIndex)
|
|
343
|
+
return { ok: true, path, absolutePath, updated: Boolean(existingSlide), slide, diagnostics: [...diagnostics, ...(projection?.diagnostics ?? [])] }
|
|
344
|
+
}
|
|
345
|
+
|
|
250
346
|
function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
251
347
|
const slidesDir = join(workspaceRoot, DECK_PLAN_SLIDES_DIR)
|
|
252
348
|
if (!existsSync(slidesDir) || !statSync(slidesDir).isDirectory()) return []
|
|
@@ -259,6 +355,7 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
259
355
|
const split = splitMarkdownSections(parsed.body)
|
|
260
356
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
261
357
|
const id = stringField(parsed.frontmatter, "id") || fileId(entry)
|
|
358
|
+
const componentPlan = parseDeckPlanComponentPlan(split.sections["component-plan"] ?? "")
|
|
262
359
|
const links = parseDeckPlanNarrativeLinks(split.sections["narrative-links"] ?? parsed.body, knownNodeIds)
|
|
263
360
|
slides.push({
|
|
264
361
|
path,
|
|
@@ -269,6 +366,7 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
269
366
|
chapter: stringField(parsed.frontmatter, "chapter"),
|
|
270
367
|
layout: stringField(parsed.frontmatter, "layout"),
|
|
271
368
|
components: arrayField(parsed.frontmatter, "components"),
|
|
369
|
+
componentPlan,
|
|
272
370
|
structural: booleanField(parsed.frontmatter, "structural", false),
|
|
273
371
|
narrativeRole: stringField(parsed.frontmatter, "narrativeRole"),
|
|
274
372
|
markdown,
|
|
@@ -338,6 +436,14 @@ function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanPr
|
|
|
338
436
|
function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
339
437
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
340
438
|
if (!slide.structural && !slide.links.some((link) => link.relation === "uses_claim")) diagnostics.push({ severity: "warning", code: "slide_claim_link_missing", message: `Non-structural deck-plan slide ${slide.id} has no claim wikilink in ## Narrative Links.`, file: slide.path, nodeId: slide.id })
|
|
439
|
+
if (!slide.layout) diagnostics.push({ severity: "warning", code: "slide_layout_missing", message: `Deck-plan slide ${slide.id} is missing a layout.`, file: slide.path, nodeId: slide.id })
|
|
440
|
+
if (slide.components.length === 0) diagnostics.push({ severity: "warning", code: "slide_components_missing", message: `Deck-plan slide ${slide.id} has no component names in frontmatter.`, file: slide.path, nodeId: slide.id })
|
|
441
|
+
if (slide.componentPlan.length === 0) diagnostics.push({ severity: "warning", code: "slide_component_plan_missing", message: `Deck-plan slide ${slide.id} is missing structured ## Component Plan entries.`, file: slide.path, nodeId: slide.id })
|
|
442
|
+
for (const component of slide.componentPlan) {
|
|
443
|
+
for (const key of ["name", "slot", "position", "purpose", "content"] as const) {
|
|
444
|
+
if (!component[key] || (Array.isArray(component[key]) && component[key].length === 0)) diagnostics.push({ severity: "warning", code: "slide_component_plan_incomplete", message: `Deck-plan slide ${slide.id} has incomplete component plan entry for ${component.name || "unnamed component"}: missing ${key}.`, file: slide.path, nodeId: slide.id })
|
|
445
|
+
}
|
|
446
|
+
}
|
|
341
447
|
if (knownNodeIds) {
|
|
342
448
|
for (const link of slide.links) {
|
|
343
449
|
if (!knownNodeIds.has(link.id)) diagnostics.push({ severity: "warning", code: "deck_plan_broken_link", message: `Deck-plan slide ${slide.id} links to unknown narrative node ${link.id}.`, file: slide.path, nodeId: slide.id })
|
|
@@ -346,6 +452,251 @@ function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<str
|
|
|
346
452
|
return diagnostics
|
|
347
453
|
}
|
|
348
454
|
|
|
455
|
+
export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undefined, inventory: { layouts: string[]; components: string[] }): DeckPlanProjectionDiagnostic[] {
|
|
456
|
+
if (!projection) return []
|
|
457
|
+
const layouts = new Set(inventory.layouts)
|
|
458
|
+
const components = new Set(inventory.components)
|
|
459
|
+
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
460
|
+
for (const slide of projection.slides) {
|
|
461
|
+
if (slide.layout && !layouts.has(slide.layout)) diagnostics.push({ severity: "warning", code: "slide_layout_unknown", message: `Deck-plan slide ${slide.id} uses layout '${slide.layout}' outside the active design inventory.`, file: slide.path, nodeId: slide.id })
|
|
462
|
+
for (const component of slide.components) {
|
|
463
|
+
if (!components.has(component)) diagnostics.push({ severity: "warning", code: "slide_component_unknown", message: `Deck-plan slide ${slide.id} uses component '${component}' outside the active design inventory.`, file: slide.path, nodeId: slide.id })
|
|
464
|
+
}
|
|
465
|
+
for (const component of slide.componentPlan) {
|
|
466
|
+
if (component.name && !components.has(component.name)) diagnostics.push({ severity: "warning", code: "slide_component_plan_unknown", message: `Deck-plan slide ${slide.id} component plan uses '${component.name}' outside the active design inventory.`, file: slide.path, nodeId: slide.id })
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return diagnostics
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: { designLayouts: string[]; designComponents: string[] }): DeckPlanProjectionDiagnostic[] {
|
|
473
|
+
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
474
|
+
const nodeId = input.id?.trim() || `slide-${input.slideIndex}`
|
|
475
|
+
if (!Number.isInteger(input.slideIndex) || input.slideIndex < 1) diagnostics.push(errorDiagnostic("slide_index_invalid", "slideIndex must be a positive 1-based integer.", nodeId))
|
|
476
|
+
for (const [key, value] of [["title", input.title], ["chapter", input.chapter], ["narrativeRole", input.narrativeRole], ["layout", input.layout]] as const) {
|
|
477
|
+
if (!String(value || "").trim()) diagnostics.push(errorDiagnostic(`slide_${key}_missing`, `${key} is required.`, nodeId))
|
|
478
|
+
}
|
|
479
|
+
if (!options.designLayouts.includes(input.layout)) diagnostics.push(errorDiagnostic("slide_layout_unknown", `Layout '${input.layout}' is not in the selected design inventory.`, nodeId))
|
|
480
|
+
if (!Array.isArray(input.components) || input.components.length === 0) diagnostics.push(errorDiagnostic("slide_components_missing", "At least one component plan entry is required.", nodeId))
|
|
481
|
+
const componentNames = new Set<string>()
|
|
482
|
+
const positions = new Set<string>()
|
|
483
|
+
for (const component of input.components ?? []) {
|
|
484
|
+
const name = component.name?.trim()
|
|
485
|
+
if (name) componentNames.add(name)
|
|
486
|
+
if (!name) diagnostics.push(errorDiagnostic("slide_component_name_missing", "Every component requires name.", nodeId))
|
|
487
|
+
else if (!options.designComponents.includes(name)) diagnostics.push(errorDiagnostic("slide_component_unknown", `Component '${name}' is not in the selected design inventory.`, nodeId))
|
|
488
|
+
for (const key of ["slot", "position", "purpose", "content"] as const) {
|
|
489
|
+
if (!String(component[key] || "").trim()) diagnostics.push(errorDiagnostic("slide_component_plan_incomplete", `Component '${name || "unnamed"}' is missing ${key}.`, nodeId))
|
|
490
|
+
}
|
|
491
|
+
if (component.position && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(component.position)) diagnostics.push(errorDiagnostic("slide_component_position_invalid", `Component '${name || "unnamed"}' position must be a non-empty kebab-case anchor.`, nodeId))
|
|
492
|
+
const positionKey = `${component.slot?.trim() || ""}:${component.position?.trim() || ""}`
|
|
493
|
+
if (component.slot?.trim() && component.position?.trim()) {
|
|
494
|
+
if (positions.has(positionKey)) diagnostics.push(errorDiagnostic("slide_component_position_duplicate", `Duplicate component slot/position '${positionKey}' makes the plan ambiguous.`, nodeId))
|
|
495
|
+
positions.add(positionKey)
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const visual = normalizeVisualIntent(input.visualIntent)
|
|
499
|
+
if (visual.component && !componentNames.has(visual.component)) diagnostics.push(errorDiagnostic("slide_visual_component_missing", `visualIntent.component '${visual.component}' is not present in component plan.`, nodeId))
|
|
500
|
+
if (!input.structural && !((input.narrativeLinks?.claimIds?.length ?? 0) > 0 || (input.narrativeLinks?.evidenceIds?.length ?? 0) > 0)) diagnostics.push({ severity: "warning", code: "slide_narrative_link_missing", message: "Non-structural slides should include at least one claim or evidence narrative link.", nodeId })
|
|
501
|
+
return diagnostics
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPlanProjectionDiagnostic {
|
|
505
|
+
return { severity: "error", code, message, nodeId }
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function parseDeckPlanComponentPlan(section: string): DeckPlanSlideComponentPlan[] {
|
|
509
|
+
const components: DeckPlanSlideComponentPlan[] = []
|
|
510
|
+
let current: DeckPlanSlideComponentPlan | undefined
|
|
511
|
+
let capture: "content" | undefined
|
|
512
|
+
const flush = () => {
|
|
513
|
+
if (current) components.push({
|
|
514
|
+
...current,
|
|
515
|
+
content: current.content.trim(),
|
|
516
|
+
claimIds: uniqueStrings(current.claimIds),
|
|
517
|
+
evidenceIds: uniqueStrings(current.evidenceIds),
|
|
518
|
+
sourceNotes: current.sourceNotes.filter(Boolean),
|
|
519
|
+
renderNotes: current.renderNotes.filter(Boolean),
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
for (const rawLine of section.replace(/\r\n/g, "\n").split("\n")) {
|
|
523
|
+
const heading = /^###\s+(.+?)\s*$/.exec(rawLine)
|
|
524
|
+
if (heading) {
|
|
525
|
+
flush()
|
|
526
|
+
current = { name: heading[1].trim(), slot: "", position: "", purpose: "", content: "", claimIds: [], evidenceIds: [], sourceNotes: [], renderNotes: [] }
|
|
527
|
+
capture = undefined
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
if (!current) continue
|
|
531
|
+
const line = rawLine.trim()
|
|
532
|
+
const field = /^-\s+([A-Za-z][A-Za-z ]+):\s*(.*)$/.exec(line)
|
|
533
|
+
if (field) {
|
|
534
|
+
capture = undefined
|
|
535
|
+
const key = field[1].toLowerCase()
|
|
536
|
+
const value = field[2].trim()
|
|
537
|
+
if (key === "slot") current.slot = value
|
|
538
|
+
else if (key === "position") current.position = value
|
|
539
|
+
else if (key === "placement note") current.placementNote = value
|
|
540
|
+
else if (key === "purpose") current.purpose = value
|
|
541
|
+
else if (key === "content") {
|
|
542
|
+
current.content = cleanPlanValue(value)
|
|
543
|
+
capture = value ? undefined : "content"
|
|
544
|
+
} else if (key === "claim ids") current.claimIds = parseCsv(value)
|
|
545
|
+
else if (key === "evidence ids") current.evidenceIds = parseCsv(value)
|
|
546
|
+
else if (key === "source notes") current.sourceNotes = parseListValue(value)
|
|
547
|
+
else if (key === "render notes") current.renderNotes = parseListValue(value)
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
if (capture === "content" && rawLine.trim()) current.content += `${current.content ? "\n" : ""}${rawLine.replace(/^\s{2}/, "")}`
|
|
551
|
+
}
|
|
552
|
+
flush()
|
|
553
|
+
return components
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: string }): string {
|
|
557
|
+
const components = input.components.map((component) => component.name.trim())
|
|
558
|
+
const lines: string[] = []
|
|
559
|
+
lines.push("---")
|
|
560
|
+
lines.push("type: deck-plan-slide")
|
|
561
|
+
lines.push(`id: ${input.id}`)
|
|
562
|
+
lines.push(`slideIndex: ${input.slideIndex}`)
|
|
563
|
+
lines.push(`title: ${yamlScalar(input.title)}`)
|
|
564
|
+
lines.push(`chapter: ${yamlScalar(input.chapter)}`)
|
|
565
|
+
lines.push(`layout: ${input.layout.trim()}`)
|
|
566
|
+
lines.push(`components: [${components.map(yamlScalar).join(", ")}]`)
|
|
567
|
+
lines.push(`structural: ${input.structural ? "true" : "false"}`)
|
|
568
|
+
lines.push(`narrativeRole: ${yamlScalar(input.narrativeRole)}`)
|
|
569
|
+
lines.push("---")
|
|
570
|
+
lines.push("")
|
|
571
|
+
lines.push(`# ${input.title.trim()}`)
|
|
572
|
+
lines.push("")
|
|
573
|
+
lines.push("## Purpose")
|
|
574
|
+
lines.push("")
|
|
575
|
+
lines.push(input.narrativeRole.trim())
|
|
576
|
+
lines.push("")
|
|
577
|
+
lines.push("## Visual Intent")
|
|
578
|
+
lines.push("")
|
|
579
|
+
lines.push(renderVisualIntent(input.visualIntent))
|
|
580
|
+
lines.push("")
|
|
581
|
+
lines.push("## Component Plan")
|
|
582
|
+
lines.push("")
|
|
583
|
+
for (const component of input.components) {
|
|
584
|
+
lines.push(`### ${component.name.trim()}`)
|
|
585
|
+
lines.push("")
|
|
586
|
+
lines.push(`- Slot: ${component.slot.trim()}`)
|
|
587
|
+
lines.push(`- Position: ${component.position.trim()}`)
|
|
588
|
+
if (component.placementNote?.trim()) lines.push(`- Placement note: ${component.placementNote.trim()}`)
|
|
589
|
+
lines.push(`- Purpose: ${component.purpose.trim()}`)
|
|
590
|
+
lines.push("- Content:")
|
|
591
|
+
lines.push(...indentMultiline(component.content.trim()))
|
|
592
|
+
lines.push(`- Claim ids: ${formatCsv(component.claimIds)}`)
|
|
593
|
+
lines.push(`- Evidence ids: ${formatCsv(component.evidenceIds)}`)
|
|
594
|
+
lines.push(`- Source notes: ${formatListValue(component.sourceNotes)}`)
|
|
595
|
+
lines.push(`- Render notes: ${formatListValue(component.renderNotes)}`)
|
|
596
|
+
lines.push("")
|
|
597
|
+
}
|
|
598
|
+
lines.push("## Narrative Links")
|
|
599
|
+
lines.push("")
|
|
600
|
+
lines.push("Claims:")
|
|
601
|
+
for (const id of input.narrativeLinks?.claimIds ?? []) lines.push(`- [[${id}]]`)
|
|
602
|
+
lines.push("")
|
|
603
|
+
lines.push("Evidence:")
|
|
604
|
+
for (const id of input.narrativeLinks?.evidenceIds ?? []) lines.push(`- [[${id}]]`)
|
|
605
|
+
lines.push("")
|
|
606
|
+
lines.push("Risks:")
|
|
607
|
+
for (const id of input.narrativeLinks?.riskIds ?? []) lines.push(`- [[${id}]]`)
|
|
608
|
+
lines.push("")
|
|
609
|
+
lines.push("Objections:")
|
|
610
|
+
for (const id of input.narrativeLinks?.objectionIds ?? []) lines.push(`- [[${id}]]`)
|
|
611
|
+
lines.push("")
|
|
612
|
+
lines.push("Gaps:")
|
|
613
|
+
for (const id of input.narrativeLinks?.gapIds ?? []) lines.push(`- [[${id}]]`)
|
|
614
|
+
lines.push("")
|
|
615
|
+
lines.push("## Caveats")
|
|
616
|
+
lines.push("")
|
|
617
|
+
const caveats = input.caveats?.filter((item) => item.trim()) ?? []
|
|
618
|
+
if (caveats.length === 0) lines.push("- None.")
|
|
619
|
+
else for (const caveat of caveats) lines.push(`- ${caveat.trim()}`)
|
|
620
|
+
lines.push("")
|
|
621
|
+
return lines.join("\n")
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function ensureDeckPlanIndex(workspaceRoot: string, narrativeHash?: string): void {
|
|
625
|
+
const absolutePath = join(workspaceRoot, DECK_PLAN_INDEX_PATH)
|
|
626
|
+
if (existsSync(absolutePath)) return
|
|
627
|
+
mkdirSync(dirname(absolutePath), { recursive: true })
|
|
628
|
+
writeFileSync(absolutePath, renderMinimalDeckPlanIndex(narrativeHash, []), "utf-8")
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function updateDeckPlanIndex(workspaceRoot: string, narrativeHash?: string): void {
|
|
632
|
+
const projection = readDeckPlanProjection(workspaceRoot, { narrativeHash })
|
|
633
|
+
const slides = projection?.slides ?? []
|
|
634
|
+
writeFileSync(join(workspaceRoot, DECK_PLAN_INDEX_PATH), renderMinimalDeckPlanIndex(narrativeHash || projection?.narrativeHash, slides), "utf-8")
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: DeckPlanSlideProjection[]): string {
|
|
638
|
+
const chapterMap = new Map<string, number[]>()
|
|
639
|
+
for (const slide of slides) {
|
|
640
|
+
const chapter = slide.chapter || "Unassigned"
|
|
641
|
+
chapterMap.set(chapter, [...(chapterMap.get(chapter) ?? []), slide.slideIndex ?? 0].filter(Boolean))
|
|
642
|
+
}
|
|
643
|
+
const lines: string[] = []
|
|
644
|
+
lines.push("---")
|
|
645
|
+
lines.push("id: deck-plan")
|
|
646
|
+
if (narrativeHash) lines.push(`narrativeHash: ${narrativeHash}`)
|
|
647
|
+
lines.push("---")
|
|
648
|
+
lines.push("")
|
|
649
|
+
lines.push("# Deck Plan")
|
|
650
|
+
lines.push("")
|
|
651
|
+
lines.push("## Source Authority")
|
|
652
|
+
lines.push("")
|
|
653
|
+
lines.push("- Meaning: `revela-narrative/` remains canonical.")
|
|
654
|
+
lines.push("- Render planning: `deck-plan/` is an execution projection, not approval state.")
|
|
655
|
+
lines.push("")
|
|
656
|
+
lines.push("## Audience / Goal / Decision")
|
|
657
|
+
lines.push("")
|
|
658
|
+
lines.push("- To be specified by narrative state and user intent.")
|
|
659
|
+
lines.push("")
|
|
660
|
+
lines.push("## Deck Parameters")
|
|
661
|
+
lines.push("")
|
|
662
|
+
lines.push(`- Slide count: ${slides.length}`)
|
|
663
|
+
if (narrativeHash) lines.push(`- Narrative hash: \`${narrativeHash}\``)
|
|
664
|
+
lines.push("")
|
|
665
|
+
lines.push("## Chapter Map")
|
|
666
|
+
lines.push("")
|
|
667
|
+
if (chapterMap.size === 0) lines.push("- No slides planned yet.")
|
|
668
|
+
else for (const [chapter, indexes] of chapterMap) lines.push(`- ${chapter}: slides ${formatSlideRange(indexes)}`)
|
|
669
|
+
lines.push("")
|
|
670
|
+
lines.push("## Slide Plan")
|
|
671
|
+
lines.push("")
|
|
672
|
+
if (slides.length === 0) lines.push("- No slide files yet.")
|
|
673
|
+
else for (const slide of slides) lines.push(`- Slide ${slide.slideIndex}: [[${slide.id}]] - ${slide.title} (${slide.path}); layout ${slide.layout || "unspecified"}; components ${slide.components.join(", ") || "none"}.`)
|
|
674
|
+
lines.push("")
|
|
675
|
+
lines.push("## Evidence Trace")
|
|
676
|
+
lines.push("")
|
|
677
|
+
const evidenceIds = uniqueStrings(slides.flatMap((slide) => slide.links.filter((link) => link.relation === "uses_evidence").map((link) => link.id)))
|
|
678
|
+
if (evidenceIds.length === 0) lines.push("- No evidence links planned yet.")
|
|
679
|
+
else for (const id of evidenceIds) lines.push(`- [[${id}]]`)
|
|
680
|
+
lines.push("")
|
|
681
|
+
lines.push("## Boundary / Risk Treatment")
|
|
682
|
+
lines.push("")
|
|
683
|
+
const boundaryIds = uniqueStrings(slides.flatMap((slide) => slide.links.filter((link) => link.relation === "addresses_risk" || link.relation === "answers_objection" || link.relation === "mentions_gap").map((link) => link.id)))
|
|
684
|
+
if (boundaryIds.length === 0) lines.push("- No risk, objection, or gap links planned yet.")
|
|
685
|
+
else for (const id of boundaryIds) lines.push(`- [[${id}]]`)
|
|
686
|
+
lines.push("")
|
|
687
|
+
lines.push("## Chapter Writing Batches")
|
|
688
|
+
lines.push("")
|
|
689
|
+
if (chapterMap.size === 0) lines.push("- No chapter batches yet.")
|
|
690
|
+
else for (const [chapter, indexes] of chapterMap) lines.push(`- ${chapter}: slides ${formatSlideRange(indexes)}.`)
|
|
691
|
+
lines.push("")
|
|
692
|
+
lines.push("## HTML Identity Contract")
|
|
693
|
+
lines.push("")
|
|
694
|
+
lines.push("- Render one `<section class=\"slide\" data-slide-index=\"N\">` per planned slide.")
|
|
695
|
+
lines.push("- Use positive 1-based slide indexes, unique indexes, DOM order, and one direct `.slide-canvas` child per slide.")
|
|
696
|
+
lines.push("")
|
|
697
|
+
return lines.join("\n")
|
|
698
|
+
}
|
|
699
|
+
|
|
349
700
|
function narrativeHashFromMarkdown(markdown: string): string {
|
|
350
701
|
const match = markdown.match(/narrativeHash:\s*`?([^`\s]+)`?/)
|
|
351
702
|
return match?.[1]?.trim() ?? ""
|
|
@@ -379,6 +730,80 @@ function arrayField(frontmatter: Record<string, string | string[] | boolean>, ke
|
|
|
379
730
|
return []
|
|
380
731
|
}
|
|
381
732
|
|
|
733
|
+
function normalizeVisualIntent(input: DeckPlanSlideUpsertInput["visualIntent"]): { kind?: string; component?: string; rationale?: string; brief?: string } {
|
|
734
|
+
if (typeof input === "string") return { brief: input.trim() }
|
|
735
|
+
return {
|
|
736
|
+
kind: input.kind?.trim(),
|
|
737
|
+
component: input.component?.trim(),
|
|
738
|
+
rationale: input.rationale?.trim(),
|
|
739
|
+
brief: input.brief?.trim(),
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function renderVisualIntent(input: DeckPlanSlideUpsertInput["visualIntent"]): string {
|
|
744
|
+
const visual = normalizeVisualIntent(input)
|
|
745
|
+
const lines: string[] = []
|
|
746
|
+
if (visual.kind) lines.push(`- Kind: ${visual.kind}`)
|
|
747
|
+
if (visual.component) lines.push(`- Component: ${visual.component}`)
|
|
748
|
+
if (visual.rationale) lines.push(`- Rationale: ${visual.rationale}`)
|
|
749
|
+
if (visual.brief) lines.push(`- Brief: ${visual.brief}`)
|
|
750
|
+
if (lines.length === 0) lines.push("- Brief: Not specified.")
|
|
751
|
+
return lines.join("\n")
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function indentMultiline(value: string): string[] {
|
|
755
|
+
return value.replace(/\r\n/g, "\n").split("\n").map((line) => ` ${line}`)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function yamlScalar(value: string): string {
|
|
759
|
+
const trimmed = value.trim()
|
|
760
|
+
if (/^[A-Za-z0-9_-]+$/.test(trimmed)) return trimmed
|
|
761
|
+
return JSON.stringify(trimmed)
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function slugify(value: string): string {
|
|
765
|
+
const slug = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
|
|
766
|
+
return slug || "slide"
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function formatCsv(value: string[] | undefined): string {
|
|
770
|
+
const items = uniqueStrings(value ?? [])
|
|
771
|
+
return items.length > 0 ? items.join(", ") : "none"
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function parseCsv(value: string): string[] {
|
|
775
|
+
const cleaned = cleanPlanValue(value)
|
|
776
|
+
if (!cleaned || cleaned.toLowerCase() === "none") return []
|
|
777
|
+
return cleaned.split(",").map((item) => item.trim()).filter(Boolean)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function formatListValue(value: string[] | undefined): string {
|
|
781
|
+
const items = (value ?? []).map((item) => item.trim()).filter(Boolean)
|
|
782
|
+
return items.length > 0 ? items.join(" | ") : "none"
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function parseListValue(value: string): string[] {
|
|
786
|
+
const cleaned = cleanPlanValue(value)
|
|
787
|
+
if (!cleaned || cleaned.toLowerCase() === "none") return []
|
|
788
|
+
return cleaned.split("|").map((item) => item.trim()).filter(Boolean)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function cleanPlanValue(value: string): string {
|
|
792
|
+
return value.replace(/^`|`$/g, "").trim()
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function uniqueStrings(values: string[]): string[] {
|
|
796
|
+
const seen = new Set<string>()
|
|
797
|
+
const result: string[] = []
|
|
798
|
+
for (const value of values) {
|
|
799
|
+
const trimmed = value.trim()
|
|
800
|
+
if (!trimmed || seen.has(trimmed)) continue
|
|
801
|
+
seen.add(trimmed)
|
|
802
|
+
result.push(trimmed)
|
|
803
|
+
}
|
|
804
|
+
return result
|
|
805
|
+
}
|
|
806
|
+
|
|
382
807
|
function titleFromSectionKey(key: string): string {
|
|
383
808
|
return key.split("-").map((part) => part ? `${part[0].toUpperCase()}${part.slice(1)}` : part).join(" ")
|
|
384
809
|
}
|
package/lib/runtime/index.ts
CHANGED
|
@@ -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,
|
|
@@ -22,7 +25,7 @@ import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
|
|
|
22
25
|
import { extractNarrativeVaultMarkdownTargetsFromPatch } from "../narrative-vault/hook-targets"
|
|
23
26
|
import { runNarrativeMarkdownQa, type MarkdownQaOptions } from "../narrative-vault/markdown-qa"
|
|
24
27
|
import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice } from "../hook-notifications"
|
|
25
|
-
import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
|
|
28
|
+
import { deckPlanDesignDiagnostics, readDeckPlanArtifact, upsertDeckPlanSlideArtifact, type DeckPlanSlideUpsertInput } from "../narrative-state/deck-plan-artifact"
|
|
26
29
|
import { extractDesignClasses } from "../design/designs"
|
|
27
30
|
import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
|
|
28
31
|
import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
|
|
@@ -59,6 +62,24 @@ 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
|
+
|
|
79
|
+
export interface RuntimeDeckPlanSlideUpsertInput extends RuntimeWorkspaceInput, DeckPlanSlideUpsertInput {
|
|
80
|
+
designName?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
62
83
|
export interface RuntimeDesignCreateInput {
|
|
63
84
|
name: string
|
|
64
85
|
base?: string
|
|
@@ -132,10 +153,44 @@ export function readDeckPlan(input: RuntimeWorkspaceInput = {}) {
|
|
|
132
153
|
const workspaceRoot = root(input.workspaceRoot)
|
|
133
154
|
const compiled = compileNarrativeVault(workspaceRoot)
|
|
134
155
|
const knownNodeIds = compiled.graph ? new Set(compiled.graph.nodes.map((node) => node.id)) : undefined
|
|
135
|
-
|
|
156
|
+
const read = readDeckPlanArtifact(workspaceRoot, {
|
|
136
157
|
narrativeHash: compiled.narrative ? computeNarrativeHash(compiled.narrative) : undefined,
|
|
137
158
|
knownNodeIds,
|
|
138
159
|
})
|
|
160
|
+
if (read.projection) {
|
|
161
|
+
try {
|
|
162
|
+
const inventory = getDesignInventory(activeDesign())
|
|
163
|
+
const diagnostics = deckPlanDesignDiagnostics(read.projection, {
|
|
164
|
+
layouts: inventory.layouts.map((layout) => layout.name),
|
|
165
|
+
components: inventory.components.map((component) => component.name),
|
|
166
|
+
})
|
|
167
|
+
read.projection.diagnostics.push(...diagnostics)
|
|
168
|
+
read.warnings.push(...diagnostics.map((diagnostic) => diagnostic.message))
|
|
169
|
+
} catch {
|
|
170
|
+
// Design diagnostics are advisory; deck-plan reading remains available.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return read
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function upsertDeckPlanSlide(input: RuntimeDeckPlanSlideUpsertInput) {
|
|
177
|
+
const workspaceRoot = root(input.workspaceRoot)
|
|
178
|
+
const designName = input.designName || activeDesign()
|
|
179
|
+
const inventory = getDesignInventory(designName)
|
|
180
|
+
const compiled = compileNarrativeVault(workspaceRoot)
|
|
181
|
+
const knownNodeIds = compiled.graph ? new Set(compiled.graph.nodes.map((node) => node.id)) : undefined
|
|
182
|
+
const narrativeHash = compiled.narrative ? computeNarrativeHash(compiled.narrative) : undefined
|
|
183
|
+
const result = upsertDeckPlanSlideArtifact(workspaceRoot, input, {
|
|
184
|
+
narrativeHash,
|
|
185
|
+
knownNodeIds,
|
|
186
|
+
designLayouts: inventory.layouts.map((layout) => layout.name),
|
|
187
|
+
designComponents: inventory.components.map((component) => component.name),
|
|
188
|
+
})
|
|
189
|
+
return {
|
|
190
|
+
...result,
|
|
191
|
+
designName,
|
|
192
|
+
narrativeHash,
|
|
193
|
+
}
|
|
139
194
|
}
|
|
140
195
|
|
|
141
196
|
export function createDeckFoundation(input: RuntimeDeckFoundationInput) {
|
|
@@ -212,7 +267,6 @@ export async function reviewDeckOpen(input: ReviewDeckOpenInput) {
|
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
export function designList() {
|
|
215
|
-
seedBuiltinDesigns()
|
|
216
270
|
return {
|
|
217
271
|
ok: true,
|
|
218
272
|
activeDesign: activeDesign(),
|
|
@@ -226,7 +280,6 @@ export function designList() {
|
|
|
226
280
|
}
|
|
227
281
|
|
|
228
282
|
export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
229
|
-
seedBuiltinDesigns()
|
|
230
283
|
const name = input.name || activeDesign()
|
|
231
284
|
if (input.section) {
|
|
232
285
|
const markdown = getDesignSection(input.section, name)
|
|
@@ -246,6 +299,33 @@ export function designRead(input: RuntimeDesignReadInput = {}) {
|
|
|
246
299
|
}
|
|
247
300
|
}
|
|
248
301
|
|
|
302
|
+
export function designInventory(input: RuntimeDesignInventoryInput = {}) {
|
|
303
|
+
return {
|
|
304
|
+
ok: true,
|
|
305
|
+
...getDesignInventory(input.name || activeDesign()),
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function designReadLayout(input: RuntimeDesignLayoutReadInput) {
|
|
310
|
+
const name = input.name || activeDesign()
|
|
311
|
+
return {
|
|
312
|
+
ok: true,
|
|
313
|
+
name,
|
|
314
|
+
layout: input.layout,
|
|
315
|
+
markdown: getDesignLayout(input.layout, name),
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function designReadComponent(input: RuntimeDesignComponentReadInput) {
|
|
320
|
+
const name = input.name || activeDesign()
|
|
321
|
+
return {
|
|
322
|
+
ok: true,
|
|
323
|
+
name,
|
|
324
|
+
component: input.component,
|
|
325
|
+
markdown: getDesignComponent(input.component, name),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
249
329
|
export function designCreate(input: RuntimeDesignCreateInput) {
|
|
250
330
|
seedBuiltinDesigns()
|
|
251
331
|
return createDesignPackage({
|
|
@@ -258,7 +338,6 @@ export function designCreate(input: RuntimeDesignCreateInput) {
|
|
|
258
338
|
}
|
|
259
339
|
|
|
260
340
|
export function designValidate(input: RuntimeNameInput) {
|
|
261
|
-
seedBuiltinDesigns()
|
|
262
341
|
return validateDesignPackage(requiredName(input, "design"))
|
|
263
342
|
}
|
|
264
343
|
|
|
@@ -296,7 +375,6 @@ export interface DesignRulesReadinessResult {
|
|
|
296
375
|
const DESIGN_RULES_MARKER_TTL_MS = 8 * 60 * 60 * 1000
|
|
297
376
|
|
|
298
377
|
export function checkDesignRulesReadiness(input: RuntimeWorkspaceInput = {}): DesignRulesReadinessResult {
|
|
299
|
-
seedBuiltinDesigns()
|
|
300
378
|
const workspaceRoot = root(input.workspaceRoot)
|
|
301
379
|
const design = activeDesign()
|
|
302
380
|
const rules = getDesignSection("rules", design)
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -13,12 +13,16 @@ type RuntimeModule = {
|
|
|
13
13
|
compileNarrative(input?: any): any
|
|
14
14
|
markdownQa(input?: any): any
|
|
15
15
|
readDeckPlan(input?: any): any
|
|
16
|
+
upsertDeckPlanSlide(input: any): any
|
|
16
17
|
createDeckFoundation(input: any): any
|
|
17
18
|
runDeckQa(input: any): Promise<any>
|
|
18
19
|
exportPdf(input: any): Promise<any>
|
|
19
20
|
exportPptx(input: any): Promise<any>
|
|
20
21
|
designList(): any
|
|
21
22
|
designRead(input?: any): any
|
|
23
|
+
designInventory(input?: any): any
|
|
24
|
+
designReadLayout(input: any): any
|
|
25
|
+
designReadComponent(input: any): any
|
|
22
26
|
designActivate(input: any): any
|
|
23
27
|
designCreate(input: any): any
|
|
24
28
|
designValidate(input: any): any
|
|
@@ -74,6 +78,25 @@ const tools = [
|
|
|
74
78
|
description: "Read the file-native deck-plan/ projection and diagnostics.",
|
|
75
79
|
inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
|
|
76
80
|
},
|
|
81
|
+
{
|
|
82
|
+
name: "revela_upsert_deck_plan_slide",
|
|
83
|
+
description: "Create or update one structured deck-plan/slides/*.md slide plan with active/requested design vocabulary validation.",
|
|
84
|
+
inputSchema: objectSchema({
|
|
85
|
+
workspaceRoot: stringProp("Optional workspace root."),
|
|
86
|
+
designName: stringProp("Optional design name. Defaults to the active design."),
|
|
87
|
+
slideIndex: requiredNumberProp("Positive 1-based slide index."),
|
|
88
|
+
id: stringProp("Optional stable slide node id."),
|
|
89
|
+
title: requiredStringProp("Slide title."),
|
|
90
|
+
chapter: requiredStringProp("Chapter name."),
|
|
91
|
+
narrativeRole: requiredStringProp("Slide's communication job."),
|
|
92
|
+
structural: booleanProp("Whether this is a structural slide such as cover, TOC, divider, or closing."),
|
|
93
|
+
layout: requiredStringProp("Design layout name from inventory."),
|
|
94
|
+
components: componentPlanArrayProp(),
|
|
95
|
+
visualIntent: visualIntentProp(),
|
|
96
|
+
narrativeLinks: narrativeLinksProp(),
|
|
97
|
+
caveats: arrayProp("Optional caveats to keep visible in the slide plan."),
|
|
98
|
+
}, ["slideIndex", "title", "chapter", "narrativeRole", "layout", "components", "visualIntent", "narrativeLinks"]),
|
|
99
|
+
},
|
|
77
100
|
{
|
|
78
101
|
name: "revela_create_deck_foundation",
|
|
79
102
|
description: "Create or repair a file-native Revela HTML deck foundation shell.",
|
|
@@ -125,6 +148,29 @@ const tools = [
|
|
|
125
148
|
section: stringProp("Optional design section, such as rules, foundation, or chart-rules."),
|
|
126
149
|
}),
|
|
127
150
|
},
|
|
151
|
+
{
|
|
152
|
+
name: "revela_design_inventory",
|
|
153
|
+
description: "List the active or requested Revela design sections, layouts, and components.",
|
|
154
|
+
inputSchema: objectSchema({
|
|
155
|
+
name: stringProp("Optional design name."),
|
|
156
|
+
}),
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "revela_design_read_layout",
|
|
160
|
+
description: "Read one or more layout blocks from the active or requested Revela design.",
|
|
161
|
+
inputSchema: objectSchema({
|
|
162
|
+
name: stringProp("Optional design name."),
|
|
163
|
+
layout: stringOrArrayProp("Layout name, comma-separated names, or an array of layout names."),
|
|
164
|
+
}, ["layout"]),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: "revela_design_read_component",
|
|
168
|
+
description: "Read one or more component blocks from the active or requested Revela design.",
|
|
169
|
+
inputSchema: objectSchema({
|
|
170
|
+
name: stringProp("Optional design name."),
|
|
171
|
+
component: stringOrArrayProp("Component name, comma-separated names, or an array of component names."),
|
|
172
|
+
}, ["component"]),
|
|
173
|
+
},
|
|
128
174
|
{
|
|
129
175
|
name: "revela_design_activate",
|
|
130
176
|
description: "Activate a Revela design for future deck planning and artifact generation.",
|
|
@@ -388,12 +434,16 @@ async function callTool(name: string, args: any): Promise<any> {
|
|
|
388
434
|
if (name === "revela_compile_narrative") return r.compileNarrative(args)
|
|
389
435
|
if (name === "revela_markdown_qa") return r.markdownQa(args)
|
|
390
436
|
if (name === "revela_read_deck_plan") return r.readDeckPlan(args)
|
|
437
|
+
if (name === "revela_upsert_deck_plan_slide") return r.upsertDeckPlanSlide(args)
|
|
391
438
|
if (name === "revela_create_deck_foundation") return r.createDeckFoundation(args)
|
|
392
439
|
if (name === "revela_run_deck_qa") return r.runDeckQa(args)
|
|
393
440
|
if (name === "revela_export_pdf") return r.exportPdf(args)
|
|
394
441
|
if (name === "revela_export_pptx") return r.exportPptx(args)
|
|
395
442
|
if (name === "revela_design_list") return r.designList()
|
|
396
443
|
if (name === "revela_design_read") return r.designRead(args)
|
|
444
|
+
if (name === "revela_design_inventory") return r.designInventory(args)
|
|
445
|
+
if (name === "revela_design_read_layout") return r.designReadLayout(args)
|
|
446
|
+
if (name === "revela_design_read_component") return r.designReadComponent(args)
|
|
397
447
|
if (name === "revela_design_activate") return r.designActivate(args)
|
|
398
448
|
if (name === "revela_design_create") return r.designCreate(args)
|
|
399
449
|
if (name === "revela_design_validate") return r.designValidate(args)
|
|
@@ -450,6 +500,10 @@ function numberProp(description: string) {
|
|
|
450
500
|
return { type: "number", description }
|
|
451
501
|
}
|
|
452
502
|
|
|
503
|
+
function requiredNumberProp(description: string) {
|
|
504
|
+
return { type: "number", description }
|
|
505
|
+
}
|
|
506
|
+
|
|
453
507
|
function enumProp(values: string[], description: string) {
|
|
454
508
|
return { type: "string", enum: values, description }
|
|
455
509
|
}
|
|
@@ -458,6 +512,16 @@ function arrayProp(description: string) {
|
|
|
458
512
|
return { type: "array", items: { type: "string" }, description }
|
|
459
513
|
}
|
|
460
514
|
|
|
515
|
+
function stringOrArrayProp(description: string) {
|
|
516
|
+
return {
|
|
517
|
+
anyOf: [
|
|
518
|
+
{ type: "string" },
|
|
519
|
+
{ type: "array", items: { type: "string" } },
|
|
520
|
+
],
|
|
521
|
+
description,
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
461
525
|
function arrayObjectProp(description: string) {
|
|
462
526
|
return {
|
|
463
527
|
type: "array",
|
|
@@ -475,6 +539,63 @@ function arrayObjectProp(description: string) {
|
|
|
475
539
|
}
|
|
476
540
|
}
|
|
477
541
|
|
|
542
|
+
function componentPlanArrayProp() {
|
|
543
|
+
return {
|
|
544
|
+
type: "array",
|
|
545
|
+
description: "Structured component plan entries.",
|
|
546
|
+
items: {
|
|
547
|
+
type: "object",
|
|
548
|
+
properties: {
|
|
549
|
+
name: { type: "string", description: "Design component name." },
|
|
550
|
+
slot: { type: "string", description: "Layout semantic slot, such as left, right, top, main, bottom, footer, or fullbleed." },
|
|
551
|
+
position: { type: "string", description: "Slot-local kebab-case anchor, such as left-top, center, or overlay-top-right." },
|
|
552
|
+
purpose: { type: "string", description: "Component communication job." },
|
|
553
|
+
content: { type: "string", description: "Exact text, data, or structure to render." },
|
|
554
|
+
claimIds: { type: "array", items: { type: "string" } },
|
|
555
|
+
evidenceIds: { type: "array", items: { type: "string" } },
|
|
556
|
+
sourceNotes: { type: "array", items: { type: "string" } },
|
|
557
|
+
renderNotes: { type: "array", items: { type: "string" } },
|
|
558
|
+
placementNote: { type: "string" },
|
|
559
|
+
},
|
|
560
|
+
required: ["name", "slot", "position", "purpose", "content"],
|
|
561
|
+
additionalProperties: false,
|
|
562
|
+
},
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function visualIntentProp() {
|
|
567
|
+
return {
|
|
568
|
+
anyOf: [
|
|
569
|
+
{ type: "string" },
|
|
570
|
+
{
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
kind: { type: "string" },
|
|
574
|
+
component: { type: "string" },
|
|
575
|
+
rationale: { type: "string" },
|
|
576
|
+
brief: { type: "string" },
|
|
577
|
+
},
|
|
578
|
+
additionalProperties: false,
|
|
579
|
+
},
|
|
580
|
+
],
|
|
581
|
+
description: "Visual plan or structured visual intent. If component is set, it must exist in components[].name.",
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function narrativeLinksProp() {
|
|
586
|
+
return {
|
|
587
|
+
type: "object",
|
|
588
|
+
properties: {
|
|
589
|
+
claimIds: { type: "array", items: { type: "string" } },
|
|
590
|
+
evidenceIds: { type: "array", items: { type: "string" } },
|
|
591
|
+
riskIds: { type: "array", items: { type: "string" } },
|
|
592
|
+
objectionIds: { type: "array", items: { type: "string" } },
|
|
593
|
+
gapIds: { type: "array", items: { type: "string" } },
|
|
594
|
+
},
|
|
595
|
+
additionalProperties: false,
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
478
599
|
function writeMessage(message: any, mode: MessageMode = activeResponseMode): void {
|
|
479
600
|
activeResponseMode = mode
|
|
480
601
|
const body = JSON.stringify(message)
|
|
@@ -11,16 +11,24 @@ 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.
|
|
15
|
-
4.
|
|
16
|
-
5.
|
|
17
|
-
6.
|
|
18
|
-
7.
|
|
14
|
+
3. Call `revela_design_inventory` before authoring or repairing `deck-plan/` so planned layout/component names come from the active design. Use `revela_upsert_deck_plan_slide` for slide-plan writes; do not hand-write slide plan Markdown.
|
|
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
|
|
|
22
24
|
Design changes are visual/artifact-level unless they change claim meaning, evidence boundaries, decision, or recommendation.
|
|
23
25
|
|
|
26
|
+
## Inventory-First Planning
|
|
27
|
+
|
|
28
|
+
Deck planning uses design vocabulary before HTML. Inspect the inventory, choose a valid layout for each slide, and choose valid component names for each planned element. Every component plan must include a semantic `slot` such as `left`, `right`, `top`, `main`, `bottom`, `footer`, or `fullbleed`, plus a non-empty kebab-case `position` such as `left-top`, `left-middle`, `right-bottom`, `center`, or `overlay-top-right`.
|
|
29
|
+
|
|
30
|
+
Use `placementNote` for natural-language placement detail when slot and position are not enough. Slot and position are planning anchors; before HTML generation, fetch the actual layout/component definitions and implement the final structure with the design's CSS and markup.
|
|
31
|
+
|
|
24
32
|
## Creating Or Editing Designs
|
|
25
33
|
|
|
26
34
|
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.
|
|
@@ -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 `
|
|
22
|
-
4.
|
|
23
|
-
5.
|
|
24
|
-
6.
|
|
25
|
-
7.
|
|
26
|
-
8.
|
|
27
|
-
9.
|
|
28
|
-
10.
|
|
29
|
-
11.
|
|
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, call `revela_upsert_deck_plan_slide` for each planned or changed slide before calling `revela_create_deck_foundation`; do not hand-write `deck-plan/slides/*.md`. Use only inventory-listed layouts/components, and provide slot, position, purpose, and exact content for every planned component.
|
|
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, call `revela_read_deck_plan`, collect the layouts and components from the projection and `componentPlan[]`, then read the specific layouts/components 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, pass `designName` to every `revela_upsert_deck_plan_slide` call, 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
|
|
|
@@ -46,4 +47,11 @@ Use this skill when the user asks to make, generate, or update a Revela deck.
|
|
|
46
47
|
|
|
47
48
|
## Deck Plan Requirements
|
|
48
49
|
|
|
49
|
-
Every deck plan should include Cover, Table of Contents, and Closing. Use 3-5 chapter headings, explicit slide ranges,
|
|
50
|
+
Every deck plan should include Cover, Table of Contents, and Closing. Use 3-5 chapter headings, explicit slide ranges, narrative links, visual intent, evidence trace, and caveats.
|
|
51
|
+
|
|
52
|
+
`revela_upsert_deck_plan_slide` is the required slide-planning write path. For every slide, provide:
|
|
53
|
+
|
|
54
|
+
- `slideIndex`, optional stable `id`, `title`, `chapter`, `narrativeRole`, `structural`, and inventory-listed `layout`.
|
|
55
|
+
- `components[]` entries with inventory-listed `name`, semantic `slot`, kebab-case `position`, `purpose`, exact `content`, plus optional `claimIds`, `evidenceIds`, `sourceNotes`, `renderNotes`, and `placementNote`.
|
|
56
|
+
- `visualIntent`; if `visualIntent.component` is set, it must match one of the component plan names.
|
|
57
|
+
- `narrativeLinks` using canonical claim/evidence/risk/objection/gap ids. Non-structural slides should include at least one claim or evidence link.
|