@cyber-dash-tech/revela 0.17.24 → 0.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -25
- package/README.zh-CN.md +25 -26
- package/bin/revela.ts +2 -4
- package/lib/commands/help.ts +13 -13
- package/lib/commands/init.ts +24 -0
- package/lib/commands/png.ts +29 -0
- package/lib/commands/refine.ts +1 -1
- package/lib/commands/research.ts +24 -0
- package/lib/commands/review.ts +92 -14
- package/lib/decks-state.ts +7 -7
- package/lib/design/designs.ts +44 -0
- package/lib/narrative-state/deck-plan-artifact.ts +504 -99
- package/lib/narrative-state/render-plan.ts +13 -14
- package/lib/pdf/export.ts +84 -24
- package/lib/refine/server.ts +4 -3
- package/lib/runtime/index.ts +21 -14
- package/lib/runtime/review.ts +4 -104
- package/lib/workspace-state/render-targets.ts +2 -2
- package/lib/workspace-state/rendered-artifacts.ts +1 -1
- package/lib/workspace-state/types.ts +1 -1
- package/package.json +1 -1
- package/plugin.ts +31 -42
- package/plugins/revela/.codex-plugin/plugin.json +2 -2
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/mcp/revela-server.ts +58 -80
- package/plugins/revela/skills/revela-design/SKILL.md +4 -2
- package/plugins/revela/skills/revela-domain/SKILL.md +1 -1
- package/plugins/revela/skills/revela-export/SKILL.md +4 -5
- package/plugins/revela/skills/revela-init/SKILL.md +19 -34
- package/plugins/revela/skills/revela-make-deck/SKILL.md +15 -41
- package/plugins/revela/skills/revela-research/SKILL.md +17 -26
- package/plugins/revela/skills/revela-review-deck/SKILL.md +11 -29
- package/skill/SKILL.md +22 -19
- package/plugins/revela/skills/revela-story/SKILL.md +0 -24
|
@@ -9,10 +9,11 @@ import type { VaultRelation, WorkspaceGraphNodeType } from "../narrative-vault/t
|
|
|
9
9
|
import type { DeckPlanChapter, DeckPlanQualityCheck, RenderPlanContract, RenderPlanSlideMetadata } from "./render-plan"
|
|
10
10
|
|
|
11
11
|
export const DECK_PLAN_DIR = "deck-plan"
|
|
12
|
+
export const DECK_PLAN_MARKDOWN_PATH = "deck-plan.md"
|
|
12
13
|
export const DECK_PLAN_INDEX_PATH = "deck-plan/index.md"
|
|
13
14
|
export const DECK_PLAN_SLIDES_DIR = "deck-plan/slides"
|
|
14
15
|
export const LEGACY_DECK_PLAN_ARTIFACT_PATH = "decks/deck-plan.md"
|
|
15
|
-
export const DECK_PLAN_ARTIFACT_PATH =
|
|
16
|
+
export const DECK_PLAN_ARTIFACT_PATH = DECK_PLAN_MARKDOWN_PATH
|
|
16
17
|
|
|
17
18
|
export interface DeckPlanArtifactInput {
|
|
18
19
|
deck: DeckSpec
|
|
@@ -65,6 +66,7 @@ export interface DeckPlanProjection {
|
|
|
65
66
|
frontmatter: Record<string, string | string[] | boolean>
|
|
66
67
|
sections: string[]
|
|
67
68
|
narrativeHash?: string
|
|
69
|
+
designName?: string
|
|
68
70
|
outputPath?: string
|
|
69
71
|
slides: DeckPlanSlideProjection[]
|
|
70
72
|
graphNodes: Array<{ id: string; type: WorkspaceGraphNodeType; file: string }>
|
|
@@ -88,6 +90,8 @@ export interface DeckPlanSlideProjection {
|
|
|
88
90
|
frontmatter: Record<string, string | string[] | boolean>
|
|
89
91
|
sections: string[]
|
|
90
92
|
links: DeckPlanNarrativeLink[]
|
|
93
|
+
sourceLinks: DeckPlanSourceLinks
|
|
94
|
+
caveats: string[]
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
export interface DeckPlanSlideComponentPlan {
|
|
@@ -101,6 +105,7 @@ export interface DeckPlanSlideComponentPlan {
|
|
|
101
105
|
sourceNotes: string[]
|
|
102
106
|
renderNotes: string[]
|
|
103
107
|
placementNote?: string
|
|
108
|
+
children?: DeckPlanSlideComponentPlan[]
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
export interface DeckPlanSlideUpsertComponentInput {
|
|
@@ -114,9 +119,20 @@ export interface DeckPlanSlideUpsertComponentInput {
|
|
|
114
119
|
sourceNotes?: string[]
|
|
115
120
|
renderNotes?: string[]
|
|
116
121
|
placementNote?: string
|
|
122
|
+
children?: DeckPlanSlideUpsertComponentInput[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface DeckPlanSourceLinks {
|
|
126
|
+
materials: string[]
|
|
127
|
+
findings: string[]
|
|
128
|
+
assets: string[]
|
|
129
|
+
urls: string[]
|
|
130
|
+
caveats: string[]
|
|
117
131
|
}
|
|
118
132
|
|
|
119
133
|
export interface DeckPlanSlideUpsertInput {
|
|
134
|
+
designName?: string
|
|
135
|
+
outputPath?: string
|
|
120
136
|
slideIndex: number
|
|
121
137
|
id?: string
|
|
122
138
|
title: string
|
|
@@ -131,7 +147,8 @@ export interface DeckPlanSlideUpsertInput {
|
|
|
131
147
|
rationale?: string
|
|
132
148
|
brief?: string
|
|
133
149
|
} | string
|
|
134
|
-
|
|
150
|
+
sourceLinks?: Partial<DeckPlanSourceLinks>
|
|
151
|
+
narrativeLinks?: {
|
|
135
152
|
claimIds?: string[]
|
|
136
153
|
evidenceIds?: string[]
|
|
137
154
|
riskIds?: string[]
|
|
@@ -165,15 +182,14 @@ export interface DeckPlanProjectionDiagnostic {
|
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
export const REQUIRED_DECK_PLAN_SECTIONS = [
|
|
185
|
+
"Goal",
|
|
186
|
+
"Audience",
|
|
187
|
+
"Design",
|
|
168
188
|
"Source Authority",
|
|
169
|
-
"Audience / Goal / Decision",
|
|
170
|
-
"Deck Parameters",
|
|
171
189
|
"Chapter Map",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"Chapter Writing Batches",
|
|
176
|
-
"HTML Identity Contract",
|
|
190
|
+
"Slides",
|
|
191
|
+
"Unresolved Inputs",
|
|
192
|
+
"HTML Contract",
|
|
177
193
|
]
|
|
178
194
|
|
|
179
195
|
export function writeDeckPlanArtifact(workspaceRoot: string, input: DeckPlanArtifactInput): { path: string; absolutePath: string } {
|
|
@@ -185,17 +201,17 @@ export function writeDeckPlanArtifact(workspaceRoot: string, input: DeckPlanArti
|
|
|
185
201
|
|
|
186
202
|
export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrativeHash?: string; knownNodeIds?: Set<string> }): DeckPlanReadResult {
|
|
187
203
|
const projection = readDeckPlanProjection(workspaceRoot, expected)
|
|
188
|
-
const absolutePath = projection?.absolutePath ?? join(workspaceRoot,
|
|
204
|
+
const absolutePath = projection?.absolutePath ?? join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
189
205
|
if (!existsSync(absolutePath)) {
|
|
190
206
|
return {
|
|
191
207
|
ok: false,
|
|
192
|
-
path:
|
|
208
|
+
path: DECK_PLAN_MARKDOWN_PATH,
|
|
193
209
|
absolutePath,
|
|
194
210
|
approvalStatus: "missing",
|
|
195
211
|
sections: [],
|
|
196
212
|
missingSections: REQUIRED_DECK_PLAN_SECTIONS,
|
|
197
213
|
warnings: [],
|
|
198
|
-
reason: `Deck plan file is missing: ${
|
|
214
|
+
reason: `Deck plan file is missing: ${DECK_PLAN_MARKDOWN_PATH}. Write the LLM-authored deck plan before HTML generation.`,
|
|
199
215
|
}
|
|
200
216
|
}
|
|
201
217
|
const markdown = projection?.markdown ?? readFileSync(absolutePath, "utf-8")
|
|
@@ -221,7 +237,7 @@ export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrati
|
|
|
221
237
|
}
|
|
222
238
|
return {
|
|
223
239
|
ok: true,
|
|
224
|
-
path: projection?.path ??
|
|
240
|
+
path: projection?.path ?? DECK_PLAN_MARKDOWN_PATH,
|
|
225
241
|
absolutePath,
|
|
226
242
|
markdown,
|
|
227
243
|
planHash,
|
|
@@ -236,9 +252,10 @@ export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrati
|
|
|
236
252
|
|
|
237
253
|
export function readDeckPlanProjection(workspaceRoot: string, expected?: { narrativeHash?: string; knownNodeIds?: Set<string> }): DeckPlanProjection | undefined {
|
|
238
254
|
const root = join(workspaceRoot, DECK_PLAN_DIR)
|
|
255
|
+
const singlePath = join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
239
256
|
const indexPath = join(workspaceRoot, DECK_PLAN_INDEX_PATH)
|
|
240
257
|
const legacyPath = join(workspaceRoot, LEGACY_DECK_PLAN_ARTIFACT_PATH)
|
|
241
|
-
const absolutePath = existsSync(indexPath) ? indexPath : existsSync(legacyPath) ? legacyPath : ""
|
|
258
|
+
const absolutePath = existsSync(singlePath) ? singlePath : existsSync(indexPath) ? indexPath : existsSync(legacyPath) ? legacyPath : ""
|
|
242
259
|
if (!absolutePath) return undefined
|
|
243
260
|
const markdown = readFileSync(absolutePath, "utf-8")
|
|
244
261
|
const parsed = parseVaultFrontmatter(markdown)
|
|
@@ -246,7 +263,8 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
246
263
|
const sections = parseMarkdownSections(markdown)
|
|
247
264
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
248
265
|
const id = stringField(parsed.frontmatter, "id") || "deck-plan"
|
|
249
|
-
const
|
|
266
|
+
const isSingleFile = relativePath(workspaceRoot, absolutePath) === DECK_PLAN_MARKDOWN_PATH
|
|
267
|
+
const slides = isSingleFile ? readDeckPlanSlidesFromSingleFile(workspaceRoot, absolutePath, markdown, expected?.knownNodeIds) : existsSync(join(root, "slides")) ? readDeckPlanSlideFiles(workspaceRoot, expected?.knownNodeIds) : []
|
|
250
268
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
251
269
|
const narrativeHash = stringField(parsed.frontmatter, "narrativeHash") || narrativeHashFromMarkdown(markdown)
|
|
252
270
|
if (expected?.narrativeHash && narrativeHash && narrativeHash !== expected.narrativeHash) diagnostics.push({ severity: "warning", code: "stale_narrative_hash", message: "Deck plan narrativeHash does not match current narrative state.", file: path, nodeId: id })
|
|
@@ -273,7 +291,8 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
273
291
|
frontmatter: parsed.frontmatter,
|
|
274
292
|
sections,
|
|
275
293
|
narrativeHash,
|
|
276
|
-
|
|
294
|
+
designName: stringField(parsed.frontmatter, "designName") || stringField(parsed.frontmatter, "design"),
|
|
295
|
+
outputPath: stringField(parsed.frontmatter, "outputPath") || stringField(parsed.frontmatter, "output"),
|
|
277
296
|
slides,
|
|
278
297
|
graphNodes,
|
|
279
298
|
graphRelations,
|
|
@@ -311,36 +330,209 @@ export function deckPlanBodyHash(markdown: string): string {
|
|
|
311
330
|
export function upsertDeckPlanSlideArtifact(
|
|
312
331
|
workspaceRoot: string,
|
|
313
332
|
input: DeckPlanSlideUpsertInput,
|
|
314
|
-
options: { narrativeHash?: string; knownNodeIds?: Set<string>; designLayouts: string[]; designComponents: string[] },
|
|
333
|
+
options: { narrativeHash?: string; knownNodeIds?: Set<string>; designLayouts: string[]; designComponents: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> },
|
|
315
334
|
): DeckPlanSlideUpsertResult {
|
|
316
335
|
const diagnostics = validateDeckPlanSlideUpsert(input, options)
|
|
317
336
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return { ok: false, diagnostics }
|
|
318
337
|
|
|
319
|
-
mkdirSync(join(workspaceRoot, DECK_PLAN_SLIDES_DIR), { recursive: true })
|
|
320
|
-
ensureDeckPlanIndex(workspaceRoot, options.narrativeHash)
|
|
321
|
-
|
|
322
338
|
const existing = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
323
339
|
const existingSlide = existing?.slides.find((slide) => slide.slideIndex === input.slideIndex)
|
|
324
340
|
const id = input.id?.trim() || existingSlide?.id || `slide-${slugify(input.title)}`
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
// duplicate slideIndex diagnostics will surface on the next read.
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
updateDeckPlanIndex(workspaceRoot, options.narrativeHash)
|
|
341
|
+
const nextSlide = projectionFromSlideInput(workspaceRoot, { ...input, id }, existingSlide)
|
|
342
|
+
const slides = [...(existing?.slides.filter((slide) => slide.slideIndex !== input.slideIndex) ?? []), nextSlide]
|
|
343
|
+
.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
344
|
+
const designName = input.designName || existing?.designName
|
|
345
|
+
const outputPath = input.outputPath || existing?.outputPath
|
|
346
|
+
writeDeckPlanSingleFile(workspaceRoot, {
|
|
347
|
+
title: "Deck Plan",
|
|
348
|
+
designName,
|
|
349
|
+
outputPath,
|
|
350
|
+
slides,
|
|
351
|
+
})
|
|
341
352
|
const projection = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
342
353
|
const slide = projection?.slides.find((item) => item.slideIndex === input.slideIndex)
|
|
343
|
-
return { ok: true, path, absolutePath, updated: Boolean(existingSlide), slide, diagnostics: [...diagnostics, ...(projection?.diagnostics ?? [])] }
|
|
354
|
+
return { ok: true, path: DECK_PLAN_MARKDOWN_PATH, absolutePath: join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH), updated: Boolean(existingSlide), slide, diagnostics: [...diagnostics, ...(projection?.diagnostics ?? [])] }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function projectionFromSlideInput(workspaceRoot: string, input: DeckPlanSlideUpsertInput & { id: string }, existing?: DeckPlanSlideProjection): DeckPlanSlideProjection {
|
|
358
|
+
const sourceLinks = sourceLinksForInput(input)
|
|
359
|
+
const caveats = uniqueStrings([...(input.caveats ?? []), ...sourceLinks.caveats])
|
|
360
|
+
const componentPlan = input.components.map(componentInputToPlan)
|
|
361
|
+
const slide: DeckPlanSlideProjection = {
|
|
362
|
+
path: DECK_PLAN_MARKDOWN_PATH,
|
|
363
|
+
absolutePath: join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH),
|
|
364
|
+
id: input.id,
|
|
365
|
+
slideIndex: input.slideIndex,
|
|
366
|
+
title: input.title,
|
|
367
|
+
chapter: input.chapter,
|
|
368
|
+
layout: input.layout,
|
|
369
|
+
components: uniqueStrings(componentPlan.flatMap(flattenComponentNames)),
|
|
370
|
+
componentPlan,
|
|
371
|
+
structural: input.structural ?? false,
|
|
372
|
+
narrativeRole: input.narrativeRole,
|
|
373
|
+
markdown: "",
|
|
374
|
+
frontmatter: existing?.frontmatter ?? {},
|
|
375
|
+
sections: [],
|
|
376
|
+
links: sourceLinksToNarrativeLinks(sourceLinks, input.narrativeLinks ? sourceLinksToNarrativeLinks(sourceLinksFromNarrativeLinks(input.narrativeLinks)) : []),
|
|
377
|
+
sourceLinks,
|
|
378
|
+
caveats,
|
|
379
|
+
}
|
|
380
|
+
slide.markdown = renderDeckPlanSlideBlock(slide, input.visualIntent)
|
|
381
|
+
return slide
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function componentInputToPlan(component: DeckPlanSlideUpsertComponentInput): DeckPlanSlideComponentPlan {
|
|
385
|
+
return normalizeComponentPlan({
|
|
386
|
+
name: component.name?.trim() || "",
|
|
387
|
+
slot: component.slot?.trim() || "",
|
|
388
|
+
position: component.position?.trim() || "",
|
|
389
|
+
purpose: component.purpose?.trim() || "",
|
|
390
|
+
content: component.content?.trim() || "",
|
|
391
|
+
claimIds: uniqueStrings(component.claimIds ?? []),
|
|
392
|
+
evidenceIds: uniqueStrings(component.evidenceIds ?? []),
|
|
393
|
+
sourceNotes: (component.sourceNotes ?? []).map((item) => item.trim()).filter(Boolean),
|
|
394
|
+
renderNotes: (component.renderNotes ?? []).map((item) => item.trim()).filter(Boolean),
|
|
395
|
+
placementNote: component.placementNote?.trim(),
|
|
396
|
+
children: component.children?.map(componentInputToPlan),
|
|
397
|
+
})
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function flattenComponentNames(component: DeckPlanSlideComponentPlan): string[] {
|
|
401
|
+
return [component.name, ...(component.children ?? []).flatMap(flattenComponentNames)].filter(Boolean)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function writeDeckPlanSingleFile(workspaceRoot: string, input: {
|
|
405
|
+
title: string
|
|
406
|
+
goal?: string
|
|
407
|
+
audience?: string
|
|
408
|
+
designName?: string
|
|
409
|
+
outputPath?: string
|
|
410
|
+
sourceAuthority?: string[]
|
|
411
|
+
unresolvedInputs?: string[]
|
|
412
|
+
slides: DeckPlanSlideProjection[]
|
|
413
|
+
}): { path: string; absolutePath: string } {
|
|
414
|
+
const absolutePath = join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
415
|
+
const lines: string[] = []
|
|
416
|
+
lines.push("---")
|
|
417
|
+
lines.push("type: deck-plan")
|
|
418
|
+
lines.push("version: 0.18.1")
|
|
419
|
+
if (input.designName) lines.push(`designName: ${yamlScalar(input.designName)}`)
|
|
420
|
+
if (input.outputPath) lines.push(`outputPath: ${yamlScalar(input.outputPath)}`)
|
|
421
|
+
lines.push("---")
|
|
422
|
+
lines.push("")
|
|
423
|
+
lines.push(`# ${input.title || "Deck Plan"}`)
|
|
424
|
+
lines.push("")
|
|
425
|
+
lines.push("## Goal")
|
|
426
|
+
lines.push("")
|
|
427
|
+
lines.push(input.goal || "To be specified from user intent and source materials.")
|
|
428
|
+
lines.push("")
|
|
429
|
+
lines.push("## Audience")
|
|
430
|
+
lines.push("")
|
|
431
|
+
lines.push(input.audience || "To be specified.")
|
|
432
|
+
lines.push("")
|
|
433
|
+
lines.push("## Design")
|
|
434
|
+
lines.push("")
|
|
435
|
+
lines.push(`- Design: ${input.designName || "active design"}`)
|
|
436
|
+
if (input.outputPath) lines.push(`- Output path: ${input.outputPath}`)
|
|
437
|
+
lines.push("")
|
|
438
|
+
lines.push("## Source Authority")
|
|
439
|
+
lines.push("")
|
|
440
|
+
const sourceAuthority = input.sourceAuthority?.filter(Boolean) ?? ["Local materials, reviewed findings, workspace assets, explicit URLs, and user intent are the source context.", "Deck-plan is the render execution plan for HTML deck generation."]
|
|
441
|
+
for (const item of sourceAuthority) lines.push(`- ${item}`)
|
|
442
|
+
lines.push("")
|
|
443
|
+
lines.push("## Chapter Map")
|
|
444
|
+
lines.push("")
|
|
445
|
+
const chapterMap = new Map<string, number[]>()
|
|
446
|
+
for (const slide of input.slides) chapterMap.set(slide.chapter || "Unassigned", [...(chapterMap.get(slide.chapter || "Unassigned") ?? []), slide.slideIndex ?? 0].filter(Boolean))
|
|
447
|
+
for (const [chapter, indexes] of chapterMap) lines.push(`- ${chapter}: slides ${formatSlideRange(indexes)}`)
|
|
448
|
+
if (chapterMap.size === 0) lines.push("- No slides planned yet.")
|
|
449
|
+
lines.push("")
|
|
450
|
+
lines.push("## Slides")
|
|
451
|
+
lines.push("")
|
|
452
|
+
for (const slide of input.slides) {
|
|
453
|
+
lines.push(renderDeckPlanSlideBlock(slide))
|
|
454
|
+
lines.push("")
|
|
455
|
+
}
|
|
456
|
+
lines.push("## Unresolved Inputs")
|
|
457
|
+
lines.push("")
|
|
458
|
+
const unresolved = input.unresolvedInputs?.filter(Boolean) ?? []
|
|
459
|
+
if (unresolved.length === 0) lines.push("- None.")
|
|
460
|
+
else for (const item of unresolved) lines.push(`- ${item}`)
|
|
461
|
+
lines.push("")
|
|
462
|
+
lines.push("## HTML Contract")
|
|
463
|
+
lines.push("")
|
|
464
|
+
lines.push("- Render one `<section class=\"slide\" data-slide-index=\"N\">` per planned slide.")
|
|
465
|
+
lines.push("- Use positive 1-based slide indexes, unique indexes, DOM order, and one direct `.slide-canvas` child per slide.")
|
|
466
|
+
lines.push("")
|
|
467
|
+
writeFileSync(absolutePath, lines.join("\n"), "utf-8")
|
|
468
|
+
return { path: DECK_PLAN_MARKDOWN_PATH, absolutePath }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function renderDeckPlanSlideBlock(slide: DeckPlanSlideProjection, visualIntent?: DeckPlanSlideUpsertInput["visualIntent"]): string {
|
|
472
|
+
const lines: string[] = []
|
|
473
|
+
lines.push(`### Slide ${slide.slideIndex ?? "?"} — ${slide.title}`)
|
|
474
|
+
lines.push("")
|
|
475
|
+
lines.push(`- Id: ${slide.id}`)
|
|
476
|
+
lines.push(`- Chapter: ${slide.chapter || "Unassigned"}`)
|
|
477
|
+
lines.push(`- Role: ${slide.narrativeRole || "Not specified"}`)
|
|
478
|
+
lines.push(`- Structural: ${slide.structural ? "true" : "false"}`)
|
|
479
|
+
lines.push(`- Layout: ${slide.layout || "unspecified"}`)
|
|
480
|
+
lines.push(`- Components: ${slide.components.join(", ") || "none"}`)
|
|
481
|
+
lines.push("")
|
|
482
|
+
lines.push("#### Visual Intent")
|
|
483
|
+
lines.push("")
|
|
484
|
+
if (visualIntent) lines.push(renderVisualIntent(visualIntent))
|
|
485
|
+
else lines.push("- Brief: Not specified.")
|
|
486
|
+
lines.push("")
|
|
487
|
+
lines.push("#### Component Plan")
|
|
488
|
+
lines.push("")
|
|
489
|
+
for (const component of slide.componentPlan) lines.push(renderComponentPlanMarkdown(component, 5))
|
|
490
|
+
lines.push("#### Source Links")
|
|
491
|
+
lines.push("")
|
|
492
|
+
lines.push(renderSourceLinksMarkdown(slide.sourceLinks))
|
|
493
|
+
lines.push("")
|
|
494
|
+
lines.push("#### Caveats")
|
|
495
|
+
lines.push("")
|
|
496
|
+
if (slide.caveats.length === 0) lines.push("- None.")
|
|
497
|
+
else for (const caveat of slide.caveats) lines.push(`- ${caveat}`)
|
|
498
|
+
lines.push("")
|
|
499
|
+
return lines.join("\n")
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function renderComponentPlanMarkdown(component: DeckPlanSlideComponentPlan, headingLevel: number): string {
|
|
503
|
+
const lines: string[] = []
|
|
504
|
+
lines.push(`${"#".repeat(headingLevel)} ${component.name}`)
|
|
505
|
+
lines.push("")
|
|
506
|
+
lines.push(`- Slot: ${component.slot}`)
|
|
507
|
+
lines.push(`- Position: ${component.position}`)
|
|
508
|
+
if (component.placementNote) lines.push(`- Placement note: ${component.placementNote}`)
|
|
509
|
+
lines.push(`- Purpose: ${component.purpose}`)
|
|
510
|
+
lines.push("- Content:")
|
|
511
|
+
lines.push(...indentMultiline(component.content))
|
|
512
|
+
lines.push(`- Claim ids: ${formatCsv(component.claimIds)}`)
|
|
513
|
+
lines.push(`- Evidence ids: ${formatCsv(component.evidenceIds)}`)
|
|
514
|
+
lines.push(`- Source notes: ${formatListValue(component.sourceNotes)}`)
|
|
515
|
+
lines.push(`- Render notes: ${formatListValue(component.renderNotes)}`)
|
|
516
|
+
lines.push("")
|
|
517
|
+
for (const child of component.children ?? []) lines.push(renderComponentPlanMarkdown(child, headingLevel + 1))
|
|
518
|
+
return lines.join("\n")
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function renderSourceLinksMarkdown(sourceLinks: DeckPlanSourceLinks): string {
|
|
522
|
+
const lines: string[] = []
|
|
523
|
+
for (const [label, values] of [
|
|
524
|
+
["Materials", sourceLinks.materials],
|
|
525
|
+
["Findings", sourceLinks.findings],
|
|
526
|
+
["Assets", sourceLinks.assets],
|
|
527
|
+
["URLs", sourceLinks.urls],
|
|
528
|
+
["Caveats", sourceLinks.caveats],
|
|
529
|
+
] as const) {
|
|
530
|
+
lines.push(`${label}:`)
|
|
531
|
+
if (values.length === 0) lines.push("- None.")
|
|
532
|
+
else for (const value of values) lines.push(value.includes("/") && !/^https?:\/\//i.test(value) ? `- [[${value}]]` : `- ${value}`)
|
|
533
|
+
lines.push("")
|
|
534
|
+
}
|
|
535
|
+
return lines.join("\n").trim()
|
|
344
536
|
}
|
|
345
537
|
|
|
346
538
|
function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
@@ -356,7 +548,9 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
356
548
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
357
549
|
const id = stringField(parsed.frontmatter, "id") || fileId(entry)
|
|
358
550
|
const componentPlan = parseDeckPlanComponentPlan(split.sections["component-plan"] ?? "")
|
|
359
|
-
const
|
|
551
|
+
const sourceLinks = parseDeckPlanSourceLinks(split.sections["source-links"] ?? "")
|
|
552
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, parseDeckPlanNarrativeLinks(split.sections["narrative-links"] ?? parsed.body, knownNodeIds))
|
|
553
|
+
const caveats = parseBulletText(split.sections["caveats"] ?? "")
|
|
360
554
|
slides.push({
|
|
361
555
|
path,
|
|
362
556
|
absolutePath,
|
|
@@ -373,11 +567,80 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
373
567
|
frontmatter: parsed.frontmatter,
|
|
374
568
|
sections: parseMarkdownSections(markdown),
|
|
375
569
|
links,
|
|
570
|
+
sourceLinks,
|
|
571
|
+
caveats,
|
|
376
572
|
})
|
|
377
573
|
}
|
|
378
574
|
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER) || a.path.localeCompare(b.path))
|
|
379
575
|
}
|
|
380
576
|
|
|
577
|
+
function readDeckPlanSlidesFromSingleFile(workspaceRoot: string, absolutePath: string, markdown: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
578
|
+
const path = relativePath(workspaceRoot, absolutePath)
|
|
579
|
+
const body = parseVaultFrontmatter(markdown).body
|
|
580
|
+
const matches = [...body.matchAll(/^[ \t]*###\s+Slide\s+(\d+)\s+(?:—|-)\s+(.+?)\s*$/gm)]
|
|
581
|
+
const slides: DeckPlanSlideProjection[] = []
|
|
582
|
+
for (let i = 0; i < matches.length; i++) {
|
|
583
|
+
const match = matches[i]
|
|
584
|
+
const start = match.index ?? 0
|
|
585
|
+
const nextSlide = i + 1 < matches.length ? matches[i + 1].index ?? body.length : body.length
|
|
586
|
+
const headingEnd = body.indexOf("\n", start)
|
|
587
|
+
const searchStart = headingEnd === -1 ? start + match[0].length : headingEnd + 1
|
|
588
|
+
const nextSection = body.slice(searchStart).search(/^[ \t]*##\s+(?!#)/m)
|
|
589
|
+
const end = Math.min(nextSlide, nextSection === -1 ? body.length : searchStart + nextSection)
|
|
590
|
+
const block = body.slice(start, end).trim()
|
|
591
|
+
const slideIndex = Number(match[1])
|
|
592
|
+
const title = match[2].trim()
|
|
593
|
+
const fields = parseSlideBlockFields(block)
|
|
594
|
+
const id = fields.id || `slide-${slugify(title)}`
|
|
595
|
+
const sourceLinks = normalizeSourceLinks(parseDeckPlanSourceLinks(singleFileSubsection(block, "Source Links")))
|
|
596
|
+
const narrativeLinks = parseDeckPlanNarrativeLinks(singleFileSubsection(block, "Narrative Links") || block, knownNodeIds)
|
|
597
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, narrativeLinks)
|
|
598
|
+
const caveats = uniqueStrings([...parseBulletText(singleFileSubsection(block, "Caveats")), ...sourceLinks.caveats])
|
|
599
|
+
const componentPlan = parseDeckPlanComponentPlan(singleFileSubsection(block, "Component Plan"))
|
|
600
|
+
slides.push({
|
|
601
|
+
path,
|
|
602
|
+
absolutePath,
|
|
603
|
+
id,
|
|
604
|
+
slideIndex,
|
|
605
|
+
title,
|
|
606
|
+
chapter: fields.chapter || "",
|
|
607
|
+
layout: fields.layout || "",
|
|
608
|
+
components: parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
609
|
+
componentPlan,
|
|
610
|
+
structural: fields.structural === "true" || fields.structural === "yes",
|
|
611
|
+
narrativeRole: fields.role || fields.narrativeRole || "",
|
|
612
|
+
markdown: block,
|
|
613
|
+
frontmatter: {},
|
|
614
|
+
sections: parseMarkdownSections(block),
|
|
615
|
+
links,
|
|
616
|
+
sourceLinks,
|
|
617
|
+
caveats,
|
|
618
|
+
})
|
|
619
|
+
}
|
|
620
|
+
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function parseSlideBlockFields(block: string): Record<string, string> {
|
|
624
|
+
const fields: Record<string, string> = {}
|
|
625
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
626
|
+
const match = /^-\s+([A-Za-z][A-Za-z ]+):\s*(.*)$/.exec(rawLine.trim())
|
|
627
|
+
if (!match) continue
|
|
628
|
+
const key = match[1].trim().replace(/\s+/g, "")
|
|
629
|
+
fields[key[0].toLowerCase() + key.slice(1)] = cleanPlanValue(match[2])
|
|
630
|
+
}
|
|
631
|
+
return fields
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function singleFileSubsection(block: string, heading: string): string {
|
|
635
|
+
const re = new RegExp(`^[ \\t]*####\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "mi")
|
|
636
|
+
const match = re.exec(block)
|
|
637
|
+
if (!match || match.index === undefined) return ""
|
|
638
|
+
const start = match.index + match[0].length
|
|
639
|
+
const rest = block.slice(start)
|
|
640
|
+
const next = rest.search(/^[ \t]*####\s+/m)
|
|
641
|
+
return (next === -1 ? rest : rest.slice(0, next)).trim()
|
|
642
|
+
}
|
|
643
|
+
|
|
381
644
|
function parseDeckPlanNarrativeLinks(section: string, knownNodeIds?: Set<string>): DeckPlanNarrativeLink[] {
|
|
382
645
|
const links: DeckPlanNarrativeLink[] = []
|
|
383
646
|
let group = ""
|
|
@@ -396,8 +659,97 @@ function parseDeckPlanNarrativeLinks(section: string, knownNodeIds?: Set<string>
|
|
|
396
659
|
return uniqueLinks(links)
|
|
397
660
|
}
|
|
398
661
|
|
|
662
|
+
function emptySourceLinks(): DeckPlanSourceLinks {
|
|
663
|
+
return { materials: [], findings: [], assets: [], urls: [], caveats: [] }
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function normalizeSourceLinks(input?: Partial<DeckPlanSourceLinks>): DeckPlanSourceLinks {
|
|
667
|
+
return {
|
|
668
|
+
materials: uniqueStrings(input?.materials ?? []),
|
|
669
|
+
findings: uniqueStrings(input?.findings ?? []),
|
|
670
|
+
assets: uniqueStrings(input?.assets ?? []),
|
|
671
|
+
urls: uniqueStrings(input?.urls ?? []),
|
|
672
|
+
caveats: uniqueStrings(input?.caveats ?? []),
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function sourceLinksFromNarrativeLinks(input?: DeckPlanSlideUpsertInput["narrativeLinks"]): DeckPlanSourceLinks {
|
|
677
|
+
const links = emptySourceLinks()
|
|
678
|
+
for (const id of input?.evidenceIds ?? []) {
|
|
679
|
+
if (/^https?:\/\//i.test(id)) links.urls.push(id)
|
|
680
|
+
else if (id.startsWith("assets/")) links.assets.push(id)
|
|
681
|
+
else if (id.startsWith("researches/")) links.findings.push(id)
|
|
682
|
+
else if (id.startsWith("materials/") || id.startsWith("sources/")) links.materials.push(id)
|
|
683
|
+
else links.findings.push(id)
|
|
684
|
+
}
|
|
685
|
+
for (const id of input?.claimIds ?? []) {
|
|
686
|
+
if (id.startsWith("researches/")) links.findings.push(id)
|
|
687
|
+
else if (id.startsWith("assets/")) links.assets.push(id)
|
|
688
|
+
else links.materials.push(id)
|
|
689
|
+
}
|
|
690
|
+
links.caveats.push(...(input?.riskIds ?? []), ...(input?.objectionIds ?? []), ...(input?.gapIds ?? []))
|
|
691
|
+
return normalizeSourceLinks(links)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function sourceLinksForInput(input: DeckPlanSlideUpsertInput): DeckPlanSourceLinks {
|
|
695
|
+
return normalizeSourceLinks({
|
|
696
|
+
...sourceLinksFromNarrativeLinks(input.narrativeLinks),
|
|
697
|
+
...(input.sourceLinks ?? {}),
|
|
698
|
+
caveats: [...(sourceLinksFromNarrativeLinks(input.narrativeLinks).caveats ?? []), ...(input.sourceLinks?.caveats ?? []), ...(input.caveats ?? [])],
|
|
699
|
+
})
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function parseDeckPlanSourceLinks(section: string): DeckPlanSourceLinks {
|
|
703
|
+
const links = emptySourceLinks()
|
|
704
|
+
let group: keyof DeckPlanSourceLinks | undefined
|
|
705
|
+
for (const rawLine of section.replace(/\r\n/g, "\n").split("\n")) {
|
|
706
|
+
const heading = /^\s*([A-Za-z][A-Za-z\s/-]*):\s*$/.exec(rawLine)
|
|
707
|
+
if (heading) {
|
|
708
|
+
const normalized = heading[1].trim().toLowerCase()
|
|
709
|
+
if (normalized.includes("material")) group = "materials"
|
|
710
|
+
else if (normalized.includes("finding") || normalized.includes("research")) group = "findings"
|
|
711
|
+
else if (normalized.includes("asset") || normalized.includes("media")) group = "assets"
|
|
712
|
+
else if (normalized.includes("url") || normalized.includes("link")) group = "urls"
|
|
713
|
+
else if (normalized.includes("caveat") || normalized.includes("risk") || normalized.includes("gap")) group = "caveats"
|
|
714
|
+
else group = undefined
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
const bullet = rawLine.replace(/^\s*[-*]\s+/, "").trim()
|
|
718
|
+
if (!bullet || bullet.toLowerCase() === "none.") continue
|
|
719
|
+
const wikilink = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/.exec(bullet)?.[1]?.trim()
|
|
720
|
+
const value = wikilink || bullet
|
|
721
|
+
if (group) links[group].push(value)
|
|
722
|
+
else if (/^https?:\/\//i.test(value)) links.urls.push(value)
|
|
723
|
+
else if (value.startsWith("assets/")) links.assets.push(value)
|
|
724
|
+
else if (value.startsWith("researches/")) links.findings.push(value)
|
|
725
|
+
else links.materials.push(value)
|
|
726
|
+
}
|
|
727
|
+
return normalizeSourceLinks(links)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function sourceLinksToNarrativeLinks(sourceLinks: DeckPlanSourceLinks, compatibility: DeckPlanNarrativeLink[] = []): DeckPlanNarrativeLink[] {
|
|
731
|
+
const links: DeckPlanNarrativeLink[] = []
|
|
732
|
+
for (const id of sourceLinks.materials) links.push({ id, relation: "uses_evidence", group: "materials" })
|
|
733
|
+
for (const id of sourceLinks.findings) links.push({ id, relation: "uses_evidence", group: "findings" })
|
|
734
|
+
for (const id of sourceLinks.assets) links.push({ id, relation: "uses_evidence", group: "assets" })
|
|
735
|
+
for (const id of sourceLinks.urls) links.push({ id, relation: "uses_evidence", group: "urls" })
|
|
736
|
+
for (const id of sourceLinks.caveats) links.push({ id, relation: "mentions_gap", group: "caveats" })
|
|
737
|
+
return uniqueLinks([...links, ...compatibility])
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function parseBulletText(section: string): string[] {
|
|
741
|
+
return section
|
|
742
|
+
.replace(/\r\n/g, "\n")
|
|
743
|
+
.split("\n")
|
|
744
|
+
.map((line) => line.replace(/^\s*[-*]\s+/, "").trim())
|
|
745
|
+
.filter((line) => line && line.toLowerCase() !== "none.")
|
|
746
|
+
}
|
|
747
|
+
|
|
399
748
|
function relationForDeckPlanLink(group: string, id: string): DeckPlanNarrativeLink["relation"] {
|
|
400
749
|
const normalized = group.toLowerCase()
|
|
750
|
+
if (normalized.includes("source") || normalized.includes("material")) return "uses_evidence"
|
|
751
|
+
if (normalized.includes("finding") || normalized.includes("research")) return "uses_evidence"
|
|
752
|
+
if (normalized.includes("asset") || normalized.includes("media")) return "uses_evidence"
|
|
401
753
|
if (normalized.includes("evidence") || id.startsWith("evidence")) return "uses_evidence"
|
|
402
754
|
if (normalized.includes("risk") || id.startsWith("risk")) return "addresses_risk"
|
|
403
755
|
if (normalized.includes("objection") || id.startsWith("objection")) return "answers_objection"
|
|
@@ -406,6 +758,8 @@ function relationForDeckPlanLink(group: string, id: string): DeckPlanNarrativeLi
|
|
|
406
758
|
}
|
|
407
759
|
|
|
408
760
|
function inferredLinkGroup(id: string, knownNodeIds?: Set<string>): string {
|
|
761
|
+
if (id.startsWith("researches/")) return "findings"
|
|
762
|
+
if (id.startsWith("assets/")) return "assets"
|
|
409
763
|
if (id.startsWith("evidence")) return "evidence"
|
|
410
764
|
if (id.startsWith("risk")) return "risk"
|
|
411
765
|
if (id.startsWith("objection")) return "objection"
|
|
@@ -416,7 +770,7 @@ function inferredLinkGroup(id: string, knownNodeIds?: Set<string>): string {
|
|
|
416
770
|
|
|
417
771
|
function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanProjectionDiagnostic[] {
|
|
418
772
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
419
|
-
if (slides.length === 0) diagnostics.push({ severity: "warning", code: "deck_plan_slides_missing", message: "deck-plan
|
|
773
|
+
if (slides.length === 0) diagnostics.push({ severity: "warning", code: "deck_plan_slides_missing", message: "deck-plan.md contains no slide blocks." })
|
|
420
774
|
const seen = new Map<number, DeckPlanSlideProjection>()
|
|
421
775
|
let previous = 0
|
|
422
776
|
for (const slide of slides) {
|
|
@@ -435,7 +789,7 @@ function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanPr
|
|
|
435
789
|
|
|
436
790
|
function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
437
791
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
438
|
-
if (!slide.structural &&
|
|
792
|
+
if (!slide.structural && slide.links.length === 0 && slide.caveats.length === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: `Non-structural deck-plan slide ${slide.id} has no source, research, asset, or caveat link.`, file: slide.path, nodeId: slide.id })
|
|
439
793
|
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
794
|
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
795
|
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 })
|
|
@@ -452,24 +806,30 @@ function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<str
|
|
|
452
806
|
return diagnostics
|
|
453
807
|
}
|
|
454
808
|
|
|
455
|
-
export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undefined, inventory: { layouts: string[]; components: string[] }): DeckPlanProjectionDiagnostic[] {
|
|
809
|
+
export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undefined, inventory: { layouts: string[]; components: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }): DeckPlanProjectionDiagnostic[] {
|
|
456
810
|
if (!projection) return []
|
|
457
811
|
const layouts = new Set(inventory.layouts)
|
|
458
812
|
const components = new Set(inventory.components)
|
|
459
813
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
460
814
|
for (const slide of projection.slides) {
|
|
461
815
|
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
|
-
}
|
|
816
|
+
for (const component of slide.componentPlan) diagnostics.push(...componentDesignDiagnostics(slide, component, inventory, components))
|
|
468
817
|
}
|
|
469
818
|
return diagnostics
|
|
470
819
|
}
|
|
471
820
|
|
|
472
|
-
function
|
|
821
|
+
function componentDesignDiagnostics(slide: DeckPlanSlideProjection, component: DeckPlanSlideComponentPlan, inventory: { layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }, components: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
822
|
+
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
823
|
+
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 })
|
|
824
|
+
const allowedSlots = slide.layout ? inventory.layoutSlots?.[slide.layout] : undefined
|
|
825
|
+
if (component.slot && allowedSlots && allowedSlots.length > 0 && !allowedSlots.includes(component.slot)) diagnostics.push({ severity: "warning", code: "slide_component_slot_invalid", message: `Deck-plan slide ${slide.id} component '${component.name}' uses slot '${component.slot}' outside layout '${slide.layout}' slots: ${allowedSlots.join(", ")}.`, file: slide.path, nodeId: slide.id })
|
|
826
|
+
const nesting = inventory.componentNesting?.[component.name]
|
|
827
|
+
if ((component.children?.length ?? 0) > 0 && nesting && !nesting.acceptsChildren) diagnostics.push({ severity: "warning", code: "slide_component_children_invalid", message: `Deck-plan slide ${slide.id} component '${component.name}' does not accept children.`, file: slide.path, nodeId: slide.id })
|
|
828
|
+
for (const child of component.children ?? []) diagnostics.push(...componentDesignDiagnostics(slide, child, inventory, components))
|
|
829
|
+
return diagnostics
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: { designLayouts: string[]; designComponents: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }): DeckPlanProjectionDiagnostic[] {
|
|
473
833
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
474
834
|
const nodeId = input.id?.trim() || `slide-${input.slideIndex}`
|
|
475
835
|
if (!Number.isInteger(input.slideIndex) || input.slideIndex < 1) diagnostics.push(errorDiagnostic("slide_index_invalid", "slideIndex must be a positive 1-based integer.", nodeId))
|
|
@@ -480,25 +840,54 @@ function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: {
|
|
|
480
840
|
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
841
|
const componentNames = new Set<string>()
|
|
482
842
|
const positions = new Set<string>()
|
|
843
|
+
const allowedSlots = options.layoutSlots?.[input.layout]
|
|
483
844
|
for (const component of input.components ?? []) {
|
|
845
|
+
validateComponentInput(component, { nodeId, componentNames, positions, options, allowedSlots, parentName: undefined, topLevel: true, diagnostics })
|
|
846
|
+
}
|
|
847
|
+
const visual = normalizeVisualIntent(input.visualIntent)
|
|
848
|
+
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))
|
|
849
|
+
const sourceLinks = sourceLinksForInput(input)
|
|
850
|
+
if (!input.structural && linksCount(sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: "Non-structural slides should include at least one material, finding, asset, URL, or caveat source link.", nodeId })
|
|
851
|
+
return diagnostics
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function validateComponentInput(component: DeckPlanSlideUpsertComponentInput, context: {
|
|
855
|
+
nodeId: string
|
|
856
|
+
componentNames: Set<string>
|
|
857
|
+
positions: Set<string>
|
|
858
|
+
options: { designComponents: string[]; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }
|
|
859
|
+
allowedSlots?: string[]
|
|
860
|
+
parentName?: string
|
|
861
|
+
topLevel: boolean
|
|
862
|
+
diagnostics: DeckPlanProjectionDiagnostic[]
|
|
863
|
+
}): void {
|
|
484
864
|
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))
|
|
865
|
+
if (name) context.componentNames.add(name)
|
|
866
|
+
if (!name) context.diagnostics.push(errorDiagnostic("slide_component_name_missing", "Every component requires name.", context.nodeId))
|
|
867
|
+
else if (!context.options.designComponents.includes(name)) context.diagnostics.push(errorDiagnostic("slide_component_unknown", `Component '${name}' is not in the selected design inventory.`, context.nodeId))
|
|
488
868
|
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))
|
|
869
|
+
if (!String(component[key] || "").trim()) context.diagnostics.push(errorDiagnostic("slide_component_plan_incomplete", `Component '${name || "unnamed"}' is missing ${key}.`, context.nodeId))
|
|
490
870
|
}
|
|
491
|
-
if (component.
|
|
871
|
+
if (context.topLevel && component.slot && context.allowedSlots && context.allowedSlots.length > 0 && !context.allowedSlots.includes(component.slot.trim())) context.diagnostics.push(errorDiagnostic("slide_component_slot_invalid", `Component '${name || "unnamed"}' slot '${component.slot}' is not valid for this layout. Allowed slots: ${context.allowedSlots.join(", ")}.`, context.nodeId))
|
|
872
|
+
if (component.position && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(component.position)) context.diagnostics.push(errorDiagnostic("slide_component_position_invalid", `Component '${name || "unnamed"}' position must be a non-empty kebab-case anchor.`, context.nodeId))
|
|
492
873
|
const positionKey = `${component.slot?.trim() || ""}:${component.position?.trim() || ""}`
|
|
493
874
|
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)
|
|
875
|
+
if (context.positions.has(positionKey)) context.diagnostics.push(errorDiagnostic("slide_component_position_duplicate", `Duplicate component slot/position '${positionKey}' makes the plan ambiguous.`, context.nodeId))
|
|
876
|
+
context.positions.add(positionKey)
|
|
496
877
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
878
|
+
const children = component.children ?? []
|
|
879
|
+
const nesting = name ? context.options.componentNesting?.[name] : undefined
|
|
880
|
+
if (children.length > 0 && nesting && !nesting.acceptsChildren) context.diagnostics.push(errorDiagnostic("slide_component_children_invalid", `Component '${name}' does not accept children. Use box as the semantic container.`, context.nodeId))
|
|
881
|
+
if (children.length > 0 && nesting?.allowedChildren) {
|
|
882
|
+
for (const child of children) {
|
|
883
|
+
if (child.name && !nesting.allowedChildren.includes(child.name)) context.diagnostics.push(errorDiagnostic("slide_component_child_invalid", `Component '${name}' cannot contain child '${child.name}'.`, context.nodeId))
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
for (const child of children) validateComponentInput(child, { ...context, parentName: name, topLevel: false })
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function linksCount(sourceLinks: DeckPlanSourceLinks): number {
|
|
890
|
+
return sourceLinks.materials.length + sourceLinks.findings.length + sourceLinks.assets.length + sourceLinks.urls.length + sourceLinks.caveats.length
|
|
502
891
|
}
|
|
503
892
|
|
|
504
893
|
function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPlanProjectionDiagnostic {
|
|
@@ -508,22 +897,33 @@ function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPl
|
|
|
508
897
|
function parseDeckPlanComponentPlan(section: string): DeckPlanSlideComponentPlan[] {
|
|
509
898
|
const components: DeckPlanSlideComponentPlan[] = []
|
|
510
899
|
let current: DeckPlanSlideComponentPlan | undefined
|
|
900
|
+
let currentChild: DeckPlanSlideComponentPlan | undefined
|
|
511
901
|
let capture: "content" | undefined
|
|
512
902
|
const flush = () => {
|
|
903
|
+
if (currentChild && current) {
|
|
904
|
+
current.children = [...(current.children ?? []), normalizeComponentPlan(currentChild)]
|
|
905
|
+
currentChild = undefined
|
|
906
|
+
}
|
|
513
907
|
if (current) components.push({
|
|
514
|
-
...current,
|
|
515
|
-
|
|
516
|
-
claimIds: uniqueStrings(current.claimIds),
|
|
517
|
-
evidenceIds: uniqueStrings(current.evidenceIds),
|
|
518
|
-
sourceNotes: current.sourceNotes.filter(Boolean),
|
|
519
|
-
renderNotes: current.renderNotes.filter(Boolean),
|
|
908
|
+
...normalizeComponentPlan(current),
|
|
909
|
+
children: current.children,
|
|
520
910
|
})
|
|
521
911
|
}
|
|
912
|
+
const target = () => currentChild ?? current
|
|
913
|
+
const startComponent = (name: string, child: boolean) => {
|
|
914
|
+
if (child && current) {
|
|
915
|
+
if (currentChild) current.children = [...(current.children ?? []), normalizeComponentPlan(currentChild)]
|
|
916
|
+
currentChild = blankComponentPlan(name)
|
|
917
|
+
} else {
|
|
918
|
+
flush()
|
|
919
|
+
current = blankComponentPlan(name)
|
|
920
|
+
currentChild = undefined
|
|
921
|
+
}
|
|
922
|
+
}
|
|
522
923
|
for (const rawLine of section.replace(/\r\n/g, "\n").split("\n")) {
|
|
523
|
-
const heading =
|
|
924
|
+
const heading = /^(#{3,6})\s+(.+?)\s*$/.exec(rawLine)
|
|
524
925
|
if (heading) {
|
|
525
|
-
|
|
526
|
-
current = { name: heading[1].trim(), slot: "", position: "", purpose: "", content: "", claimIds: [], evidenceIds: [], sourceNotes: [], renderNotes: [] }
|
|
926
|
+
startComponent(heading[2].trim(), heading[1].length > 5)
|
|
527
927
|
capture = undefined
|
|
528
928
|
continue
|
|
529
929
|
}
|
|
@@ -534,25 +934,46 @@ function parseDeckPlanComponentPlan(section: string): DeckPlanSlideComponentPlan
|
|
|
534
934
|
capture = undefined
|
|
535
935
|
const key = field[1].toLowerCase()
|
|
536
936
|
const value = field[2].trim()
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
else if (key === "
|
|
937
|
+
const item = target()
|
|
938
|
+
if (!item) continue
|
|
939
|
+
if (key === "slot") item.slot = value
|
|
940
|
+
else if (key === "position") item.position = value
|
|
941
|
+
else if (key === "placement note") item.placementNote = value
|
|
942
|
+
else if (key === "purpose") item.purpose = value
|
|
541
943
|
else if (key === "content") {
|
|
542
|
-
|
|
944
|
+
item.content = cleanPlanValue(value)
|
|
543
945
|
capture = value ? undefined : "content"
|
|
544
|
-
} else if (key === "claim ids")
|
|
545
|
-
else if (key === "evidence ids")
|
|
546
|
-
else if (key === "source notes")
|
|
547
|
-
else if (key === "render notes")
|
|
946
|
+
} else if (key === "claim ids") item.claimIds = parseCsv(value)
|
|
947
|
+
else if (key === "evidence ids") item.evidenceIds = parseCsv(value)
|
|
948
|
+
else if (key === "source notes") item.sourceNotes = parseListValue(value)
|
|
949
|
+
else if (key === "render notes") item.renderNotes = parseListValue(value)
|
|
548
950
|
continue
|
|
549
951
|
}
|
|
550
|
-
if (capture === "content" && rawLine.trim())
|
|
952
|
+
if (capture === "content" && rawLine.trim()) {
|
|
953
|
+
const item = target()
|
|
954
|
+
if (item) item.content += `${item.content ? "\n" : ""}${rawLine.replace(/^\s{2}/, "")}`
|
|
955
|
+
}
|
|
551
956
|
}
|
|
552
957
|
flush()
|
|
553
958
|
return components
|
|
554
959
|
}
|
|
555
960
|
|
|
961
|
+
function blankComponentPlan(name: string): DeckPlanSlideComponentPlan {
|
|
962
|
+
return { name, slot: "", position: "", purpose: "", content: "", claimIds: [], evidenceIds: [], sourceNotes: [], renderNotes: [] }
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function normalizeComponentPlan(component: DeckPlanSlideComponentPlan): DeckPlanSlideComponentPlan {
|
|
966
|
+
return {
|
|
967
|
+
...component,
|
|
968
|
+
content: component.content.trim(),
|
|
969
|
+
claimIds: uniqueStrings(component.claimIds),
|
|
970
|
+
evidenceIds: uniqueStrings(component.evidenceIds),
|
|
971
|
+
sourceNotes: component.sourceNotes.filter(Boolean),
|
|
972
|
+
renderNotes: component.renderNotes.filter(Boolean),
|
|
973
|
+
children: component.children?.map(normalizeComponentPlan),
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
556
977
|
function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: string }): string {
|
|
557
978
|
const components = input.components.map((component) => component.name.trim())
|
|
558
979
|
const lines: string[] = []
|
|
@@ -595,23 +1016,7 @@ function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: str
|
|
|
595
1016
|
lines.push(`- Render notes: ${formatListValue(component.renderNotes)}`)
|
|
596
1017
|
lines.push("")
|
|
597
1018
|
}
|
|
598
|
-
lines.push("##
|
|
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("")
|
|
1019
|
+
lines.push(renderSourceLinksMarkdown(sourceLinksForInput(input)).replace(/^####/gm, "##"))
|
|
615
1020
|
lines.push("## Caveats")
|
|
616
1021
|
lines.push("")
|
|
617
1022
|
const caveats = input.caveats?.filter((item) => item.trim()) ?? []
|
|
@@ -650,8 +1055,8 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
650
1055
|
lines.push("")
|
|
651
1056
|
lines.push("## Source Authority")
|
|
652
1057
|
lines.push("")
|
|
653
|
-
lines.push("-
|
|
654
|
-
lines.push("- Render planning: `deck-plan
|
|
1058
|
+
lines.push("- Sources: local materials, reviewed findings, workspace assets, URLs, and user intent.")
|
|
1059
|
+
lines.push("- Render planning: `deck-plan.md` is the execution blueprint for HTML deck generation.")
|
|
655
1060
|
lines.push("")
|
|
656
1061
|
lines.push("## Audience / Goal / Decision")
|
|
657
1062
|
lines.push("")
|
|
@@ -669,7 +1074,7 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
669
1074
|
lines.push("")
|
|
670
1075
|
lines.push("## Slide Plan")
|
|
671
1076
|
lines.push("")
|
|
672
|
-
if (slides.length === 0) lines.push("- No slide
|
|
1077
|
+
if (slides.length === 0) lines.push("- No slide blocks yet.")
|
|
673
1078
|
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
1079
|
lines.push("")
|
|
675
1080
|
lines.push("## Evidence Trace")
|