@cyber-dash-tech/revela 0.17.24 → 0.18.3
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 +98 -19
- package/lib/decks-state.ts +7 -7
- package/lib/design/designs.ts +44 -0
- package/lib/narrative-state/deck-plan-artifact.ts +631 -114
- package/lib/narrative-state/render-plan.ts +53 -24
- 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 +16 -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 +44 -35
- package/plugins/revela/skills/revela-story/SKILL.md +0 -24
|
@@ -9,10 +9,12 @@ 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
|
|
17
|
+
export const MAX_HTML_SLIDES_PER_BATCH = 5
|
|
16
18
|
|
|
17
19
|
export interface DeckPlanArtifactInput {
|
|
18
20
|
deck: DeckSpec
|
|
@@ -65,13 +67,24 @@ export interface DeckPlanProjection {
|
|
|
65
67
|
frontmatter: Record<string, string | string[] | boolean>
|
|
66
68
|
sections: string[]
|
|
67
69
|
narrativeHash?: string
|
|
70
|
+
designName?: string
|
|
68
71
|
outputPath?: string
|
|
69
72
|
slides: DeckPlanSlideProjection[]
|
|
73
|
+
htmlWritingBatches: DeckPlanHtmlWritingBatch[]
|
|
74
|
+
htmlWritingInstruction: string
|
|
70
75
|
graphNodes: Array<{ id: string; type: WorkspaceGraphNodeType; file: string }>
|
|
71
76
|
graphRelations: VaultRelation[]
|
|
72
77
|
diagnostics: DeckPlanProjectionDiagnostic[]
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
export interface DeckPlanHtmlWritingBatch {
|
|
81
|
+
label: string
|
|
82
|
+
chapterTitle: string
|
|
83
|
+
slideIndexes: number[]
|
|
84
|
+
maxSlides: number
|
|
85
|
+
instructions: string
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export interface DeckPlanSlideProjection {
|
|
76
89
|
path: string
|
|
77
90
|
absolutePath: string
|
|
@@ -88,6 +101,8 @@ export interface DeckPlanSlideProjection {
|
|
|
88
101
|
frontmatter: Record<string, string | string[] | boolean>
|
|
89
102
|
sections: string[]
|
|
90
103
|
links: DeckPlanNarrativeLink[]
|
|
104
|
+
sourceLinks: DeckPlanSourceLinks
|
|
105
|
+
caveats: string[]
|
|
91
106
|
}
|
|
92
107
|
|
|
93
108
|
export interface DeckPlanSlideComponentPlan {
|
|
@@ -101,6 +116,7 @@ export interface DeckPlanSlideComponentPlan {
|
|
|
101
116
|
sourceNotes: string[]
|
|
102
117
|
renderNotes: string[]
|
|
103
118
|
placementNote?: string
|
|
119
|
+
children?: DeckPlanSlideComponentPlan[]
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
export interface DeckPlanSlideUpsertComponentInput {
|
|
@@ -114,9 +130,20 @@ export interface DeckPlanSlideUpsertComponentInput {
|
|
|
114
130
|
sourceNotes?: string[]
|
|
115
131
|
renderNotes?: string[]
|
|
116
132
|
placementNote?: string
|
|
133
|
+
children?: DeckPlanSlideUpsertComponentInput[]
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface DeckPlanSourceLinks {
|
|
137
|
+
materials: string[]
|
|
138
|
+
findings: string[]
|
|
139
|
+
assets: string[]
|
|
140
|
+
urls: string[]
|
|
141
|
+
caveats: string[]
|
|
117
142
|
}
|
|
118
143
|
|
|
119
144
|
export interface DeckPlanSlideUpsertInput {
|
|
145
|
+
designName?: string
|
|
146
|
+
outputPath?: string
|
|
120
147
|
slideIndex: number
|
|
121
148
|
id?: string
|
|
122
149
|
title: string
|
|
@@ -131,7 +158,8 @@ export interface DeckPlanSlideUpsertInput {
|
|
|
131
158
|
rationale?: string
|
|
132
159
|
brief?: string
|
|
133
160
|
} | string
|
|
134
|
-
|
|
161
|
+
sourceLinks?: Partial<DeckPlanSourceLinks>
|
|
162
|
+
narrativeLinks?: {
|
|
135
163
|
claimIds?: string[]
|
|
136
164
|
evidenceIds?: string[]
|
|
137
165
|
riskIds?: string[]
|
|
@@ -165,15 +193,14 @@ export interface DeckPlanProjectionDiagnostic {
|
|
|
165
193
|
}
|
|
166
194
|
|
|
167
195
|
export const REQUIRED_DECK_PLAN_SECTIONS = [
|
|
196
|
+
"Goal",
|
|
197
|
+
"Audience",
|
|
198
|
+
"Design",
|
|
168
199
|
"Source Authority",
|
|
169
|
-
"Audience / Goal / Decision",
|
|
170
|
-
"Deck Parameters",
|
|
171
200
|
"Chapter Map",
|
|
172
|
-
"
|
|
173
|
-
"
|
|
174
|
-
"
|
|
175
|
-
"Chapter Writing Batches",
|
|
176
|
-
"HTML Identity Contract",
|
|
201
|
+
"Slides",
|
|
202
|
+
"Unresolved Inputs",
|
|
203
|
+
"HTML Contract",
|
|
177
204
|
]
|
|
178
205
|
|
|
179
206
|
export function writeDeckPlanArtifact(workspaceRoot: string, input: DeckPlanArtifactInput): { path: string; absolutePath: string } {
|
|
@@ -185,17 +212,17 @@ export function writeDeckPlanArtifact(workspaceRoot: string, input: DeckPlanArti
|
|
|
185
212
|
|
|
186
213
|
export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrativeHash?: string; knownNodeIds?: Set<string> }): DeckPlanReadResult {
|
|
187
214
|
const projection = readDeckPlanProjection(workspaceRoot, expected)
|
|
188
|
-
const absolutePath = projection?.absolutePath ?? join(workspaceRoot,
|
|
215
|
+
const absolutePath = projection?.absolutePath ?? join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
189
216
|
if (!existsSync(absolutePath)) {
|
|
190
217
|
return {
|
|
191
218
|
ok: false,
|
|
192
|
-
path:
|
|
219
|
+
path: DECK_PLAN_MARKDOWN_PATH,
|
|
193
220
|
absolutePath,
|
|
194
221
|
approvalStatus: "missing",
|
|
195
222
|
sections: [],
|
|
196
223
|
missingSections: REQUIRED_DECK_PLAN_SECTIONS,
|
|
197
224
|
warnings: [],
|
|
198
|
-
reason: `Deck plan file is missing: ${
|
|
225
|
+
reason: `Deck plan file is missing: ${DECK_PLAN_MARKDOWN_PATH}. Write the LLM-authored deck plan before HTML generation.`,
|
|
199
226
|
}
|
|
200
227
|
}
|
|
201
228
|
const markdown = projection?.markdown ?? readFileSync(absolutePath, "utf-8")
|
|
@@ -221,7 +248,7 @@ export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrati
|
|
|
221
248
|
}
|
|
222
249
|
return {
|
|
223
250
|
ok: true,
|
|
224
|
-
path: projection?.path ??
|
|
251
|
+
path: projection?.path ?? DECK_PLAN_MARKDOWN_PATH,
|
|
225
252
|
absolutePath,
|
|
226
253
|
markdown,
|
|
227
254
|
planHash,
|
|
@@ -236,9 +263,10 @@ export function readDeckPlanArtifact(workspaceRoot: string, expected?: { narrati
|
|
|
236
263
|
|
|
237
264
|
export function readDeckPlanProjection(workspaceRoot: string, expected?: { narrativeHash?: string; knownNodeIds?: Set<string> }): DeckPlanProjection | undefined {
|
|
238
265
|
const root = join(workspaceRoot, DECK_PLAN_DIR)
|
|
266
|
+
const singlePath = join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
239
267
|
const indexPath = join(workspaceRoot, DECK_PLAN_INDEX_PATH)
|
|
240
268
|
const legacyPath = join(workspaceRoot, LEGACY_DECK_PLAN_ARTIFACT_PATH)
|
|
241
|
-
const absolutePath = existsSync(indexPath) ? indexPath : existsSync(legacyPath) ? legacyPath : ""
|
|
269
|
+
const absolutePath = existsSync(singlePath) ? singlePath : existsSync(indexPath) ? indexPath : existsSync(legacyPath) ? legacyPath : ""
|
|
242
270
|
if (!absolutePath) return undefined
|
|
243
271
|
const markdown = readFileSync(absolutePath, "utf-8")
|
|
244
272
|
const parsed = parseVaultFrontmatter(markdown)
|
|
@@ -246,7 +274,8 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
246
274
|
const sections = parseMarkdownSections(markdown)
|
|
247
275
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
248
276
|
const id = stringField(parsed.frontmatter, "id") || "deck-plan"
|
|
249
|
-
const
|
|
277
|
+
const isSingleFile = relativePath(workspaceRoot, absolutePath) === DECK_PLAN_MARKDOWN_PATH
|
|
278
|
+
const slides = isSingleFile ? readDeckPlanSlidesFromSingleFile(workspaceRoot, absolutePath, markdown, expected?.knownNodeIds) : existsSync(join(root, "slides")) ? readDeckPlanSlideFiles(workspaceRoot, expected?.knownNodeIds) : []
|
|
250
279
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
251
280
|
const narrativeHash = stringField(parsed.frontmatter, "narrativeHash") || narrativeHashFromMarkdown(markdown)
|
|
252
281
|
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 })
|
|
@@ -265,6 +294,7 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
265
294
|
file: slide.path,
|
|
266
295
|
source: "inline" as const,
|
|
267
296
|
})))
|
|
297
|
+
const htmlWritingBatches = buildHtmlWritingBatches(slides)
|
|
268
298
|
return {
|
|
269
299
|
path,
|
|
270
300
|
absolutePath,
|
|
@@ -273,8 +303,11 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
273
303
|
frontmatter: parsed.frontmatter,
|
|
274
304
|
sections,
|
|
275
305
|
narrativeHash,
|
|
276
|
-
|
|
306
|
+
designName: stringField(parsed.frontmatter, "designName") || stringField(parsed.frontmatter, "design"),
|
|
307
|
+
outputPath: stringField(parsed.frontmatter, "outputPath") || stringField(parsed.frontmatter, "output"),
|
|
277
308
|
slides,
|
|
309
|
+
htmlWritingBatches,
|
|
310
|
+
htmlWritingInstruction: htmlWritingInstruction(),
|
|
278
311
|
graphNodes,
|
|
279
312
|
graphRelations,
|
|
280
313
|
diagnostics,
|
|
@@ -311,36 +344,212 @@ export function deckPlanBodyHash(markdown: string): string {
|
|
|
311
344
|
export function upsertDeckPlanSlideArtifact(
|
|
312
345
|
workspaceRoot: string,
|
|
313
346
|
input: DeckPlanSlideUpsertInput,
|
|
314
|
-
options: { narrativeHash?: string; knownNodeIds?: Set<string>; designLayouts: string[]; designComponents: string[] },
|
|
347
|
+
options: { narrativeHash?: string; knownNodeIds?: Set<string>; designLayouts: string[]; designComponents: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> },
|
|
315
348
|
): DeckPlanSlideUpsertResult {
|
|
316
349
|
const diagnostics = validateDeckPlanSlideUpsert(input, options)
|
|
317
350
|
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) return { ok: false, diagnostics }
|
|
318
351
|
|
|
319
|
-
mkdirSync(join(workspaceRoot, DECK_PLAN_SLIDES_DIR), { recursive: true })
|
|
320
|
-
ensureDeckPlanIndex(workspaceRoot, options.narrativeHash)
|
|
321
|
-
|
|
322
352
|
const existing = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
323
353
|
const existingSlide = existing?.slides.find((slide) => slide.slideIndex === input.slideIndex)
|
|
324
354
|
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)
|
|
355
|
+
const nextSlide = projectionFromSlideInput(workspaceRoot, { ...input, id }, existingSlide)
|
|
356
|
+
const slides = [...(existing?.slides.filter((slide) => slide.slideIndex !== input.slideIndex) ?? []), nextSlide]
|
|
357
|
+
.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
358
|
+
const designName = input.designName || existing?.designName
|
|
359
|
+
const outputPath = input.outputPath || existing?.outputPath
|
|
360
|
+
writeDeckPlanSingleFile(workspaceRoot, {
|
|
361
|
+
title: "Deck Plan",
|
|
362
|
+
designName,
|
|
363
|
+
outputPath,
|
|
364
|
+
slides,
|
|
365
|
+
})
|
|
341
366
|
const projection = readDeckPlanProjection(workspaceRoot, { narrativeHash: options.narrativeHash, knownNodeIds: options.knownNodeIds })
|
|
342
367
|
const slide = projection?.slides.find((item) => item.slideIndex === input.slideIndex)
|
|
343
|
-
return { ok: true, path, absolutePath, updated: Boolean(existingSlide), slide, diagnostics: [...diagnostics, ...(projection?.diagnostics ?? [])] }
|
|
368
|
+
return { ok: true, path: DECK_PLAN_MARKDOWN_PATH, absolutePath: join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH), updated: Boolean(existingSlide), slide, diagnostics: [...diagnostics, ...(projection?.diagnostics ?? [])] }
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function projectionFromSlideInput(workspaceRoot: string, input: DeckPlanSlideUpsertInput & { id: string }, existing?: DeckPlanSlideProjection): DeckPlanSlideProjection {
|
|
372
|
+
const sourceLinks = sourceLinksForInput(input)
|
|
373
|
+
const caveats = uniqueStrings([...(input.caveats ?? []), ...sourceLinks.caveats])
|
|
374
|
+
const componentPlan = input.components.map(componentInputToPlan)
|
|
375
|
+
const slide: DeckPlanSlideProjection = {
|
|
376
|
+
path: DECK_PLAN_MARKDOWN_PATH,
|
|
377
|
+
absolutePath: join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH),
|
|
378
|
+
id: input.id,
|
|
379
|
+
slideIndex: input.slideIndex,
|
|
380
|
+
title: input.title,
|
|
381
|
+
chapter: input.chapter,
|
|
382
|
+
layout: input.layout,
|
|
383
|
+
components: uniqueStrings(componentPlan.flatMap(flattenComponentNames)),
|
|
384
|
+
componentPlan,
|
|
385
|
+
structural: input.structural ?? false,
|
|
386
|
+
narrativeRole: input.narrativeRole,
|
|
387
|
+
markdown: "",
|
|
388
|
+
frontmatter: existing?.frontmatter ?? {},
|
|
389
|
+
sections: [],
|
|
390
|
+
links: sourceLinksToNarrativeLinks(sourceLinks, input.narrativeLinks ? sourceLinksToNarrativeLinks(sourceLinksFromNarrativeLinks(input.narrativeLinks)) : []),
|
|
391
|
+
sourceLinks,
|
|
392
|
+
caveats,
|
|
393
|
+
}
|
|
394
|
+
slide.markdown = renderDeckPlanSlideBlock(slide, input.visualIntent)
|
|
395
|
+
return slide
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function componentInputToPlan(component: DeckPlanSlideUpsertComponentInput): DeckPlanSlideComponentPlan {
|
|
399
|
+
return normalizeComponentPlan({
|
|
400
|
+
name: component.name?.trim() || "",
|
|
401
|
+
slot: component.slot?.trim() || "",
|
|
402
|
+
position: component.position?.trim() || "",
|
|
403
|
+
purpose: component.purpose?.trim() || "",
|
|
404
|
+
content: component.content?.trim() || "",
|
|
405
|
+
claimIds: uniqueStrings(component.claimIds ?? []),
|
|
406
|
+
evidenceIds: uniqueStrings(component.evidenceIds ?? []),
|
|
407
|
+
sourceNotes: (component.sourceNotes ?? []).map((item) => item.trim()).filter(Boolean),
|
|
408
|
+
renderNotes: (component.renderNotes ?? []).map((item) => item.trim()).filter(Boolean),
|
|
409
|
+
placementNote: component.placementNote?.trim(),
|
|
410
|
+
children: component.children?.map(componentInputToPlan),
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function flattenComponentNames(component: DeckPlanSlideComponentPlan): string[] {
|
|
415
|
+
return [component.name, ...(component.children ?? []).flatMap(flattenComponentNames)].filter(Boolean)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function writeDeckPlanSingleFile(workspaceRoot: string, input: {
|
|
419
|
+
title: string
|
|
420
|
+
goal?: string
|
|
421
|
+
audience?: string
|
|
422
|
+
designName?: string
|
|
423
|
+
outputPath?: string
|
|
424
|
+
sourceAuthority?: string[]
|
|
425
|
+
unresolvedInputs?: string[]
|
|
426
|
+
slides: DeckPlanSlideProjection[]
|
|
427
|
+
}): { path: string; absolutePath: string } {
|
|
428
|
+
const absolutePath = join(workspaceRoot, DECK_PLAN_MARKDOWN_PATH)
|
|
429
|
+
const lines: string[] = []
|
|
430
|
+
lines.push("---")
|
|
431
|
+
lines.push("type: deck-plan")
|
|
432
|
+
lines.push("version: 0.18.1")
|
|
433
|
+
if (input.designName) lines.push(`designName: ${yamlScalar(input.designName)}`)
|
|
434
|
+
if (input.outputPath) lines.push(`outputPath: ${yamlScalar(input.outputPath)}`)
|
|
435
|
+
lines.push("---")
|
|
436
|
+
lines.push("")
|
|
437
|
+
lines.push(`# ${input.title || "Deck Plan"}`)
|
|
438
|
+
lines.push("")
|
|
439
|
+
lines.push("## Goal")
|
|
440
|
+
lines.push("")
|
|
441
|
+
lines.push(input.goal || "To be specified from user intent and source materials.")
|
|
442
|
+
lines.push("")
|
|
443
|
+
lines.push("## Audience")
|
|
444
|
+
lines.push("")
|
|
445
|
+
lines.push(input.audience || "To be specified.")
|
|
446
|
+
lines.push("")
|
|
447
|
+
lines.push("## Design")
|
|
448
|
+
lines.push("")
|
|
449
|
+
lines.push(`- Design: ${input.designName || "active design"}`)
|
|
450
|
+
if (input.outputPath) lines.push(`- Output path: ${input.outputPath}`)
|
|
451
|
+
lines.push("")
|
|
452
|
+
lines.push("## Source Authority")
|
|
453
|
+
lines.push("")
|
|
454
|
+
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."]
|
|
455
|
+
for (const item of sourceAuthority) lines.push(`- ${item}`)
|
|
456
|
+
lines.push("")
|
|
457
|
+
lines.push("## Chapter Map")
|
|
458
|
+
lines.push("")
|
|
459
|
+
const chapterMap = new Map<string, number[]>()
|
|
460
|
+
for (const slide of input.slides) chapterMap.set(slide.chapter || "Unassigned", [...(chapterMap.get(slide.chapter || "Unassigned") ?? []), slide.slideIndex ?? 0].filter(Boolean))
|
|
461
|
+
for (const [chapter, indexes] of chapterMap) lines.push(`- ${chapter}: slides ${formatSlideRange(indexes)}`)
|
|
462
|
+
if (chapterMap.size === 0) lines.push("- No slides planned yet.")
|
|
463
|
+
lines.push("")
|
|
464
|
+
lines.push("## Slides")
|
|
465
|
+
lines.push("")
|
|
466
|
+
for (const slide of input.slides) {
|
|
467
|
+
lines.push(renderDeckPlanSlideBlock(slide))
|
|
468
|
+
lines.push("")
|
|
469
|
+
}
|
|
470
|
+
lines.push("## Unresolved Inputs")
|
|
471
|
+
lines.push("")
|
|
472
|
+
const unresolved = input.unresolvedInputs?.filter(Boolean) ?? []
|
|
473
|
+
if (unresolved.length === 0) lines.push("- None.")
|
|
474
|
+
else for (const item of unresolved) lines.push(`- ${item}`)
|
|
475
|
+
lines.push("")
|
|
476
|
+
lines.push("## HTML Contract")
|
|
477
|
+
lines.push("")
|
|
478
|
+
lines.push("- Render one `<section class=\"slide\" data-slide-index=\"N\">` per planned slide.")
|
|
479
|
+
lines.push("- Use positive 1-based slide indexes, unique indexes, DOM order, and one direct `.slide-canvas` child per slide.")
|
|
480
|
+
lines.push("")
|
|
481
|
+
writeFileSync(absolutePath, lines.join("\n"), "utf-8")
|
|
482
|
+
return { path: DECK_PLAN_MARKDOWN_PATH, absolutePath }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function renderDeckPlanSlideBlock(slide: DeckPlanSlideProjection, visualIntent?: DeckPlanSlideUpsertInput["visualIntent"]): string {
|
|
486
|
+
const lines: string[] = []
|
|
487
|
+
lines.push("---")
|
|
488
|
+
lines.push(`slideIndex: ${slide.slideIndex ?? ""}`)
|
|
489
|
+
lines.push(`id: ${slide.id}`)
|
|
490
|
+
lines.push(`title: ${yamlScalar(slide.title)}`)
|
|
491
|
+
lines.push(`chapter: ${yamlScalar(slide.chapter || "Unassigned")}`)
|
|
492
|
+
lines.push(`role: ${yamlScalar(slide.narrativeRole || "Not specified")}`)
|
|
493
|
+
lines.push(`structural: ${slide.structural ? "true" : "false"}`)
|
|
494
|
+
lines.push(`layout: ${slide.layout || "unspecified"}`)
|
|
495
|
+
lines.push(`components: ${slide.components.join(", ") || "none"}`)
|
|
496
|
+
lines.push("---")
|
|
497
|
+
lines.push("")
|
|
498
|
+
lines.push("#### Content Plan")
|
|
499
|
+
lines.push("")
|
|
500
|
+
lines.push(`- Message: ${slide.narrativeRole || "Not specified."}`)
|
|
501
|
+
lines.push(`- Role: ${slide.narrativeRole || "Not specified"}`)
|
|
502
|
+
lines.push("- Speaker notes: Not specified.")
|
|
503
|
+
lines.push("")
|
|
504
|
+
lines.push("#### Source Links")
|
|
505
|
+
lines.push("")
|
|
506
|
+
lines.push(renderSourceLinksMarkdown(slide.sourceLinks))
|
|
507
|
+
lines.push("")
|
|
508
|
+
lines.push("#### Design Plan")
|
|
509
|
+
lines.push("")
|
|
510
|
+
lines.push(`- Layout: ${slide.layout || "unspecified"}`)
|
|
511
|
+
lines.push(`- Components: ${slide.components.join(", ") || "none"}`)
|
|
512
|
+
lines.push("- Visual intent:")
|
|
513
|
+
lines.push(...indentMultiline(visualIntent ? renderVisualIntent(visualIntent) : "- Brief: Not specified."))
|
|
514
|
+
lines.push("")
|
|
515
|
+
for (const component of slide.componentPlan) lines.push(renderComponentPlanMarkdown(component, 5))
|
|
516
|
+
lines.push("")
|
|
517
|
+
return lines.join("\n")
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function renderComponentPlanMarkdown(component: DeckPlanSlideComponentPlan, headingLevel: number): string {
|
|
521
|
+
const lines: string[] = []
|
|
522
|
+
lines.push(`${"#".repeat(headingLevel)} ${component.name}`)
|
|
523
|
+
lines.push("")
|
|
524
|
+
lines.push(`- Slot: ${component.slot}`)
|
|
525
|
+
lines.push(`- Position: ${component.position}`)
|
|
526
|
+
if (component.placementNote) lines.push(`- Placement note: ${component.placementNote}`)
|
|
527
|
+
lines.push(`- Purpose: ${component.purpose}`)
|
|
528
|
+
lines.push("- Content:")
|
|
529
|
+
lines.push(...indentMultiline(component.content))
|
|
530
|
+
lines.push(`- Claim ids: ${formatCsv(component.claimIds)}`)
|
|
531
|
+
lines.push(`- Evidence ids: ${formatCsv(component.evidenceIds)}`)
|
|
532
|
+
lines.push(`- Source notes: ${formatListValue(component.sourceNotes)}`)
|
|
533
|
+
lines.push(`- Render notes: ${formatListValue(component.renderNotes)}`)
|
|
534
|
+
lines.push("")
|
|
535
|
+
for (const child of component.children ?? []) lines.push(renderComponentPlanMarkdown(child, headingLevel + 1))
|
|
536
|
+
return lines.join("\n")
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function renderSourceLinksMarkdown(sourceLinks: DeckPlanSourceLinks): string {
|
|
540
|
+
const lines: string[] = []
|
|
541
|
+
for (const [label, values] of [
|
|
542
|
+
["Materials", sourceLinks.materials],
|
|
543
|
+
["Findings", sourceLinks.findings],
|
|
544
|
+
["Assets", sourceLinks.assets],
|
|
545
|
+
["URLs", sourceLinks.urls],
|
|
546
|
+
] as const) {
|
|
547
|
+
lines.push(`${label}:`)
|
|
548
|
+
if (values.length === 0) lines.push("- None.")
|
|
549
|
+
else for (const value of values) lines.push(value.includes("/") && !/^https?:\/\//i.test(value) ? `- [[${value}]]` : `- ${value}`)
|
|
550
|
+
lines.push("")
|
|
551
|
+
}
|
|
552
|
+
return lines.join("\n").trim()
|
|
344
553
|
}
|
|
345
554
|
|
|
346
555
|
function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
@@ -356,7 +565,9 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
356
565
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
357
566
|
const id = stringField(parsed.frontmatter, "id") || fileId(entry)
|
|
358
567
|
const componentPlan = parseDeckPlanComponentPlan(split.sections["component-plan"] ?? "")
|
|
359
|
-
const
|
|
568
|
+
const sourceLinks = parseDeckPlanSourceLinks(split.sections["source-links"] ?? "")
|
|
569
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, parseDeckPlanNarrativeLinks(split.sections["narrative-links"] ?? parsed.body, knownNodeIds))
|
|
570
|
+
const caveats = parseBulletText(split.sections["caveats"] ?? "")
|
|
360
571
|
slides.push({
|
|
361
572
|
path,
|
|
362
573
|
absolutePath,
|
|
@@ -373,11 +584,134 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
373
584
|
frontmatter: parsed.frontmatter,
|
|
374
585
|
sections: parseMarkdownSections(markdown),
|
|
375
586
|
links,
|
|
587
|
+
sourceLinks,
|
|
588
|
+
caveats,
|
|
376
589
|
})
|
|
377
590
|
}
|
|
378
591
|
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER) || a.path.localeCompare(b.path))
|
|
379
592
|
}
|
|
380
593
|
|
|
594
|
+
function readDeckPlanSlidesFromSingleFile(workspaceRoot: string, absolutePath: string, markdown: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
595
|
+
const slideBlocks = readDeckPlanSeparatorSlidesFromSingleFile(workspaceRoot, absolutePath, markdown, knownNodeIds)
|
|
596
|
+
if (slideBlocks.length > 0) return slideBlocks
|
|
597
|
+
const path = relativePath(workspaceRoot, absolutePath)
|
|
598
|
+
const body = parseVaultFrontmatter(markdown).body
|
|
599
|
+
const matches = [...body.matchAll(/^[ \t]*###\s+Slide\s+(\d+)\s+(?:—|-)\s+(.+?)\s*$/gm)]
|
|
600
|
+
const slides: DeckPlanSlideProjection[] = []
|
|
601
|
+
for (let i = 0; i < matches.length; i++) {
|
|
602
|
+
const match = matches[i]
|
|
603
|
+
const start = match.index ?? 0
|
|
604
|
+
const nextSlide = i + 1 < matches.length ? matches[i + 1].index ?? body.length : body.length
|
|
605
|
+
const headingEnd = body.indexOf("\n", start)
|
|
606
|
+
const searchStart = headingEnd === -1 ? start + match[0].length : headingEnd + 1
|
|
607
|
+
const nextSection = body.slice(searchStart).search(/^[ \t]*##\s+(?!#)/m)
|
|
608
|
+
const end = Math.min(nextSlide, nextSection === -1 ? body.length : searchStart + nextSection)
|
|
609
|
+
const block = body.slice(start, end).trim()
|
|
610
|
+
const slideIndex = Number(match[1])
|
|
611
|
+
const title = match[2].trim()
|
|
612
|
+
const fields = parseSlideBlockFields(block)
|
|
613
|
+
const id = fields.id || `slide-${slugify(title)}`
|
|
614
|
+
const sourceLinks = normalizeSourceLinks(parseDeckPlanSourceLinks(singleFileSubsection(block, "Source Links")))
|
|
615
|
+
const narrativeLinks = parseDeckPlanNarrativeLinks(singleFileSubsection(block, "Narrative Links") || block, knownNodeIds)
|
|
616
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, narrativeLinks)
|
|
617
|
+
const caveats = uniqueStrings([...parseBulletText(singleFileSubsection(block, "Caveats")), ...sourceLinks.caveats])
|
|
618
|
+
const componentPlan = parseDeckPlanComponentPlan(singleFileSubsection(block, "Component Plan"))
|
|
619
|
+
slides.push({
|
|
620
|
+
path,
|
|
621
|
+
absolutePath,
|
|
622
|
+
id,
|
|
623
|
+
slideIndex,
|
|
624
|
+
title,
|
|
625
|
+
chapter: fields.chapter || "",
|
|
626
|
+
layout: fields.layout || "",
|
|
627
|
+
components: parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
628
|
+
componentPlan,
|
|
629
|
+
structural: fields.structural === "true" || fields.structural === "yes",
|
|
630
|
+
narrativeRole: fields.role || fields.narrativeRole || "",
|
|
631
|
+
markdown: block,
|
|
632
|
+
frontmatter: {},
|
|
633
|
+
sections: parseMarkdownSections(block),
|
|
634
|
+
links,
|
|
635
|
+
sourceLinks,
|
|
636
|
+
caveats,
|
|
637
|
+
})
|
|
638
|
+
}
|
|
639
|
+
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function readDeckPlanSeparatorSlidesFromSingleFile(workspaceRoot: string, absolutePath: string, markdown: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
643
|
+
const path = relativePath(workspaceRoot, absolutePath)
|
|
644
|
+
const body = parseVaultFrontmatter(markdown).body
|
|
645
|
+
const slidesHeading = /^[ \t]*##\s+Slides\s*$/mi.exec(body)
|
|
646
|
+
if (!slidesHeading || slidesHeading.index === undefined) return []
|
|
647
|
+
const headingEnd = body.indexOf("\n", slidesHeading.index)
|
|
648
|
+
const slidesStart = headingEnd === -1 ? slidesHeading.index + slidesHeading[0].length : headingEnd + 1
|
|
649
|
+
const rest = body.slice(slidesStart)
|
|
650
|
+
const nextSection = rest.search(/^[ \t]*##\s+(?!#)/m)
|
|
651
|
+
const slidesRegion = nextSection === -1 ? rest : rest.slice(0, nextSection)
|
|
652
|
+
const metadataMatches = [...slidesRegion.matchAll(/^[ \t]*---[ \t]*\n[\s\S]*?\n[ \t]*---[ \t]*(?:\n|$)/gm)]
|
|
653
|
+
const slides: DeckPlanSlideProjection[] = []
|
|
654
|
+
for (let i = 0; i < metadataMatches.length; i++) {
|
|
655
|
+
const match = metadataMatches[i]
|
|
656
|
+
const start = match.index ?? 0
|
|
657
|
+
const next = i + 1 < metadataMatches.length ? metadataMatches[i + 1].index ?? slidesRegion.length : slidesRegion.length
|
|
658
|
+
const block = slidesRegion.slice(start, next).trim()
|
|
659
|
+
const parsed = parseVaultFrontmatter(block)
|
|
660
|
+
const slideIndex = numberField(parsed.frontmatter, "slideIndex")
|
|
661
|
+
const title = stringField(parsed.frontmatter, "title") || firstHeading(parsed.body) || `Slide ${slideIndex ?? i + 1}`
|
|
662
|
+
const fields = parseSlideBlockFields(parsed.body)
|
|
663
|
+
const sourceLinks = normalizeSourceLinks(parseDeckPlanSourceLinks(singleFileSubsection(block, "Source Links")))
|
|
664
|
+
const narrativeLinks = parseDeckPlanNarrativeLinks(singleFileSubsection(block, "Narrative Links") || block, knownNodeIds)
|
|
665
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, narrativeLinks)
|
|
666
|
+
const componentPlan = parseDeckPlanComponentPlan(singleFileSubsection(block, "Component Plan") || singleFileSubsection(block, "Design Plan"))
|
|
667
|
+
slides.push({
|
|
668
|
+
path,
|
|
669
|
+
absolutePath,
|
|
670
|
+
id: stringField(parsed.frontmatter, "id") || fields.id || `slide-${slugify(title)}`,
|
|
671
|
+
slideIndex,
|
|
672
|
+
title,
|
|
673
|
+
chapter: stringField(parsed.frontmatter, "chapter") || fields.chapter || "",
|
|
674
|
+
layout: stringField(parsed.frontmatter, "layout") || fields.layout || "",
|
|
675
|
+
components: arrayField(parsed.frontmatter, "components").length > 0 ? arrayField(parsed.frontmatter, "components") : parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
676
|
+
componentPlan,
|
|
677
|
+
structural: booleanField(parsed.frontmatter, "structural", fields.structural === "true" || fields.structural === "yes"),
|
|
678
|
+
narrativeRole: stringField(parsed.frontmatter, "role") || stringField(parsed.frontmatter, "narrativeRole") || fields.role || fields.narrativeRole || "",
|
|
679
|
+
markdown: block,
|
|
680
|
+
frontmatter: parsed.frontmatter,
|
|
681
|
+
sections: parseMarkdownSections(block),
|
|
682
|
+
links,
|
|
683
|
+
sourceLinks,
|
|
684
|
+
caveats: uniqueStrings([...parseBulletText(singleFileSubsection(block, "Caveats")), ...sourceLinks.caveats]),
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function firstHeading(markdown: string): string {
|
|
691
|
+
return /^#{1,6}\s+(.+?)\s*$/m.exec(markdown)?.[1]?.trim() ?? ""
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function parseSlideBlockFields(block: string): Record<string, string> {
|
|
695
|
+
const fields: Record<string, string> = {}
|
|
696
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
697
|
+
const match = /^-\s+([A-Za-z][A-Za-z ]+):\s*(.*)$/.exec(rawLine.trim())
|
|
698
|
+
if (!match) continue
|
|
699
|
+
const key = match[1].trim().replace(/\s+/g, "")
|
|
700
|
+
fields[key[0].toLowerCase() + key.slice(1)] = cleanPlanValue(match[2])
|
|
701
|
+
}
|
|
702
|
+
return fields
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function singleFileSubsection(block: string, heading: string): string {
|
|
706
|
+
const re = new RegExp(`^[ \\t]*####\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "mi")
|
|
707
|
+
const match = re.exec(block)
|
|
708
|
+
if (!match || match.index === undefined) return ""
|
|
709
|
+
const start = match.index + match[0].length
|
|
710
|
+
const rest = block.slice(start)
|
|
711
|
+
const next = rest.search(/^[ \t]*####\s+/m)
|
|
712
|
+
return (next === -1 ? rest : rest.slice(0, next)).trim()
|
|
713
|
+
}
|
|
714
|
+
|
|
381
715
|
function parseDeckPlanNarrativeLinks(section: string, knownNodeIds?: Set<string>): DeckPlanNarrativeLink[] {
|
|
382
716
|
const links: DeckPlanNarrativeLink[] = []
|
|
383
717
|
let group = ""
|
|
@@ -396,8 +730,97 @@ function parseDeckPlanNarrativeLinks(section: string, knownNodeIds?: Set<string>
|
|
|
396
730
|
return uniqueLinks(links)
|
|
397
731
|
}
|
|
398
732
|
|
|
733
|
+
function emptySourceLinks(): DeckPlanSourceLinks {
|
|
734
|
+
return { materials: [], findings: [], assets: [], urls: [], caveats: [] }
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function normalizeSourceLinks(input?: Partial<DeckPlanSourceLinks>): DeckPlanSourceLinks {
|
|
738
|
+
return {
|
|
739
|
+
materials: uniqueStrings(input?.materials ?? []),
|
|
740
|
+
findings: uniqueStrings(input?.findings ?? []),
|
|
741
|
+
assets: uniqueStrings(input?.assets ?? []),
|
|
742
|
+
urls: uniqueStrings(input?.urls ?? []),
|
|
743
|
+
caveats: uniqueStrings(input?.caveats ?? []),
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function sourceLinksFromNarrativeLinks(input?: DeckPlanSlideUpsertInput["narrativeLinks"]): DeckPlanSourceLinks {
|
|
748
|
+
const links = emptySourceLinks()
|
|
749
|
+
for (const id of input?.evidenceIds ?? []) {
|
|
750
|
+
if (/^https?:\/\//i.test(id)) links.urls.push(id)
|
|
751
|
+
else if (id.startsWith("assets/")) links.assets.push(id)
|
|
752
|
+
else if (id.startsWith("researches/")) links.findings.push(id)
|
|
753
|
+
else if (id.startsWith("materials/") || id.startsWith("sources/")) links.materials.push(id)
|
|
754
|
+
else links.findings.push(id)
|
|
755
|
+
}
|
|
756
|
+
for (const id of input?.claimIds ?? []) {
|
|
757
|
+
if (id.startsWith("researches/")) links.findings.push(id)
|
|
758
|
+
else if (id.startsWith("assets/")) links.assets.push(id)
|
|
759
|
+
else links.materials.push(id)
|
|
760
|
+
}
|
|
761
|
+
links.caveats.push(...(input?.riskIds ?? []), ...(input?.objectionIds ?? []), ...(input?.gapIds ?? []))
|
|
762
|
+
return normalizeSourceLinks(links)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function sourceLinksForInput(input: DeckPlanSlideUpsertInput): DeckPlanSourceLinks {
|
|
766
|
+
return normalizeSourceLinks({
|
|
767
|
+
...sourceLinksFromNarrativeLinks(input.narrativeLinks),
|
|
768
|
+
...(input.sourceLinks ?? {}),
|
|
769
|
+
caveats: [...(sourceLinksFromNarrativeLinks(input.narrativeLinks).caveats ?? []), ...(input.sourceLinks?.caveats ?? []), ...(input.caveats ?? [])],
|
|
770
|
+
})
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function parseDeckPlanSourceLinks(section: string): DeckPlanSourceLinks {
|
|
774
|
+
const links = emptySourceLinks()
|
|
775
|
+
let group: keyof DeckPlanSourceLinks | undefined
|
|
776
|
+
for (const rawLine of section.replace(/\r\n/g, "\n").split("\n")) {
|
|
777
|
+
const heading = /^\s*([A-Za-z][A-Za-z\s/-]*):\s*$/.exec(rawLine)
|
|
778
|
+
if (heading) {
|
|
779
|
+
const normalized = heading[1].trim().toLowerCase()
|
|
780
|
+
if (normalized.includes("material")) group = "materials"
|
|
781
|
+
else if (normalized.includes("finding") || normalized.includes("research")) group = "findings"
|
|
782
|
+
else if (normalized.includes("asset") || normalized.includes("media")) group = "assets"
|
|
783
|
+
else if (normalized.includes("url") || normalized.includes("link")) group = "urls"
|
|
784
|
+
else if (normalized.includes("caveat") || normalized.includes("risk") || normalized.includes("gap")) group = "caveats"
|
|
785
|
+
else group = undefined
|
|
786
|
+
continue
|
|
787
|
+
}
|
|
788
|
+
const bullet = rawLine.replace(/^\s*[-*]\s+/, "").trim()
|
|
789
|
+
if (!bullet || bullet.toLowerCase() === "none.") continue
|
|
790
|
+
const wikilink = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/.exec(bullet)?.[1]?.trim()
|
|
791
|
+
const value = wikilink || bullet
|
|
792
|
+
if (group) links[group].push(value)
|
|
793
|
+
else if (/^https?:\/\//i.test(value)) links.urls.push(value)
|
|
794
|
+
else if (value.startsWith("assets/")) links.assets.push(value)
|
|
795
|
+
else if (value.startsWith("researches/")) links.findings.push(value)
|
|
796
|
+
else links.materials.push(value)
|
|
797
|
+
}
|
|
798
|
+
return normalizeSourceLinks(links)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function sourceLinksToNarrativeLinks(sourceLinks: DeckPlanSourceLinks, compatibility: DeckPlanNarrativeLink[] = []): DeckPlanNarrativeLink[] {
|
|
802
|
+
const links: DeckPlanNarrativeLink[] = []
|
|
803
|
+
for (const id of sourceLinks.materials) links.push({ id, relation: "uses_evidence", group: "materials" })
|
|
804
|
+
for (const id of sourceLinks.findings) links.push({ id, relation: "uses_evidence", group: "findings" })
|
|
805
|
+
for (const id of sourceLinks.assets) links.push({ id, relation: "uses_evidence", group: "assets" })
|
|
806
|
+
for (const id of sourceLinks.urls) links.push({ id, relation: "uses_evidence", group: "urls" })
|
|
807
|
+
for (const id of sourceLinks.caveats) links.push({ id, relation: "mentions_gap", group: "caveats" })
|
|
808
|
+
return uniqueLinks([...links, ...compatibility])
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function parseBulletText(section: string): string[] {
|
|
812
|
+
return section
|
|
813
|
+
.replace(/\r\n/g, "\n")
|
|
814
|
+
.split("\n")
|
|
815
|
+
.map((line) => line.replace(/^\s*[-*]\s+/, "").trim())
|
|
816
|
+
.filter((line) => line && line.toLowerCase() !== "none.")
|
|
817
|
+
}
|
|
818
|
+
|
|
399
819
|
function relationForDeckPlanLink(group: string, id: string): DeckPlanNarrativeLink["relation"] {
|
|
400
820
|
const normalized = group.toLowerCase()
|
|
821
|
+
if (normalized.includes("source") || normalized.includes("material")) return "uses_evidence"
|
|
822
|
+
if (normalized.includes("finding") || normalized.includes("research")) return "uses_evidence"
|
|
823
|
+
if (normalized.includes("asset") || normalized.includes("media")) return "uses_evidence"
|
|
401
824
|
if (normalized.includes("evidence") || id.startsWith("evidence")) return "uses_evidence"
|
|
402
825
|
if (normalized.includes("risk") || id.startsWith("risk")) return "addresses_risk"
|
|
403
826
|
if (normalized.includes("objection") || id.startsWith("objection")) return "answers_objection"
|
|
@@ -406,6 +829,8 @@ function relationForDeckPlanLink(group: string, id: string): DeckPlanNarrativeLi
|
|
|
406
829
|
}
|
|
407
830
|
|
|
408
831
|
function inferredLinkGroup(id: string, knownNodeIds?: Set<string>): string {
|
|
832
|
+
if (id.startsWith("researches/")) return "findings"
|
|
833
|
+
if (id.startsWith("assets/")) return "assets"
|
|
409
834
|
if (id.startsWith("evidence")) return "evidence"
|
|
410
835
|
if (id.startsWith("risk")) return "risk"
|
|
411
836
|
if (id.startsWith("objection")) return "objection"
|
|
@@ -416,7 +841,7 @@ function inferredLinkGroup(id: string, knownNodeIds?: Set<string>): string {
|
|
|
416
841
|
|
|
417
842
|
function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanProjectionDiagnostic[] {
|
|
418
843
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
419
|
-
if (slides.length === 0) diagnostics.push({ severity: "warning", code: "deck_plan_slides_missing", message: "deck-plan
|
|
844
|
+
if (slides.length === 0) diagnostics.push({ severity: "warning", code: "deck_plan_slides_missing", message: "deck-plan.md contains no slide blocks." })
|
|
420
845
|
const seen = new Map<number, DeckPlanSlideProjection>()
|
|
421
846
|
let previous = 0
|
|
422
847
|
for (const slide of slides) {
|
|
@@ -433,9 +858,53 @@ function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanPr
|
|
|
433
858
|
return diagnostics
|
|
434
859
|
}
|
|
435
860
|
|
|
861
|
+
function buildHtmlWritingBatches(slides: DeckPlanSlideProjection[]): DeckPlanHtmlWritingBatch[] {
|
|
862
|
+
const ordered = slides
|
|
863
|
+
.filter((slide) => Number.isInteger(slide.slideIndex) && (slide.slideIndex ?? 0) > 0)
|
|
864
|
+
.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
865
|
+
const chapterGroups: Array<{ chapterTitle: string; slideIndexes: number[] }> = []
|
|
866
|
+
for (const slide of ordered) {
|
|
867
|
+
const chapterTitle = slide.chapter || "Unassigned"
|
|
868
|
+
const current = chapterGroups[chapterGroups.length - 1]
|
|
869
|
+
if (current && current.chapterTitle === chapterTitle) current.slideIndexes.push(slide.slideIndex!)
|
|
870
|
+
else chapterGroups.push({ chapterTitle, slideIndexes: [slide.slideIndex!] })
|
|
871
|
+
}
|
|
872
|
+
const batches: DeckPlanHtmlWritingBatch[] = []
|
|
873
|
+
for (const group of chapterGroups) {
|
|
874
|
+
const chunks = chunkNumbers(group.slideIndexes, MAX_HTML_SLIDES_PER_BATCH)
|
|
875
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
876
|
+
const chunk = chunks[index]
|
|
877
|
+
const chapterSuffix = chunks.length > 1 ? ` part ${index + 1}` : ""
|
|
878
|
+
const label = batches.length === 0
|
|
879
|
+
? `Initial shell and ${group.chapterTitle}${chapterSuffix}`
|
|
880
|
+
: `${group.chapterTitle}${chapterSuffix}`
|
|
881
|
+
batches.push({
|
|
882
|
+
label,
|
|
883
|
+
chapterTitle: group.chapterTitle,
|
|
884
|
+
slideIndexes: chunk,
|
|
885
|
+
maxSlides: MAX_HTML_SLIDES_PER_BATCH,
|
|
886
|
+
instructions: batches.length === 0
|
|
887
|
+
? `Create or update the foundation if needed, then write only slide sections ${formatSlideRange(chunk)}. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`
|
|
888
|
+
: `Patch only slide sections ${formatSlideRange(chunk)}, preserve previously written slides, and keep the file valid after the patch. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`,
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return batches
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function chunkNumbers(values: number[], size: number): number[][] {
|
|
896
|
+
const chunks: number[][] = []
|
|
897
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size))
|
|
898
|
+
return chunks
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function htmlWritingInstruction(): string {
|
|
902
|
+
return `Before every HTML write/edit/apply_patch, follow htmlWritingBatches and add or rewrite at most ${MAX_HTML_SLIDES_PER_BATCH} <section class="slide"> blocks. Run Artifact QA after each batch before continuing.`
|
|
903
|
+
}
|
|
904
|
+
|
|
436
905
|
function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
437
906
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
438
|
-
if (!slide.structural &&
|
|
907
|
+
if (!slide.structural && linksCount(slide.sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: `Non-structural deck-plan slide ${slide.id} has no material, finding, asset, or URL source link.`, file: slide.path, nodeId: slide.id })
|
|
439
908
|
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
909
|
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
910
|
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 +921,30 @@ function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<str
|
|
|
452
921
|
return diagnostics
|
|
453
922
|
}
|
|
454
923
|
|
|
455
|
-
export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undefined, inventory: { layouts: string[]; components: string[] }): DeckPlanProjectionDiagnostic[] {
|
|
924
|
+
export function deckPlanDesignDiagnostics(projection: DeckPlanProjection | undefined, inventory: { layouts: string[]; components: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }): DeckPlanProjectionDiagnostic[] {
|
|
456
925
|
if (!projection) return []
|
|
457
926
|
const layouts = new Set(inventory.layouts)
|
|
458
927
|
const components = new Set(inventory.components)
|
|
459
928
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
460
929
|
for (const slide of projection.slides) {
|
|
461
930
|
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
|
-
}
|
|
931
|
+
for (const component of slide.componentPlan) diagnostics.push(...componentDesignDiagnostics(slide, component, inventory, components))
|
|
468
932
|
}
|
|
469
933
|
return diagnostics
|
|
470
934
|
}
|
|
471
935
|
|
|
472
|
-
function
|
|
936
|
+
function componentDesignDiagnostics(slide: DeckPlanSlideProjection, component: DeckPlanSlideComponentPlan, inventory: { layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }, components: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
937
|
+
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
938
|
+
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 })
|
|
939
|
+
const allowedSlots = slide.layout ? inventory.layoutSlots?.[slide.layout] : undefined
|
|
940
|
+
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 })
|
|
941
|
+
const nesting = inventory.componentNesting?.[component.name]
|
|
942
|
+
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 })
|
|
943
|
+
for (const child of component.children ?? []) diagnostics.push(...componentDesignDiagnostics(slide, child, inventory, components))
|
|
944
|
+
return diagnostics
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: { designLayouts: string[]; designComponents: string[]; layoutSlots?: Record<string, string[]>; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }): DeckPlanProjectionDiagnostic[] {
|
|
473
948
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
474
949
|
const nodeId = input.id?.trim() || `slide-${input.slideIndex}`
|
|
475
950
|
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 +955,54 @@ function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: {
|
|
|
480
955
|
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
956
|
const componentNames = new Set<string>()
|
|
482
957
|
const positions = new Set<string>()
|
|
958
|
+
const allowedSlots = options.layoutSlots?.[input.layout]
|
|
483
959
|
for (const component of input.components ?? []) {
|
|
960
|
+
validateComponentInput(component, { nodeId, componentNames, positions, options, allowedSlots, parentName: undefined, topLevel: true, diagnostics })
|
|
961
|
+
}
|
|
962
|
+
const visual = normalizeVisualIntent(input.visualIntent)
|
|
963
|
+
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))
|
|
964
|
+
const sourceLinks = sourceLinksForInput(input)
|
|
965
|
+
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, or URL source link.", nodeId })
|
|
966
|
+
return diagnostics
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function validateComponentInput(component: DeckPlanSlideUpsertComponentInput, context: {
|
|
970
|
+
nodeId: string
|
|
971
|
+
componentNames: Set<string>
|
|
972
|
+
positions: Set<string>
|
|
973
|
+
options: { designComponents: string[]; componentNesting?: Record<string, { acceptsChildren: boolean; allowedChildren?: string[] }> }
|
|
974
|
+
allowedSlots?: string[]
|
|
975
|
+
parentName?: string
|
|
976
|
+
topLevel: boolean
|
|
977
|
+
diagnostics: DeckPlanProjectionDiagnostic[]
|
|
978
|
+
}): void {
|
|
484
979
|
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))
|
|
980
|
+
if (name) context.componentNames.add(name)
|
|
981
|
+
if (!name) context.diagnostics.push(errorDiagnostic("slide_component_name_missing", "Every component requires name.", context.nodeId))
|
|
982
|
+
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
983
|
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))
|
|
984
|
+
if (!String(component[key] || "").trim()) context.diagnostics.push(errorDiagnostic("slide_component_plan_incomplete", `Component '${name || "unnamed"}' is missing ${key}.`, context.nodeId))
|
|
490
985
|
}
|
|
491
|
-
if (component.
|
|
986
|
+
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))
|
|
987
|
+
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
988
|
const positionKey = `${component.slot?.trim() || ""}:${component.position?.trim() || ""}`
|
|
493
989
|
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)
|
|
990
|
+
if (context.positions.has(positionKey)) context.diagnostics.push(errorDiagnostic("slide_component_position_duplicate", `Duplicate component slot/position '${positionKey}' makes the plan ambiguous.`, context.nodeId))
|
|
991
|
+
context.positions.add(positionKey)
|
|
496
992
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
993
|
+
const children = component.children ?? []
|
|
994
|
+
const nesting = name ? context.options.componentNesting?.[name] : undefined
|
|
995
|
+
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))
|
|
996
|
+
if (children.length > 0 && nesting?.allowedChildren) {
|
|
997
|
+
for (const child of children) {
|
|
998
|
+
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))
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
for (const child of children) validateComponentInput(child, { ...context, parentName: name, topLevel: false })
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function linksCount(sourceLinks: DeckPlanSourceLinks): number {
|
|
1005
|
+
return sourceLinks.materials.length + sourceLinks.findings.length + sourceLinks.assets.length + sourceLinks.urls.length
|
|
502
1006
|
}
|
|
503
1007
|
|
|
504
1008
|
function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPlanProjectionDiagnostic {
|
|
@@ -508,22 +1012,33 @@ function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPl
|
|
|
508
1012
|
function parseDeckPlanComponentPlan(section: string): DeckPlanSlideComponentPlan[] {
|
|
509
1013
|
const components: DeckPlanSlideComponentPlan[] = []
|
|
510
1014
|
let current: DeckPlanSlideComponentPlan | undefined
|
|
1015
|
+
let currentChild: DeckPlanSlideComponentPlan | undefined
|
|
511
1016
|
let capture: "content" | undefined
|
|
512
1017
|
const flush = () => {
|
|
1018
|
+
if (currentChild && current) {
|
|
1019
|
+
current.children = [...(current.children ?? []), normalizeComponentPlan(currentChild)]
|
|
1020
|
+
currentChild = undefined
|
|
1021
|
+
}
|
|
513
1022
|
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),
|
|
1023
|
+
...normalizeComponentPlan(current),
|
|
1024
|
+
children: current.children,
|
|
520
1025
|
})
|
|
521
1026
|
}
|
|
1027
|
+
const target = () => currentChild ?? current
|
|
1028
|
+
const startComponent = (name: string, child: boolean) => {
|
|
1029
|
+
if (child && current) {
|
|
1030
|
+
if (currentChild) current.children = [...(current.children ?? []), normalizeComponentPlan(currentChild)]
|
|
1031
|
+
currentChild = blankComponentPlan(name)
|
|
1032
|
+
} else {
|
|
1033
|
+
flush()
|
|
1034
|
+
current = blankComponentPlan(name)
|
|
1035
|
+
currentChild = undefined
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
522
1038
|
for (const rawLine of section.replace(/\r\n/g, "\n").split("\n")) {
|
|
523
|
-
const heading =
|
|
1039
|
+
const heading = /^(#{3,6})\s+(.+?)\s*$/.exec(rawLine)
|
|
524
1040
|
if (heading) {
|
|
525
|
-
|
|
526
|
-
current = { name: heading[1].trim(), slot: "", position: "", purpose: "", content: "", claimIds: [], evidenceIds: [], sourceNotes: [], renderNotes: [] }
|
|
1041
|
+
startComponent(heading[2].trim(), heading[1].length > 5)
|
|
527
1042
|
capture = undefined
|
|
528
1043
|
continue
|
|
529
1044
|
}
|
|
@@ -534,25 +1049,46 @@ function parseDeckPlanComponentPlan(section: string): DeckPlanSlideComponentPlan
|
|
|
534
1049
|
capture = undefined
|
|
535
1050
|
const key = field[1].toLowerCase()
|
|
536
1051
|
const value = field[2].trim()
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
else if (key === "
|
|
1052
|
+
const item = target()
|
|
1053
|
+
if (!item) continue
|
|
1054
|
+
if (key === "slot") item.slot = value
|
|
1055
|
+
else if (key === "position") item.position = value
|
|
1056
|
+
else if (key === "placement note") item.placementNote = value
|
|
1057
|
+
else if (key === "purpose") item.purpose = value
|
|
541
1058
|
else if (key === "content") {
|
|
542
|
-
|
|
1059
|
+
item.content = cleanPlanValue(value)
|
|
543
1060
|
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")
|
|
1061
|
+
} else if (key === "claim ids") item.claimIds = parseCsv(value)
|
|
1062
|
+
else if (key === "evidence ids") item.evidenceIds = parseCsv(value)
|
|
1063
|
+
else if (key === "source notes") item.sourceNotes = parseListValue(value)
|
|
1064
|
+
else if (key === "render notes") item.renderNotes = parseListValue(value)
|
|
548
1065
|
continue
|
|
549
1066
|
}
|
|
550
|
-
if (capture === "content" && rawLine.trim())
|
|
1067
|
+
if (capture === "content" && rawLine.trim()) {
|
|
1068
|
+
const item = target()
|
|
1069
|
+
if (item) item.content += `${item.content ? "\n" : ""}${rawLine.replace(/^\s{2}/, "")}`
|
|
1070
|
+
}
|
|
551
1071
|
}
|
|
552
1072
|
flush()
|
|
553
1073
|
return components
|
|
554
1074
|
}
|
|
555
1075
|
|
|
1076
|
+
function blankComponentPlan(name: string): DeckPlanSlideComponentPlan {
|
|
1077
|
+
return { name, slot: "", position: "", purpose: "", content: "", claimIds: [], evidenceIds: [], sourceNotes: [], renderNotes: [] }
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function normalizeComponentPlan(component: DeckPlanSlideComponentPlan): DeckPlanSlideComponentPlan {
|
|
1081
|
+
return {
|
|
1082
|
+
...component,
|
|
1083
|
+
content: component.content.trim(),
|
|
1084
|
+
claimIds: uniqueStrings(component.claimIds),
|
|
1085
|
+
evidenceIds: uniqueStrings(component.evidenceIds),
|
|
1086
|
+
sourceNotes: component.sourceNotes.filter(Boolean),
|
|
1087
|
+
renderNotes: component.renderNotes.filter(Boolean),
|
|
1088
|
+
children: component.children?.map(normalizeComponentPlan),
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
556
1092
|
function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: string }): string {
|
|
557
1093
|
const components = input.components.map((component) => component.name.trim())
|
|
558
1094
|
const lines: string[] = []
|
|
@@ -595,29 +1131,7 @@ function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: str
|
|
|
595
1131
|
lines.push(`- Render notes: ${formatListValue(component.renderNotes)}`)
|
|
596
1132
|
lines.push("")
|
|
597
1133
|
}
|
|
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("")
|
|
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("")
|
|
1134
|
+
lines.push(renderSourceLinksMarkdown(sourceLinksForInput(input)).replace(/^####/gm, "##"))
|
|
621
1135
|
return lines.join("\n")
|
|
622
1136
|
}
|
|
623
1137
|
|
|
@@ -650,8 +1164,8 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
650
1164
|
lines.push("")
|
|
651
1165
|
lines.push("## Source Authority")
|
|
652
1166
|
lines.push("")
|
|
653
|
-
lines.push("-
|
|
654
|
-
lines.push("- Render planning: `deck-plan
|
|
1167
|
+
lines.push("- Sources: local materials, reviewed findings, workspace assets, URLs, and user intent.")
|
|
1168
|
+
lines.push("- Render planning: `deck-plan.md` is the execution blueprint for HTML deck generation.")
|
|
655
1169
|
lines.push("")
|
|
656
1170
|
lines.push("## Audience / Goal / Decision")
|
|
657
1171
|
lines.push("")
|
|
@@ -669,7 +1183,7 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
669
1183
|
lines.push("")
|
|
670
1184
|
lines.push("## Slide Plan")
|
|
671
1185
|
lines.push("")
|
|
672
|
-
if (slides.length === 0) lines.push("- No slide
|
|
1186
|
+
if (slides.length === 0) lines.push("- No slide blocks yet.")
|
|
673
1187
|
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
1188
|
lines.push("")
|
|
675
1189
|
lines.push("## Evidence Trace")
|
|
@@ -678,10 +1192,10 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
678
1192
|
if (evidenceIds.length === 0) lines.push("- No evidence links planned yet.")
|
|
679
1193
|
else for (const id of evidenceIds) lines.push(`- [[${id}]]`)
|
|
680
1194
|
lines.push("")
|
|
681
|
-
lines.push("##
|
|
1195
|
+
lines.push("## Source Limitations")
|
|
682
1196
|
lines.push("")
|
|
683
1197
|
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
|
|
1198
|
+
if (boundaryIds.length === 0) lines.push("- No legacy risk, objection, or gap links planned.")
|
|
685
1199
|
else for (const id of boundaryIds) lines.push(`- [[${id}]]`)
|
|
686
1200
|
lines.push("")
|
|
687
1201
|
lines.push("## Chapter Writing Batches")
|
|
@@ -725,8 +1239,8 @@ function booleanField(frontmatter: Record<string, string | string[] | boolean>,
|
|
|
725
1239
|
|
|
726
1240
|
function arrayField(frontmatter: Record<string, string | string[] | boolean>, key: string): string[] {
|
|
727
1241
|
const value = frontmatter[key]
|
|
728
|
-
if (Array.isArray(value)) return value.map((item) => item.trim()).filter(
|
|
729
|
-
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter(
|
|
1242
|
+
if (Array.isArray(value)) return value.map((item) => item.trim()).filter((item) => item && item.toLowerCase() !== "none")
|
|
1243
|
+
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter((item) => item && item.toLowerCase() !== "none")
|
|
730
1244
|
return []
|
|
731
1245
|
}
|
|
732
1246
|
|
|
@@ -860,8 +1374,8 @@ function renderDeckPlanMarkdown(input: DeckPlanArtifactInput): string {
|
|
|
860
1374
|
lines.push("")
|
|
861
1375
|
lines.push("- Write one `<section class=\"slide\" data-slide-index=\"N\">` per planned slide in the completed deck, using positive 1-based slide indexes that are unique and strictly increase in DOM order. Partial chapter-by-chapter drafts may contain only the written prefix/range.")
|
|
862
1376
|
lines.push("- Keep every rendered slide exactly 1920x1080px with no page-level scrollbars or hidden overflow.")
|
|
863
|
-
lines.push("- Preserve claim-led chapters, visual intent, evidence ids, source trace,
|
|
864
|
-
lines.push(
|
|
1377
|
+
lines.push("- Preserve claim-led chapters, visual intent, evidence ids, source trace, source limitations, unresolved inputs, and user review notes.")
|
|
1378
|
+
lines.push(`- Generate HTML in the listed writing batches; do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in one write or patch.`)
|
|
865
1379
|
lines.push("")
|
|
866
1380
|
lines.push("## Chapter Map")
|
|
867
1381
|
lines.push("")
|
|
@@ -879,14 +1393,17 @@ function renderDeckPlanMarkdown(input: DeckPlanArtifactInput): string {
|
|
|
879
1393
|
}
|
|
880
1394
|
lines.push("## Chapter Writing Batches")
|
|
881
1395
|
lines.push("")
|
|
882
|
-
lines.push(
|
|
1396
|
+
lines.push(`Use these batches for HTML generation. Each batch is capped at ${MAX_HTML_SLIDES_PER_BATCH} slide sections. Keep the HTML valid after every batch and preserve previously written slides.`)
|
|
883
1397
|
lines.push("")
|
|
884
1398
|
if (input.renderPlan) {
|
|
885
|
-
for (const batch of input.renderPlan.chapterWritingBatches) lines.push(`- ${batch.label}: ${batch.chapterTitle}, slides ${formatSlideRange(batch.slideIndexes)}. ${batch.instructions}`)
|
|
1399
|
+
for (const batch of input.renderPlan.chapterWritingBatches) lines.push(`- ${batch.label}: ${batch.chapterTitle}, slides ${formatSlideRange(batch.slideIndexes)}; max ${batch.maxSlides} slides. ${batch.instructions}`)
|
|
886
1400
|
} else {
|
|
887
1401
|
input.chapters.forEach((chapter, index) => {
|
|
888
1402
|
const prefix = index === 0 ? "Initial shell and first chapter" : `Chapter batch ${index + 1}`
|
|
889
|
-
|
|
1403
|
+
for (const [chunkIndex, chunk] of chunkNumbers(chapter.slideIndexes, MAX_HTML_SLIDES_PER_BATCH).entries()) {
|
|
1404
|
+
const suffix = chunkIndex === 0 ? "" : ` part ${chunkIndex + 1}`
|
|
1405
|
+
lines.push(`- ${prefix}${suffix}: ${chapter.title}, slides ${formatSlideRange(chunk)}; max ${MAX_HTML_SLIDES_PER_BATCH} slides.`)
|
|
1406
|
+
}
|
|
890
1407
|
})
|
|
891
1408
|
}
|
|
892
1409
|
if (input.renderPlan) {
|