@cyber-dash-tech/revela 0.17.23 → 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/narrative-state/deck-plan-artifact.ts +426 -1
- package/lib/runtime/index.ts +40 -2
- package/package.json +1 -1
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +82 -0
- package/plugins/revela/skills/revela-design/SKILL.md +7 -1
- package/plugins/revela/skills/revela-make-deck/SKILL.md +10 -3
|
@@ -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
|
@@ -25,7 +25,7 @@ import { autoCompileNarrativeVault } from "../narrative-vault/auto-compile"
|
|
|
25
25
|
import { extractNarrativeVaultMarkdownTargetsFromPatch } from "../narrative-vault/hook-targets"
|
|
26
26
|
import { runNarrativeMarkdownQa, type MarkdownQaOptions } from "../narrative-vault/markdown-qa"
|
|
27
27
|
import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice } from "../hook-notifications"
|
|
28
|
-
import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
|
|
28
|
+
import { deckPlanDesignDiagnostics, readDeckPlanArtifact, upsertDeckPlanSlideArtifact, type DeckPlanSlideUpsertInput } from "../narrative-state/deck-plan-artifact"
|
|
29
29
|
import { extractDesignClasses } from "../design/designs"
|
|
30
30
|
import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
|
|
31
31
|
import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
|
|
@@ -76,6 +76,10 @@ export interface RuntimeDesignComponentReadInput {
|
|
|
76
76
|
component: string | string[]
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
export interface RuntimeDeckPlanSlideUpsertInput extends RuntimeWorkspaceInput, DeckPlanSlideUpsertInput {
|
|
80
|
+
designName?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
export interface RuntimeDesignCreateInput {
|
|
80
84
|
name: string
|
|
81
85
|
base?: string
|
|
@@ -149,10 +153,44 @@ export function readDeckPlan(input: RuntimeWorkspaceInput = {}) {
|
|
|
149
153
|
const workspaceRoot = root(input.workspaceRoot)
|
|
150
154
|
const compiled = compileNarrativeVault(workspaceRoot)
|
|
151
155
|
const knownNodeIds = compiled.graph ? new Set(compiled.graph.nodes.map((node) => node.id)) : undefined
|
|
152
|
-
|
|
156
|
+
const read = readDeckPlanArtifact(workspaceRoot, {
|
|
153
157
|
narrativeHash: compiled.narrative ? computeNarrativeHash(compiled.narrative) : undefined,
|
|
154
158
|
knownNodeIds,
|
|
155
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
|
+
}
|
|
156
194
|
}
|
|
157
195
|
|
|
158
196
|
export function createDeckFoundation(input: RuntimeDeckFoundationInput) {
|
package/package.json
CHANGED
package/plugins/revela/.mcp.json
CHANGED
|
@@ -13,6 +13,7 @@ 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>
|
|
@@ -77,6 +78,25 @@ const tools = [
|
|
|
77
78
|
description: "Read the file-native deck-plan/ projection and diagnostics.",
|
|
78
79
|
inputSchema: objectSchema({ workspaceRoot: stringProp("Optional workspace root.") }),
|
|
79
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
|
+
},
|
|
80
100
|
{
|
|
81
101
|
name: "revela_create_deck_foundation",
|
|
82
102
|
description: "Create or repair a file-native Revela HTML deck foundation shell.",
|
|
@@ -414,6 +434,7 @@ async function callTool(name: string, args: any): Promise<any> {
|
|
|
414
434
|
if (name === "revela_compile_narrative") return r.compileNarrative(args)
|
|
415
435
|
if (name === "revela_markdown_qa") return r.markdownQa(args)
|
|
416
436
|
if (name === "revela_read_deck_plan") return r.readDeckPlan(args)
|
|
437
|
+
if (name === "revela_upsert_deck_plan_slide") return r.upsertDeckPlanSlide(args)
|
|
417
438
|
if (name === "revela_create_deck_foundation") return r.createDeckFoundation(args)
|
|
418
439
|
if (name === "revela_run_deck_qa") return r.runDeckQa(args)
|
|
419
440
|
if (name === "revela_export_pdf") return r.exportPdf(args)
|
|
@@ -479,6 +500,10 @@ function numberProp(description: string) {
|
|
|
479
500
|
return { type: "number", description }
|
|
480
501
|
}
|
|
481
502
|
|
|
503
|
+
function requiredNumberProp(description: string) {
|
|
504
|
+
return { type: "number", description }
|
|
505
|
+
}
|
|
506
|
+
|
|
482
507
|
function enumProp(values: string[], description: string) {
|
|
483
508
|
return { type: "string", enum: values, description }
|
|
484
509
|
}
|
|
@@ -514,6 +539,63 @@ function arrayObjectProp(description: string) {
|
|
|
514
539
|
}
|
|
515
540
|
}
|
|
516
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
|
+
|
|
517
599
|
function writeMessage(message: any, mode: MessageMode = activeResponseMode): void {
|
|
518
600
|
activeResponseMode = mode
|
|
519
601
|
const body = JSON.stringify(message)
|
|
@@ -11,7 +11,7 @@ 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. Call `revela_design_inventory` before authoring or repairing `deck-plan/` so planned layout/component names come from the active design.
|
|
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
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
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
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.
|
|
@@ -23,6 +23,12 @@ Deck HTML must keep exactly one direct `.slide-canvas` child inside every `<sect
|
|
|
23
23
|
|
|
24
24
|
Design changes are visual/artifact-level unless they change claim meaning, evidence boundaries, decision, or recommendation.
|
|
25
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
|
+
|
|
26
32
|
## Creating Or Editing Designs
|
|
27
33
|
|
|
28
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.
|
|
@@ -20,11 +20,11 @@ Use this skill when the user asks to make, generate, or update a Revela deck.
|
|
|
20
20
|
2. Report narrative and Markdown diagnostics, but treat only malformed/unsafe files and technical artifact validity as hard blockers.
|
|
21
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
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,
|
|
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
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
25
|
7. Do not start HTML generation from narrative alone unless the user explicitly asks for a throwaway diagnostic smoke deck.
|
|
26
26
|
8. For new HTML files, call `revela_create_deck_foundation`.
|
|
27
|
-
9. Before patching slide HTML,
|
|
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
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
29
|
11. Generate chapter by chapter. Keep the HTML valid after each write.
|
|
30
30
|
12. After every HTML write, call `revela_run_deck_qa` and repair hard errors before review or export.
|
|
@@ -47,4 +47,11 @@ Use this skill when the user asks to make, generate, or update a Revela deck.
|
|
|
47
47
|
|
|
48
48
|
## Deck Plan Requirements
|
|
49
49
|
|
|
50
|
-
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.
|