@cyber-dash-tech/revela 0.17.0 → 0.17.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 +45 -567
- package/README.zh-CN.md +47 -535
- package/designs/monet/DESIGN.md +1 -1
- package/designs/starter/DESIGN.md +1 -1
- package/designs/summit/DESIGN.md +1 -1
- package/lib/commands/help.ts +4 -4
- package/lib/commands/init.ts +9 -7
- package/lib/commands/narrative.ts +13 -4
- package/lib/commands/pdf.ts +24 -15
- package/lib/commands/pptx.ts +2 -16
- package/lib/commands/research.ts +2 -0
- package/lib/commands/review.ts +81 -93
- package/lib/deck-html/contract.ts +26 -10
- package/lib/decks-state.ts +75 -86
- package/lib/edit/deck-state.ts +3 -111
- package/lib/edit/open.ts +2 -2
- package/lib/edit/resolve-deck.ts +14 -24
- package/lib/inspect/open.ts +2 -2
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/deck-plan-artifact.ts +584 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +649 -44
- package/lib/narrative-state/research-gaps.ts +5 -2
- package/lib/narrative-vault/compile.ts +16 -1
- package/lib/narrative-vault/types.ts +4 -2
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/open.ts +2 -2
- package/lib/refine/server.ts +157 -20
- package/package.json +1 -1
- package/plugin.ts +2 -2
- package/skill/NARRATIVE_SKILL.md +19 -19
- package/skill/SKILL.md +99 -35
- package/tools/decks.ts +83 -51
- package/tools/narrative-view.ts +16 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deckPlanHash, upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec } from "../decks-state"
|
|
1
|
+
import { deckPlanHash, upsertDeck, upsertSlides, type DeckSpec, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec, type VisualBrief } from "../decks-state"
|
|
2
2
|
import { ensureActiveHtmlDeckRenderTarget } from "../workspace-state/render-targets"
|
|
3
3
|
import { getClaimSlideRefs } from "./queries"
|
|
4
4
|
import { computeNarrativeHash } from "./hash"
|
|
@@ -15,10 +15,38 @@ export interface CompileDeckPlanResult {
|
|
|
15
15
|
skipped: boolean
|
|
16
16
|
reason?: string
|
|
17
17
|
narrativeHash: string
|
|
18
|
+
planHash?: string
|
|
19
|
+
planArtifactPath?: string
|
|
18
20
|
slideCount: number
|
|
19
21
|
slides: SlideSpec[]
|
|
20
22
|
chapters?: DeckPlanChapter[]
|
|
21
23
|
qualityChecks?: DeckPlanQualityCheck[]
|
|
24
|
+
renderPlan?: RenderPlanContract
|
|
25
|
+
planningPacket?: DeckPlanningPacket
|
|
26
|
+
deckPlanRequirements?: DeckPlanRequirements
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface DeckPlanningPacket {
|
|
30
|
+
narrativeHash: string
|
|
31
|
+
outputPath: string
|
|
32
|
+
audience: NarrativeStateV1["audience"]
|
|
33
|
+
decision: NarrativeStateV1["decision"]
|
|
34
|
+
thesis: NarrativeStateV1["thesis"]
|
|
35
|
+
centralClaims: NarrativeClaim[]
|
|
36
|
+
supportingClaims: NarrativeClaim[]
|
|
37
|
+
evidenceBindings: NarrativeEvidenceBinding[]
|
|
38
|
+
objections: NarrativeStateV1["objections"]
|
|
39
|
+
risks: NarrativeStateV1["risks"]
|
|
40
|
+
researchGaps: NarrativeStateV1["researchGaps"]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface DeckPlanRequirements {
|
|
44
|
+
planArtifactPath: string
|
|
45
|
+
slidePlanDir: string
|
|
46
|
+
defaultProfile: string
|
|
47
|
+
userConfirmations: string[]
|
|
48
|
+
authoringRules: string[]
|
|
49
|
+
requiredSections: string[]
|
|
22
50
|
}
|
|
23
51
|
|
|
24
52
|
export interface DeckPlanChapter {
|
|
@@ -27,6 +55,7 @@ export interface DeckPlanChapter {
|
|
|
27
55
|
slideIndexes: number[]
|
|
28
56
|
claimIds: string[]
|
|
29
57
|
evidenceBindingIds: string[]
|
|
58
|
+
sourceClaimId?: string
|
|
30
59
|
}
|
|
31
60
|
|
|
32
61
|
export interface DeckPlanQualityCheck {
|
|
@@ -35,6 +64,63 @@ export interface DeckPlanQualityCheck {
|
|
|
35
64
|
message: string
|
|
36
65
|
}
|
|
37
66
|
|
|
67
|
+
export type RenderPlanSlideKind = "cover" | "toc" | "chapter-divider" | "claim-framing" | "claim-evidence" | "claim-implication" | "supporting-evidence" | "risk" | "ask" | "content"
|
|
68
|
+
|
|
69
|
+
export interface RenderPlanSlideMetadata {
|
|
70
|
+
index: number
|
|
71
|
+
title: string
|
|
72
|
+
chapterTitle?: string
|
|
73
|
+
chapterRole?: DeckPlanChapter["role"]
|
|
74
|
+
slideKind: RenderPlanSlideKind
|
|
75
|
+
structural: boolean
|
|
76
|
+
countsTowardClaimSubstance: boolean
|
|
77
|
+
requiredComponents: string[]
|
|
78
|
+
evidenceTraceRequired: boolean
|
|
79
|
+
claimChapterRequirement?: "divider" | "framing" | "proof" | "implication" | "supporting-evidence" | "risk" | "ask"
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface RenderPlanChapterRequirement {
|
|
83
|
+
title: string
|
|
84
|
+
role: DeckPlanChapter["role"]
|
|
85
|
+
sourceClaimId?: string
|
|
86
|
+
slideIndexes: number[]
|
|
87
|
+
requiredSubstanceSlides: number
|
|
88
|
+
actualSubstanceSlides: number
|
|
89
|
+
allowedStructuralSlides: string[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface RenderPlanWritingBatch {
|
|
93
|
+
label: string
|
|
94
|
+
chapterTitle: string
|
|
95
|
+
slideIndexes: number[]
|
|
96
|
+
instructions: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface RenderPlanContract {
|
|
100
|
+
sourceAuthority: {
|
|
101
|
+
meaning: string
|
|
102
|
+
renderPlan: string
|
|
103
|
+
state: string
|
|
104
|
+
htmlIdentity: string
|
|
105
|
+
}
|
|
106
|
+
renderRules: string[]
|
|
107
|
+
htmlIdentityContract: string[]
|
|
108
|
+
chapterRequirements: RenderPlanChapterRequirement[]
|
|
109
|
+
chapterWritingBatches: RenderPlanWritingBatch[]
|
|
110
|
+
slideRenderMetadata: RenderPlanSlideMetadata[]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type VisualIntentKind = "hero" | "toc" | "metric-stat" | "evidence-table" | "comparison-grid" | "risk-matrix" | "steps" | "text-only"
|
|
114
|
+
type ClaimChapterSlideKind = "framing" | "evidence" | "implication"
|
|
115
|
+
|
|
116
|
+
interface VisualIntent {
|
|
117
|
+
kind: VisualIntentKind
|
|
118
|
+
component: string
|
|
119
|
+
rationale: string
|
|
120
|
+
dataSignals: string[]
|
|
121
|
+
evidenceBindingIds: string[]
|
|
122
|
+
}
|
|
123
|
+
|
|
38
124
|
export function compileDeckPlanFromNarrative(state: DecksState, options: CompileDeckPlanOptions = {}): { state: DecksState; result: CompileDeckPlanResult } {
|
|
39
125
|
const narrative = normalizeNarrativeState(state)
|
|
40
126
|
const narrativeHash = computeNarrativeHash(narrative)
|
|
@@ -56,10 +142,8 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
56
142
|
const deckKey = state.activeDeck ?? Object.keys(state.decks)[0]
|
|
57
143
|
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
58
144
|
const slug = deck?.slug ?? state.activeDeck ?? "deck"
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const qualityChecks = checkPlanQuality(narrative, slides, chapters)
|
|
62
|
-
const planCoverage = deckPlanCoverage(narrative, slides)
|
|
145
|
+
const planningPacket = buildDeckPlanningPacket(narrative, narrativeHash, deck?.outputPath ?? `decks/${slug}.html`)
|
|
146
|
+
const deckPlanRequirements = buildDeckPlanRequirements(narrativeHash)
|
|
63
147
|
const requiredInputs: Partial<RequiredInputs> = {
|
|
64
148
|
topicClarified: true,
|
|
65
149
|
audienceClarified: Boolean(narrative.audience.primary),
|
|
@@ -83,13 +167,12 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
83
167
|
} as RequiredInputs,
|
|
84
168
|
writeReadiness: deck?.writeReadiness ?? { status: "blocked" as const, blockers: [] },
|
|
85
169
|
})
|
|
86
|
-
next = upsertSlides(next, slug, slides)
|
|
87
170
|
const plannedDeck = next.decks[slug]
|
|
88
171
|
plannedDeck.planReview = {
|
|
89
172
|
status: "pending",
|
|
90
173
|
narrativeHash,
|
|
91
|
-
planHash:
|
|
92
|
-
qualityChecks,
|
|
174
|
+
planHash: "pending-deck-plan-md",
|
|
175
|
+
qualityChecks: [],
|
|
93
176
|
}
|
|
94
177
|
plannedDeck.requiredInputs = { ...plannedDeck.requiredInputs, slidePlanConfirmed: false }
|
|
95
178
|
plannedDeck.writeReadiness = { status: "blocked", blockers: [] }
|
|
@@ -99,13 +182,13 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
99
182
|
if (htmlTarget) {
|
|
100
183
|
htmlTarget.data = {
|
|
101
184
|
...(htmlTarget.data ?? {}),
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
requiredClaimIds:
|
|
107
|
-
coveredClaimIds:
|
|
108
|
-
missingClaimIds:
|
|
185
|
+
narrativeId: narrative.id,
|
|
186
|
+
narrativeHash,
|
|
187
|
+
planningPacket,
|
|
188
|
+
deckPlanRequirements,
|
|
189
|
+
requiredClaimIds: narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id),
|
|
190
|
+
coveredClaimIds: [],
|
|
191
|
+
missingClaimIds: narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id),
|
|
109
192
|
claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
|
|
110
193
|
claimId: ref.claimId,
|
|
111
194
|
claimText: ref.claimText,
|
|
@@ -124,14 +207,169 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
124
207
|
compiled: true,
|
|
125
208
|
skipped: false,
|
|
126
209
|
narrativeHash,
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
210
|
+
planArtifactPath: "deck-plan/index.md",
|
|
211
|
+
slideCount: 0,
|
|
212
|
+
slides: [],
|
|
213
|
+
chapters: [],
|
|
214
|
+
qualityChecks: [],
|
|
215
|
+
planningPacket,
|
|
216
|
+
deckPlanRequirements,
|
|
131
217
|
},
|
|
132
218
|
}
|
|
133
219
|
}
|
|
134
220
|
|
|
221
|
+
function buildDeckPlanningPacket(narrative: NarrativeStateV1, narrativeHash: string, outputPath: string): DeckPlanningPacket {
|
|
222
|
+
return {
|
|
223
|
+
narrativeHash,
|
|
224
|
+
outputPath,
|
|
225
|
+
audience: narrative.audience,
|
|
226
|
+
decision: narrative.decision,
|
|
227
|
+
thesis: narrative.thesis,
|
|
228
|
+
centralClaims: orderedClaims(narrative, (claim) => claim.importance === "central"),
|
|
229
|
+
supportingClaims: orderedClaims(narrative, (claim) => claim.importance !== "central"),
|
|
230
|
+
evidenceBindings: narrative.evidenceBindings,
|
|
231
|
+
objections: narrative.objections,
|
|
232
|
+
risks: narrative.risks,
|
|
233
|
+
researchGaps: narrative.researchGaps,
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildDeckPlanRequirements(narrativeHash: string): DeckPlanRequirements {
|
|
238
|
+
return {
|
|
239
|
+
planArtifactPath: "deck-plan/index.md",
|
|
240
|
+
slidePlanDir: "deck-plan/slides",
|
|
241
|
+
defaultProfile: "executive decision deck, usually 12-18 slides unless the user confirms otherwise",
|
|
242
|
+
userConfirmations: [
|
|
243
|
+
"Confirm target slide count or acceptable range when it is unclear.",
|
|
244
|
+
"Confirm audience and decision context when the narrative does not make them explicit.",
|
|
245
|
+
"Confirm language, emphasis, or visual style only when needed before writing the plan.",
|
|
246
|
+
],
|
|
247
|
+
authoringRules: [
|
|
248
|
+
"LLM writes deck-plan/index.md and deck-plan/slides/*.md from the planning packet; compileDeckPlan does not generate the final slide list.",
|
|
249
|
+
"Use 3-5 chapters for normal executive decks.",
|
|
250
|
+
"Cover every central claim, but group related central claims into chapters instead of giving each claim its own chapter.",
|
|
251
|
+
"Each substantive chapter should have framing, proof, and implication/boundary coverage.",
|
|
252
|
+
"Chapter divider or chapter TOC slides may use the toc component as structural wayfinding.",
|
|
253
|
+
"Do not create filler slides, repeated thesis pages, or generic bridge slides.",
|
|
254
|
+
"Preserve evidence ids, source trace, supported scope, unsupported scope, caveats, and strength where available.",
|
|
255
|
+
"Do not render internal labels such as Evidence gap:, Unsupported scope:, Caveat:, Missing Data, or Evidence Boundary in executive body copy.",
|
|
256
|
+
"Do not infer plan structure from DECKS.json slides[]; it is compatibility cache only.",
|
|
257
|
+
"Use ## Narrative Links in slide files for [[claim-id]], [[evidence-id]], [[risk-id]], [[objection-id]], or [[gap-id]] references; do not use canonical ## Relations in deck-plan files.",
|
|
258
|
+
],
|
|
259
|
+
requiredSections: [
|
|
260
|
+
"Source Authority",
|
|
261
|
+
"Audience / Goal / Decision",
|
|
262
|
+
"Deck Parameters",
|
|
263
|
+
"Chapter Map",
|
|
264
|
+
"Slide Plan",
|
|
265
|
+
"Evidence Trace",
|
|
266
|
+
"Boundary / Risk Treatment",
|
|
267
|
+
"Chapter Writing Batches",
|
|
268
|
+
"HTML Identity Contract",
|
|
269
|
+
],
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function buildRenderPlanContract(deck: DeckSpec, chapters: DeckPlanChapter[]): RenderPlanContract {
|
|
274
|
+
return {
|
|
275
|
+
sourceAuthority: {
|
|
276
|
+
meaning: "revela-narrative/ canonical narrative state",
|
|
277
|
+
renderPlan: "deck-plan/ projection workspace plus compileDeckPlan planning packet",
|
|
278
|
+
state: "DECKS.json compatibility/render state only; slides[] is cached projection data",
|
|
279
|
+
htmlIdentity: "positive 1-based data-slide-index values, unique and strictly increasing in DOM order",
|
|
280
|
+
},
|
|
281
|
+
renderRules: [
|
|
282
|
+
"Do not infer deck structure, slide count, or chapter substance from DECKS.json slides[].",
|
|
283
|
+
"Use the compileDeckPlan planning packet plus deck-plan/ projection Markdown as the render-plan contract.",
|
|
284
|
+
"Render chapter divider slides with the toc component when slideKind is chapter-divider.",
|
|
285
|
+
"Chapter divider and global TOC slides are structural wayfinding and do not count toward central-claim substance.",
|
|
286
|
+
"Each central claim chapter needs non-structural framing, proof, and implication/boundary slides unless the current deck-plan projection explicitly says otherwise.",
|
|
287
|
+
"Generate HTML chapter by chapter, preserving valid HTML and already-written slides after every batch.",
|
|
288
|
+
],
|
|
289
|
+
htmlIdentityContract: [
|
|
290
|
+
"Every written slide section uses class slide and a positive 1-based data-slide-index.",
|
|
291
|
+
"data-slide-index values are unique and strictly increase in DOM order.",
|
|
292
|
+
"Partial chapter-by-chapter artifacts are allowed when written slide identities are self-consistent.",
|
|
293
|
+
"Do not pad missing planned chapters just to match cached DECKS.json slides[].",
|
|
294
|
+
],
|
|
295
|
+
chapterRequirements: chapters.map((chapter) => {
|
|
296
|
+
const substanceSlides = chapter.slideIndexes
|
|
297
|
+
.map((index) => deck.slides.find((slide) => slide.index === index))
|
|
298
|
+
.filter((slide): slide is SlideSpec => Boolean(slide))
|
|
299
|
+
.filter((slide) => slideCountsTowardClaimSubstance(slide))
|
|
300
|
+
return {
|
|
301
|
+
title: chapter.title,
|
|
302
|
+
role: chapter.role,
|
|
303
|
+
sourceClaimId: chapter.sourceClaimId,
|
|
304
|
+
slideIndexes: chapter.slideIndexes,
|
|
305
|
+
requiredSubstanceSlides: chapter.sourceClaimId ? 3 : 0,
|
|
306
|
+
actualSubstanceSlides: substanceSlides.length,
|
|
307
|
+
allowedStructuralSlides: chapter.sourceClaimId ? ["chapter-divider", "toc"] : ["cover", "toc", "ask"],
|
|
308
|
+
}
|
|
309
|
+
}),
|
|
310
|
+
chapterWritingBatches: chapters.map((chapter, index) => ({
|
|
311
|
+
label: index === 0 ? "Initial shell and first chapter" : `Chapter batch ${index + 1}`,
|
|
312
|
+
chapterTitle: chapter.title,
|
|
313
|
+
slideIndexes: chapter.slideIndexes,
|
|
314
|
+
instructions: index === 0
|
|
315
|
+
? "Create the stable HTML shell, required structural slides, and this first chapter range only."
|
|
316
|
+
: "Patch exactly this chapter range, preserve previously written slides, and keep the file valid after the patch.",
|
|
317
|
+
})),
|
|
318
|
+
slideRenderMetadata: deck.slides.map((slide) => slideRenderMetadata(slide, chapters)),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function slideRenderMetadata(slide: SlideSpec, chapters: DeckPlanChapter[]): RenderPlanSlideMetadata {
|
|
323
|
+
const chapter = chapters.find((item) => item.slideIndexes.includes(slide.index))
|
|
324
|
+
const slideKind = renderPlanSlideKind(slide, chapter)
|
|
325
|
+
return {
|
|
326
|
+
index: slide.index,
|
|
327
|
+
title: slide.title,
|
|
328
|
+
chapterTitle: chapter?.title,
|
|
329
|
+
chapterRole: chapter?.role,
|
|
330
|
+
slideKind,
|
|
331
|
+
structural: isStructuralRenderPlanSlide(slideKind),
|
|
332
|
+
countsTowardClaimSubstance: slideCountsTowardClaimSubstance(slide),
|
|
333
|
+
requiredComponents: slide.components ?? [],
|
|
334
|
+
evidenceTraceRequired: (slide.evidenceBindingIds?.length ?? 0) > 0 || (slide.evidence?.length ?? 0) > 0,
|
|
335
|
+
claimChapterRequirement: claimChapterRequirementForSlideKind(slideKind),
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function renderPlanSlideKind(slide: SlideSpec, chapter: DeckPlanChapter | undefined): RenderPlanSlideKind {
|
|
340
|
+
const chapterSlide = (slide.content?.data as { chapterSlide?: string } | undefined)?.chapterSlide
|
|
341
|
+
if (slide.index === 1 && slide.components.includes("hero")) return "cover"
|
|
342
|
+
if (slide.layout === "toc" && chapter?.sourceClaimId) return "chapter-divider"
|
|
343
|
+
if (slide.layout === "toc") return "toc"
|
|
344
|
+
if (chapterSlide === "framing") return "claim-framing"
|
|
345
|
+
if (chapterSlide === "evidence") return "claim-evidence"
|
|
346
|
+
if (chapterSlide === "implication") return "claim-implication"
|
|
347
|
+
if (slide.narrativeRole === "risk") return "risk"
|
|
348
|
+
if (slide.narrativeRole === "ask" || slide.narrativeRole === "close") return "ask"
|
|
349
|
+
if (slide.narrativeRole === "evidence") return "supporting-evidence"
|
|
350
|
+
return "content"
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isStructuralRenderPlanSlide(kind: RenderPlanSlideKind): boolean {
|
|
354
|
+
return kind === "cover" || kind === "toc" || kind === "chapter-divider" || kind === "ask"
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function slideCountsTowardClaimSubstance(slide: SlideSpec): boolean {
|
|
358
|
+
const kind = (slide.content?.data as { chapterSlide?: string } | undefined)?.chapterSlide
|
|
359
|
+
return kind === "framing" || kind === "evidence" || kind === "implication"
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function claimChapterRequirementForSlideKind(kind: RenderPlanSlideKind): RenderPlanSlideMetadata["claimChapterRequirement"] {
|
|
363
|
+
if (kind === "chapter-divider") return "divider"
|
|
364
|
+
if (kind === "claim-framing") return "framing"
|
|
365
|
+
if (kind === "claim-evidence") return "proof"
|
|
366
|
+
if (kind === "claim-implication") return "implication"
|
|
367
|
+
if (kind === "supporting-evidence") return "supporting-evidence"
|
|
368
|
+
if (kind === "risk") return "risk"
|
|
369
|
+
if (kind === "ask") return "ask"
|
|
370
|
+
return undefined
|
|
371
|
+
}
|
|
372
|
+
|
|
135
373
|
function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chapters: DeckPlanChapter[] } {
|
|
136
374
|
const slides: SlideSpec[] = []
|
|
137
375
|
const evidenceByClaim = evidenceBindingsByClaim(narrative.evidenceBindings)
|
|
@@ -144,9 +382,18 @@ function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chap
|
|
|
144
382
|
slides.push(tocSlide(slides.length + 1, chapters))
|
|
145
383
|
|
|
146
384
|
for (const claim of centralClaims) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
385
|
+
const bindings = evidenceByClaim.get(claim.id) ?? []
|
|
386
|
+
const chapter = chapters.find((item) => item.sourceClaimId === claim.id)
|
|
387
|
+
const divider = chapterDividerSlide(slides.length + 1, chapters, chapter ?? chapterForClaimFallback(claim))
|
|
388
|
+
slides.push(divider)
|
|
389
|
+
if (chapter) assignSlideToSpecificChapter(chapter, divider)
|
|
390
|
+
else assignSlideToChapter(chapters, chapterRoleForClaim(claim), divider)
|
|
391
|
+
for (const kind of ["framing", "evidence", "implication"] as const) {
|
|
392
|
+
const slide = claimChapterSlide(slides.length + 1, claim, bindings, kind, narrative)
|
|
393
|
+
slides.push(slide)
|
|
394
|
+
if (chapter) assignSlideToSpecificChapter(chapter, slide)
|
|
395
|
+
else assignSlideToChapter(chapters, chapterRoleForClaim(claim), slide)
|
|
396
|
+
}
|
|
150
397
|
}
|
|
151
398
|
if (supportingClaims.length > 0) {
|
|
152
399
|
const slide = supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim)
|
|
@@ -168,6 +415,7 @@ function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chap
|
|
|
168
415
|
}
|
|
169
416
|
|
|
170
417
|
function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
418
|
+
const visualIntent = visualIntentForStructuralSlide("hero", "Use a hero frame to anchor the decision context and belief shift before evidence detail.")
|
|
171
419
|
return {
|
|
172
420
|
index,
|
|
173
421
|
title: "Decision Context",
|
|
@@ -183,13 +431,16 @@ function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
|
183
431
|
narrative.audience.beliefAfter ? `After: ${narrative.audience.beliefAfter}` : "After belief needs confirmation.",
|
|
184
432
|
],
|
|
185
433
|
bullets: narrative.decision.action ? [`Decision: ${narrative.decision.action}`] : [],
|
|
434
|
+
data: { visualIntent },
|
|
186
435
|
},
|
|
436
|
+
visuals: visualBriefs(index, visualIntent),
|
|
187
437
|
evidence: [],
|
|
188
438
|
status: "planned",
|
|
189
439
|
}
|
|
190
440
|
}
|
|
191
441
|
|
|
192
442
|
function tocSlide(index: number, chapters: DeckPlanChapter[]): SlideSpec {
|
|
443
|
+
const visualIntent = visualIntentForStructuralSlide("toc", "Render the chapter sequence as a visual table of contents instead of a bullet-only agenda.")
|
|
193
444
|
return {
|
|
194
445
|
index,
|
|
195
446
|
title: "Storyline",
|
|
@@ -201,14 +452,55 @@ function tocSlide(index: number, chapters: DeckPlanChapter[]): SlideSpec {
|
|
|
201
452
|
content: {
|
|
202
453
|
headline: "How the decision story is organized",
|
|
203
454
|
bullets: chapters.map((chapter) => chapter.title),
|
|
204
|
-
data: { chapters: chapters.map((chapter) => ({ title: chapter.title, role: chapter.role })) },
|
|
455
|
+
data: { chapters: chapters.map((chapter) => ({ title: chapter.title, role: chapter.role })), visualIntent },
|
|
205
456
|
},
|
|
457
|
+
visuals: visualBriefs(index, visualIntent),
|
|
206
458
|
evidence: [],
|
|
207
459
|
status: "planned",
|
|
208
460
|
}
|
|
209
461
|
}
|
|
210
462
|
|
|
463
|
+
function chapterDividerSlide(index: number, chapters: DeckPlanChapter[], chapter: DeckPlanChapter): SlideSpec {
|
|
464
|
+
const visualIntent = visualIntentForStructuralSlide("toc", "Use a chapter divider as wayfinding before the claim-led chapter; it is structural and does not replace framing, proof, or implication slides.")
|
|
465
|
+
return {
|
|
466
|
+
index,
|
|
467
|
+
title: chapter.title,
|
|
468
|
+
purpose: `Open the ${chapter.title} chapter with a TOC-style divider before claim substance slides.`,
|
|
469
|
+
narrativeRole: "context",
|
|
470
|
+
layout: "toc",
|
|
471
|
+
qa: false,
|
|
472
|
+
components: ["toc", "text-panel"],
|
|
473
|
+
claimIds: chapter.sourceClaimId ? [chapter.sourceClaimId] : [],
|
|
474
|
+
claimRefs: chapter.sourceClaimId ? [{ claimId: chapter.sourceClaimId, role: "supporting", note: "Structural chapter divider; not counted as claim proof." }] : [],
|
|
475
|
+
evidenceBindingIds: [],
|
|
476
|
+
content: {
|
|
477
|
+
headline: chapter.title,
|
|
478
|
+
bullets: chapters.map((item) => item.title),
|
|
479
|
+
data: {
|
|
480
|
+
activeChapter: chapter.title,
|
|
481
|
+
chapters: chapters.map((item) => ({ title: item.title, role: item.role, active: item.title === chapter.title })),
|
|
482
|
+
visualIntent,
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
visuals: visualBriefs(index, visualIntent),
|
|
486
|
+
evidence: [],
|
|
487
|
+
status: "planned",
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function chapterForClaimFallback(claim: NarrativeClaim): DeckPlanChapter {
|
|
492
|
+
return {
|
|
493
|
+
title: claimChapterTitle(claim),
|
|
494
|
+
role: chapterRoleForClaim(claim),
|
|
495
|
+
slideIndexes: [],
|
|
496
|
+
claimIds: [claim.id],
|
|
497
|
+
evidenceBindingIds: [],
|
|
498
|
+
sourceClaimId: claim.id,
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
211
502
|
function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
503
|
+
const visualIntent = visualIntentForClaim(claim, bindings)
|
|
212
504
|
return {
|
|
213
505
|
index,
|
|
214
506
|
title: titleFromClaim(claim),
|
|
@@ -216,21 +508,120 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
|
|
|
216
508
|
narrativeRole: claim.kind === "risk" || claim.kind === "assumption" ? "risk" : claim.kind === "ask" ? "ask" : claim.kind === "recommendation" ? "recommendation" : "evidence",
|
|
217
509
|
layout: "two-col",
|
|
218
510
|
qa: true,
|
|
219
|
-
components: claimComponents(claim, bindings),
|
|
511
|
+
components: claimComponents(claim, bindings, visualIntent),
|
|
220
512
|
claimIds: [claim.id],
|
|
221
513
|
claimRefs: [{ claimId: claim.id, role: "primary", note: claimBoundaryNote(claim) }],
|
|
222
514
|
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
223
515
|
content: {
|
|
224
516
|
headline: claim.text,
|
|
225
517
|
bullets: claimBullets(claim, bindings),
|
|
518
|
+
data: { visualIntent },
|
|
519
|
+
},
|
|
520
|
+
evidence: bindings.map(evidenceRefFromBinding),
|
|
521
|
+
visuals: visualBriefs(index, visualIntent),
|
|
522
|
+
status: "planned",
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function claimChapterSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[], kind: ClaimChapterSlideKind, narrative: NarrativeStateV1): SlideSpec {
|
|
527
|
+
if (kind === "framing") return claimFramingSlide(index, claim, bindings)
|
|
528
|
+
if (kind === "evidence") return claimEvidenceSlide(index, claim, bindings)
|
|
529
|
+
return claimImplicationSlide(index, claim, bindings, narrative)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function claimFramingSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
533
|
+
const visualIntent = visualIntentForStructuralSlide("text-only", "Frame why this central claim deserves a chapter before proof detail; keep evidence boundaries visible.")
|
|
534
|
+
const notes = internalEvidenceDiagnostics(claim, bindings)
|
|
535
|
+
return {
|
|
536
|
+
index,
|
|
537
|
+
title: `${titleFromClaim(claim)} - framing`,
|
|
538
|
+
purpose: `Frame the audience context, decision relevance, and evidence boundary for central claim ${claim.id}.`,
|
|
539
|
+
narrativeRole: claim.kind === "problem" || claim.kind === "opportunity" ? "tension" : claim.kind === "context" ? "context" : "evidence",
|
|
540
|
+
layout: "two-col",
|
|
541
|
+
qa: true,
|
|
542
|
+
components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
|
|
543
|
+
claimIds: [claim.id],
|
|
544
|
+
claimRefs: [{ claimId: claim.id, role: "primary", note: claimBoundaryNote(claim) }],
|
|
545
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
546
|
+
content: {
|
|
547
|
+
headline: claim.text,
|
|
548
|
+
bullets: [
|
|
549
|
+
`Chapter claim: ${claim.text}`,
|
|
550
|
+
claim.supportedScope ? `What the available evidence supports: ${claim.supportedScope}` : undefined,
|
|
551
|
+
claim.evidenceRequired ? audienceEvidenceGapBullet(claim, bindings) ?? `Proof base: ${bindings.length} bound evidence item${bindings.length === 1 ? "" : "s"} supports this claim.` : "This framing claim does not require separate proof in the deck plan.",
|
|
552
|
+
].filter((item): item is string => Boolean(item)),
|
|
553
|
+
speakerNotes: notes,
|
|
554
|
+
data: { visualIntent, chapterSlide: "framing" },
|
|
226
555
|
},
|
|
227
556
|
evidence: bindings.map(evidenceRefFromBinding),
|
|
557
|
+
visuals: visualBriefs(index, visualIntent),
|
|
558
|
+
status: "planned",
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function claimEvidenceSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
563
|
+
const visualIntent = visualIntentForClaim(claim, bindings)
|
|
564
|
+
const notes = internalEvidenceDiagnostics(claim, bindings)
|
|
565
|
+
return {
|
|
566
|
+
index,
|
|
567
|
+
title: `${titleFromClaim(claim)} - proof`,
|
|
568
|
+
purpose: `Show the specific evidence, source trace, support scope, unsupported scope, caveat, and strength for central claim ${claim.id}.`,
|
|
569
|
+
narrativeRole: "evidence",
|
|
570
|
+
layout: "two-col",
|
|
571
|
+
qa: true,
|
|
572
|
+
components: claimComponents(claim, bindings, visualIntent),
|
|
573
|
+
claimIds: [claim.id],
|
|
574
|
+
claimRefs: [{ claimId: claim.id, role: "evidence", note: claimBoundaryNote(claim) }],
|
|
575
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
576
|
+
content: {
|
|
577
|
+
headline: claim.text,
|
|
578
|
+
bullets: claimBullets(claim, bindings),
|
|
579
|
+
speakerNotes: notes,
|
|
580
|
+
data: { visualIntent, chapterSlide: "evidence" },
|
|
581
|
+
},
|
|
582
|
+
evidence: bindings.map(evidenceRefFromBinding),
|
|
583
|
+
visuals: visualBriefs(index, visualIntent),
|
|
584
|
+
status: "planned",
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function claimImplicationSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[], narrative: NarrativeStateV1): SlideSpec {
|
|
589
|
+
const relatedRisks = narrative.risks.filter((risk) => risk.claimId === claim.id)
|
|
590
|
+
const relatedObjections = narrative.objections.filter((objection) => objection.claimId === claim.id)
|
|
591
|
+
const visualIntent = visualIntentForClaimImplication(claim, relatedRisks.length + relatedObjections.length)
|
|
592
|
+
const notes = internalEvidenceDiagnostics(claim, bindings)
|
|
593
|
+
return {
|
|
594
|
+
index,
|
|
595
|
+
title: `${titleFromClaim(claim)} - implication`,
|
|
596
|
+
purpose: `Translate central claim ${claim.id} into decision implication while keeping boundaries and risks explicit.`,
|
|
597
|
+
narrativeRole: claim.kind === "recommendation" || claim.kind === "ask" ? "recommendation" : relatedRisks.length > 0 || relatedObjections.length > 0 ? "risk" : "evidence",
|
|
598
|
+
layout: "two-col",
|
|
599
|
+
qa: true,
|
|
600
|
+
components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
|
|
601
|
+
claimIds: [claim.id],
|
|
602
|
+
claimRefs: [{ claimId: claim.id, role: relatedRisks.length > 0 || relatedObjections.length > 0 ? "risk" : "supporting", note: claimBoundaryNote(claim) }],
|
|
603
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
604
|
+
content: {
|
|
605
|
+
headline: `Decision implication: ${claim.text}`,
|
|
606
|
+
bullets: [
|
|
607
|
+
...claimBoundaryBullets(claim),
|
|
608
|
+
...relatedRisks.slice(0, 2).map((risk) => risk.mitigation ? `Risk: ${risk.text} Mitigation: ${risk.mitigation}` : `Risk: ${risk.text}`),
|
|
609
|
+
...relatedObjections.slice(0, 2).map((objection) => objection.response ? `Objection: ${objection.text} Response: ${objection.response}` : `Objection: ${objection.text}`),
|
|
610
|
+
relatedRisks.length === 0 && relatedObjections.length === 0 && !claim.unsupportedScope && (claim.caveats ?? []).length === 0 ? "Decision use: no specific limiting condition is recorded for this central claim." : undefined,
|
|
611
|
+
].filter((item): item is string => Boolean(item)),
|
|
612
|
+
speakerNotes: notes,
|
|
613
|
+
data: { visualIntent, chapterSlide: "implication" },
|
|
614
|
+
},
|
|
615
|
+
evidence: bindings.map(evidenceRefFromBinding),
|
|
616
|
+
visuals: visualBriefs(index, visualIntent),
|
|
228
617
|
status: "planned",
|
|
229
618
|
}
|
|
230
619
|
}
|
|
231
620
|
|
|
232
621
|
function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
|
|
233
622
|
const supportingBindings = claims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
|
|
623
|
+
const visualIntent = visualIntentForSupportingLogic(claims, supportingBindings)
|
|
624
|
+
const notes = claims.map((claim) => internalEvidenceDiagnostics(claim, evidenceByClaim.get(claim.id) ?? [])).filter(Boolean).join("\n\n") || undefined
|
|
234
625
|
return {
|
|
235
626
|
index,
|
|
236
627
|
title: "Supporting Logic",
|
|
@@ -238,15 +629,18 @@ function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceB
|
|
|
238
629
|
narrativeRole: "evidence",
|
|
239
630
|
layout: "card-grid",
|
|
240
631
|
qa: true,
|
|
241
|
-
components: ["box", "text-panel"],
|
|
632
|
+
components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
|
|
242
633
|
claimIds: claims.map((claim) => claim.id),
|
|
243
634
|
claimRefs: claims.map((claim) => ({ claimId: claim.id, role: "supporting" as const, note: claimBoundaryNote(claim) })),
|
|
244
635
|
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
245
636
|
content: {
|
|
246
637
|
headline: "Supporting claims and boundaries",
|
|
247
|
-
bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim),
|
|
638
|
+
bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim), audienceEvidenceGapBullet(claim, evidenceByClaim.get(claim.id) ?? [])]).filter((item): item is string => Boolean(item)).slice(0, 8),
|
|
639
|
+
speakerNotes: notes,
|
|
640
|
+
data: { visualIntent },
|
|
248
641
|
},
|
|
249
642
|
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
643
|
+
visuals: visualBriefs(index, visualIntent),
|
|
250
644
|
status: "planned",
|
|
251
645
|
}
|
|
252
646
|
}
|
|
@@ -258,6 +652,7 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1, evidence
|
|
|
258
652
|
]
|
|
259
653
|
const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
|
|
260
654
|
const challengedBindings = challengedClaimIds.flatMap((claimId) => evidenceByClaim.get(claimId) ?? [])
|
|
655
|
+
const visualIntent = visualIntentForRiskObjection(narrative, challengedBindings)
|
|
261
656
|
return {
|
|
262
657
|
index,
|
|
263
658
|
title: "Risks And Objections",
|
|
@@ -265,7 +660,7 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1, evidence
|
|
|
265
660
|
narrativeRole: "risk",
|
|
266
661
|
layout: "two-col",
|
|
267
662
|
qa: true,
|
|
268
|
-
components: ["box", "text-panel"],
|
|
663
|
+
components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
|
|
269
664
|
claimIds: challengedClaimIds,
|
|
270
665
|
claimRefs: dedupeClaimRefs(challengedClaimRefs),
|
|
271
666
|
evidenceBindingIds: challengedBindings.map((binding) => binding.id),
|
|
@@ -275,14 +670,17 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1, evidence
|
|
|
275
670
|
...narrative.risks.slice(0, 3).map((risk) => risk.mitigation ? `${risk.text} Mitigation: ${risk.mitigation}` : risk.text),
|
|
276
671
|
...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
|
|
277
672
|
],
|
|
673
|
+
data: { visualIntent },
|
|
278
674
|
},
|
|
279
675
|
evidence: challengedBindings.map(evidenceRefFromBinding),
|
|
676
|
+
visuals: visualBriefs(index, visualIntent),
|
|
280
677
|
status: "planned",
|
|
281
678
|
}
|
|
282
679
|
}
|
|
283
680
|
|
|
284
681
|
function decisionAskSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
285
682
|
const askClaims = orderedClaims(narrative, (claim) => claim.kind === "ask" || claim.kind === "recommendation")
|
|
683
|
+
const visualIntent = visualIntentForStructuralSlide("steps", "Show the requested decision, owner, deadline, and consequence as an action sequence rather than a dense closing paragraph.")
|
|
286
684
|
return {
|
|
287
685
|
index,
|
|
288
686
|
title: "Decision Ask",
|
|
@@ -300,8 +698,10 @@ function decisionAskSlide(index: number, narrative: NarrativeStateV1): SlideSpec
|
|
|
300
698
|
narrative.decision.deadline ? `Deadline: ${narrative.decision.deadline}` : undefined,
|
|
301
699
|
narrative.decision.consequenceOfNoDecision ? `If no decision: ${narrative.decision.consequenceOfNoDecision}` : undefined,
|
|
302
700
|
].filter((item): item is string => Boolean(item)),
|
|
701
|
+
data: { visualIntent },
|
|
303
702
|
},
|
|
304
703
|
evidence: [],
|
|
704
|
+
visuals: visualBriefs(index, visualIntent),
|
|
305
705
|
status: "planned",
|
|
306
706
|
}
|
|
307
707
|
}
|
|
@@ -332,20 +732,27 @@ function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeCla
|
|
|
332
732
|
const claims = [...centralClaims, ...supportingClaims]
|
|
333
733
|
const chapters: DeckPlanChapter[] = []
|
|
334
734
|
addChapter(chapters, narrative.audience.decisionContext ? "Decision context" : "Context and belief shift", "context")
|
|
335
|
-
|
|
336
|
-
if (claims.some((claim) => claim.kind === "evidence") || narrative.evidenceBindings.length > 0) addChapter(chapters, "Evidence and proof", "evidence")
|
|
337
|
-
if (claims.some((claim) => claim.kind === "recommendation" || claim.kind === "ask")
|
|
735
|
+
for (const claim of centralClaims) addClaimChapter(chapters, claim)
|
|
736
|
+
if (supportingClaims.length > 0 || centralClaims.length === 0 && (claims.some((claim) => claim.kind === "evidence") || narrative.evidenceBindings.length > 0)) addChapter(chapters, "Evidence and proof", "evidence")
|
|
737
|
+
if (centralClaims.length === 0 && claims.some((claim) => claim.kind === "recommendation" || claim.kind === "ask")) addChapter(chapters, "Recommendation and decision", "recommendation")
|
|
338
738
|
if (narrative.risks.length > 0 || narrative.objections.length > 0 || centralClaims.some((claim) => claim.unsupportedScope || (claim.caveats ?? []).length > 0)) addChapter(chapters, "Risks and boundaries", "risk")
|
|
339
739
|
addChapter(chapters, "Decision ask", "ask")
|
|
340
740
|
if (chapters.length < 3) addChapter(chapters, "Evidence and proof", "evidence")
|
|
341
741
|
while (chapters.length > 5) {
|
|
342
|
-
const
|
|
343
|
-
if (
|
|
344
|
-
else
|
|
742
|
+
const removableIndex = chapters.findIndex((chapter) => !chapter.sourceClaimId && chapter.role !== "context" && chapter.role !== "ask")
|
|
743
|
+
if (removableIndex >= 0) chapters.splice(removableIndex, 1)
|
|
744
|
+
else break
|
|
345
745
|
}
|
|
346
746
|
return chapters
|
|
347
747
|
}
|
|
348
748
|
|
|
749
|
+
function addClaimChapter(chapters: DeckPlanChapter[], claim: NarrativeClaim): void {
|
|
750
|
+
const role = chapterRoleForClaim(claim)
|
|
751
|
+
const title = claimChapterTitle(claim)
|
|
752
|
+
if (chapters.some((chapter) => chapter.sourceClaimId === claim.id)) return
|
|
753
|
+
chapters.push({ title, role, slideIndexes: [], claimIds: [], evidenceBindingIds: [], sourceClaimId: claim.id })
|
|
754
|
+
}
|
|
755
|
+
|
|
349
756
|
function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanChapter["role"]): void {
|
|
350
757
|
if (chapters.some((chapter) => chapter.role === role || chapter.title === title)) return
|
|
351
758
|
chapters.push({ title, role, slideIndexes: [], claimIds: [], evidenceBindingIds: [] })
|
|
@@ -354,6 +761,10 @@ function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanCh
|
|
|
354
761
|
function assignSlideToChapter(chapters: DeckPlanChapter[], role: DeckPlanChapter["role"], slide: SlideSpec): void {
|
|
355
762
|
const chapter = chapters.find((item) => item.role === role) ?? chapters.find((item) => item.role === "evidence") ?? chapters[chapters.length - 1]
|
|
356
763
|
if (!chapter) return
|
|
764
|
+
assignSlideToSpecificChapter(chapter, slide)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function assignSlideToSpecificChapter(chapter: DeckPlanChapter, slide: SlideSpec): void {
|
|
357
768
|
chapter.slideIndexes.push(slide.index)
|
|
358
769
|
for (const claimId of slide.claimIds ?? []) addUnique(chapter.claimIds, claimId)
|
|
359
770
|
for (const ref of slide.claimRefs ?? []) addUnique(chapter.claimIds, ref.claimId)
|
|
@@ -377,30 +788,153 @@ function hasClaimKind(claims: NarrativeClaim[], kinds: NarrativeClaim["kind"][])
|
|
|
377
788
|
return claims.some((claim) => kinds.includes(claim.kind))
|
|
378
789
|
}
|
|
379
790
|
|
|
380
|
-
function claimComponents(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
|
|
381
|
-
|
|
382
|
-
if (claim.kind === "recommendation" || claim.kind === "ask") return ["box", "text-panel", "steps"]
|
|
383
|
-
return
|
|
791
|
+
function claimComponents(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[], visualIntent: VisualIntent): string[] {
|
|
792
|
+
const base = bindings.some((binding) => binding.quote?.trim()) ? ["box", "text-panel", "quote"] : ["box", "text-panel"]
|
|
793
|
+
if ((claim.kind === "recommendation" || claim.kind === "ask") && visualIntent.kind === "text-only") return ["box", "text-panel", "steps"]
|
|
794
|
+
return componentsForVisualIntent(base, visualIntent)
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function visualIntentForClaim(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): VisualIntent {
|
|
798
|
+
const evidenceBindingIds = bindings.map((binding) => binding.id)
|
|
799
|
+
const dataSignals = dataSignalsFromBindings(bindings)
|
|
800
|
+
if (bindings.length >= 2) {
|
|
801
|
+
return {
|
|
802
|
+
kind: "evidence-table",
|
|
803
|
+
component: "data-table",
|
|
804
|
+
rationale: "Compare multiple evidence bindings with source, support scope, and caveat columns so the slide is not a bullet stack.",
|
|
805
|
+
dataSignals,
|
|
806
|
+
evidenceBindingIds,
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
if (dataSignals.length > 0) {
|
|
810
|
+
return {
|
|
811
|
+
kind: "metric-stat",
|
|
812
|
+
component: "stat-card",
|
|
813
|
+
rationale: "Promote the strongest quantitative evidence signal into a metric card, with the source quote retained for traceability.",
|
|
814
|
+
dataSignals,
|
|
815
|
+
evidenceBindingIds,
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (claim.kind === "recommendation" || claim.kind === "ask") {
|
|
819
|
+
return {
|
|
820
|
+
kind: "steps",
|
|
821
|
+
component: "steps",
|
|
822
|
+
rationale: "Show the recommendation as phased actions or decision gates rather than a paragraph.",
|
|
823
|
+
dataSignals,
|
|
824
|
+
evidenceBindingIds,
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return {
|
|
828
|
+
kind: "text-only",
|
|
829
|
+
component: "box",
|
|
830
|
+
rationale: "No quantified or multi-source visual signal is available; use semantic evidence boxes and keep boundaries explicit.",
|
|
831
|
+
dataSignals,
|
|
832
|
+
evidenceBindingIds,
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function visualIntentForSupportingLogic(claims: NarrativeClaim[], bindings: NarrativeEvidenceBinding[]): VisualIntent {
|
|
837
|
+
const dataSignals = dataSignalsFromBindings(bindings)
|
|
838
|
+
return {
|
|
839
|
+
kind: claims.length >= 3 || bindings.length >= 2 ? "comparison-grid" : "evidence-table",
|
|
840
|
+
component: "data-table",
|
|
841
|
+
rationale: "Organize supporting claims as a comparison grid with evidence status and boundaries, avoiding a long undifferentiated bullet list.",
|
|
842
|
+
dataSignals,
|
|
843
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function visualIntentForRiskObjection(narrative: NarrativeStateV1, bindings: NarrativeEvidenceBinding[]): VisualIntent {
|
|
848
|
+
return {
|
|
849
|
+
kind: "risk-matrix",
|
|
850
|
+
component: "data-table",
|
|
851
|
+
rationale: "Pair each risk or objection with mitigation or response in a compact matrix so caveats stay visible without becoming prose-heavy.",
|
|
852
|
+
dataSignals: [...narrative.risks.map((risk) => risk.severity), ...narrative.objections.map((objection) => objection.priority)].filter(Boolean),
|
|
853
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function visualIntentForStructuralSlide(kind: Extract<VisualIntentKind, "hero" | "toc" | "steps" | "text-only">, rationale: string): VisualIntent {
|
|
858
|
+
return { kind, component: kind === "toc" ? "toc" : kind === "steps" ? "steps" : kind === "text-only" ? "box" : "hero", rationale, dataSignals: [], evidenceBindingIds: [] }
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function visualIntentForClaimImplication(claim: NarrativeClaim, relatedBoundaryCount: number): VisualIntent {
|
|
862
|
+
if (claim.kind === "recommendation" || claim.kind === "ask") {
|
|
863
|
+
return {
|
|
864
|
+
kind: "steps",
|
|
865
|
+
component: "steps",
|
|
866
|
+
rationale: "Translate the chapter proof into concrete decision gates or actions while preserving evidence boundaries.",
|
|
867
|
+
dataSignals: [],
|
|
868
|
+
evidenceBindingIds: [],
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (relatedBoundaryCount > 0 || claim.unsupportedScope || (claim.caveats ?? []).length > 0) {
|
|
872
|
+
return {
|
|
873
|
+
kind: "risk-matrix",
|
|
874
|
+
component: "data-table",
|
|
875
|
+
rationale: "Show the decision implication alongside risks, objections, unsupported scope, and caveats so boundaries shape the recommendation.",
|
|
876
|
+
dataSignals: [],
|
|
877
|
+
evidenceBindingIds: [],
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return {
|
|
881
|
+
kind: "text-only",
|
|
882
|
+
component: "box",
|
|
883
|
+
rationale: "State the decision implication without inventing risk or ROI detail that is not present in the canonical narrative.",
|
|
884
|
+
dataSignals: [],
|
|
885
|
+
evidenceBindingIds: [],
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function componentsForVisualIntent(base: string[], visualIntent: VisualIntent): string[] {
|
|
890
|
+
const next = [...base]
|
|
891
|
+
if (visualIntent.component && !next.includes(visualIntent.component)) next.push(visualIntent.component)
|
|
892
|
+
return next
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function visualBriefs(slideIndex: number, visualIntent: VisualIntent): VisualBrief[] {
|
|
896
|
+
return [{
|
|
897
|
+
id: `visual:${slideIndex}:${visualIntent.kind}`,
|
|
898
|
+
purpose: visualIntent.kind,
|
|
899
|
+
brief: `${visualIntent.rationale} Use ${visualIntent.component} and preserve cited evidence boundaries${visualIntent.dataSignals.length > 0 ? `; visible signals: ${visualIntent.dataSignals.slice(0, 4).join(", ")}.` : "."}`,
|
|
900
|
+
}]
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
function dataSignalsFromBindings(bindings: NarrativeEvidenceBinding[]): string[] {
|
|
904
|
+
const signals = bindings.flatMap((binding) => numericSignals([binding.quote, binding.supportScope, binding.source, binding.location].filter(Boolean).join(" ")))
|
|
905
|
+
return [...new Set(signals)].slice(0, 6)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function numericSignals(text: string): string[] {
|
|
909
|
+
return [...text.matchAll(/(?:[$€£¥]\s*)?\d+(?:\.\d+)?\s*(?:%|bps|x|k|m|bn|billion|million|year|years|yr|yrs)?/gi)]
|
|
910
|
+
.map((match) => match[0].trim())
|
|
911
|
+
.filter(Boolean)
|
|
384
912
|
}
|
|
385
913
|
|
|
386
914
|
function claimBullets(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
|
|
387
915
|
return [
|
|
388
916
|
...claimBoundaryBullets(claim),
|
|
389
|
-
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence
|
|
390
|
-
|
|
917
|
+
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence points to: ${binding.supportScope}` : undefined),
|
|
918
|
+
audienceEvidenceGapBullet(claim, bindings),
|
|
391
919
|
].filter((item): item is string => Boolean(item))
|
|
392
920
|
}
|
|
393
921
|
|
|
394
|
-
function
|
|
922
|
+
function audienceEvidenceGapBullet(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string | undefined {
|
|
395
923
|
if (!claim.evidenceRequired || bindings.length > 0) return undefined
|
|
396
|
-
return
|
|
924
|
+
return claim.evidenceStatus === "missing"
|
|
925
|
+
? "Decision boundary: this point needs supporting proof before it can carry the recommendation."
|
|
926
|
+
: "Decision boundary: current support is incomplete, so use this point as a qualified signal rather than a proven conclusion."
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function isAudienceEvidenceGapBullet(text: string): boolean {
|
|
930
|
+
return text.startsWith("Decision boundary:") && (text.includes("supporting proof") || text.includes("qualified signal"))
|
|
397
931
|
}
|
|
398
932
|
|
|
399
933
|
function claimBoundaryBullets(claim: NarrativeClaim): string[] {
|
|
400
934
|
return [
|
|
401
|
-
claim.supportedScope ? `
|
|
402
|
-
claim.unsupportedScope ? `
|
|
403
|
-
...(claim.caveats ?? []).map((caveat) => `
|
|
935
|
+
claim.supportedScope ? `What the available evidence supports: ${claim.supportedScope}` : undefined,
|
|
936
|
+
claim.unsupportedScope ? `What this does not yet prove: ${claim.unsupportedScope}` : undefined,
|
|
937
|
+
...(claim.caveats ?? []).map((caveat) => `Use with caution: ${caveat}`),
|
|
404
938
|
].filter((item): item is string => Boolean(item))
|
|
405
939
|
}
|
|
406
940
|
|
|
@@ -409,6 +943,18 @@ function claimBoundaryNote(claim: NarrativeClaim): string | undefined {
|
|
|
409
943
|
return notes.length > 0 ? notes.join(" ") : undefined
|
|
410
944
|
}
|
|
411
945
|
|
|
946
|
+
function internalEvidenceDiagnostics(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string | undefined {
|
|
947
|
+
const lines = [
|
|
948
|
+
claim.supportedScope ? `Supported scope: ${claim.supportedScope}` : undefined,
|
|
949
|
+
claim.unsupportedScope ? `Unsupported scope: ${claim.unsupportedScope}` : undefined,
|
|
950
|
+
...(claim.caveats ?? []).map((caveat) => `Caveat: ${caveat}`),
|
|
951
|
+
!claim.evidenceRequired ? undefined : bindings.length === 0 ? `Evidence gap: ${claim.evidenceStatus === "missing" ? "no binding yet" : "support remains incomplete"}.` : undefined,
|
|
952
|
+
...bindings.map((binding) => binding.caveat ? `Evidence ${binding.id} caveat: ${binding.caveat}` : undefined),
|
|
953
|
+
...bindings.map((binding) => binding.unsupportedScope ? `Evidence ${binding.id} unsupported scope: ${binding.unsupportedScope}` : undefined),
|
|
954
|
+
].filter((item): item is string => Boolean(item))
|
|
955
|
+
return lines.length > 0 ? `Internal evidence diagnostics for author/reviewer use only. Do not render these labels as executive-facing body copy.\n${lines.map((line) => `- ${line}`).join("\n")}` : undefined
|
|
956
|
+
}
|
|
957
|
+
|
|
412
958
|
function dedupeClaimRefs<T extends { claimId: string; role: "risk" | "objection" }>(refs: T[]): T[] {
|
|
413
959
|
const seen = new Set<string>()
|
|
414
960
|
return refs.filter((ref) => {
|
|
@@ -435,6 +981,24 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
435
981
|
const coverage = deckPlanCoverage(narrative, slides)
|
|
436
982
|
const centralClaimIds = narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id)
|
|
437
983
|
const missingCentralClaims = centralClaimIds.filter((claimId) => coverage.missingClaimIds.includes(claimId))
|
|
984
|
+
const claimChapters = chapters.filter((chapter) => chapter.sourceClaimId)
|
|
985
|
+
const centralClaimsWithoutChapter = centralClaimIds.filter((claimId) => !claimChapters.some((chapter) => chapter.sourceClaimId === claimId))
|
|
986
|
+
const thinClaimChapters = claimChapters.filter((chapter) => nonStructuralClaimSlides(chapter, slides).length < 3)
|
|
987
|
+
const evidenceInvisibleChapters = claimChapters.filter((chapter) => {
|
|
988
|
+
const claim = narrative.claims.find((item) => item.id === chapter.sourceClaimId)
|
|
989
|
+
if (!claim?.evidenceRequired) return false
|
|
990
|
+
return chapter.evidenceBindingIds.length === 0 && !nonStructuralClaimSlides(chapter, slides).some((slide) => (slide.content.bullets ?? []).some((bullet) => isAudienceEvidenceGapBullet(bullet)))
|
|
991
|
+
})
|
|
992
|
+
const paddedClaimChapters = claimChapters.filter((chapter) => nonStructuralClaimSlides(chapter, slides).some((slide) => isFillerSlide(slide)))
|
|
993
|
+
const boundaryMissingChapters = claimChapters.filter((chapter) => {
|
|
994
|
+
const claim = narrative.claims.find((item) => item.id === chapter.sourceClaimId)
|
|
995
|
+
if (!claim) return false
|
|
996
|
+
const hasBoundary = Boolean(claim.unsupportedScope || (claim.caveats ?? []).length > 0 || narrative.risks.some((risk) => risk.claimId === claim.id) || narrative.objections.some((objection) => objection.claimId === claim.id))
|
|
997
|
+
if (!hasBoundary) return false
|
|
998
|
+
const text = nonStructuralClaimSlides(chapter, slides).flatMap((slide) => [slide.content.headline, ...(slide.content.bullets ?? [])]).join("\n")
|
|
999
|
+
const claimBoundaryVisible = [claim.unsupportedScope, ...(claim.caveats ?? [])].filter(Boolean).some((boundary) => text.includes(boundary!))
|
|
1000
|
+
return !(claimBoundaryVisible || text.includes("Risk:") || text.includes("Objection:"))
|
|
1001
|
+
})
|
|
438
1002
|
const incompatibleComponents = [...new Set(slides.flatMap((slide) => slide.components).filter((component) => component === "card"))]
|
|
439
1003
|
const toc = slides.find((slide) => slide.components.includes("toc"))
|
|
440
1004
|
const tocBullets = toc?.content.bullets ?? []
|
|
@@ -444,6 +1008,31 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
444
1008
|
const risksOrObjectionsVisible = narrative.risks.length === 0 && narrative.objections.length === 0 || slides.some((slide) => slide.narrativeRole === "risk")
|
|
445
1009
|
|
|
446
1010
|
return [
|
|
1011
|
+
{
|
|
1012
|
+
id: "claim_chapters_present",
|
|
1013
|
+
status: centralClaimsWithoutChapter.length === 0 ? "pass" : "blocker",
|
|
1014
|
+
message: centralClaimsWithoutChapter.length === 0 ? "Every central claim has a deterministic claim-led chapter." : `Central claims missing claim-led chapters: ${centralClaimsWithoutChapter.join(", ")}`,
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
id: "claim_chapters_min_three_slides",
|
|
1018
|
+
status: thinClaimChapters.length === 0 ? "pass" : "blocker",
|
|
1019
|
+
message: thinClaimChapters.length === 0 ? "Every central claim chapter has at least three non-structural slides: framing, proof, and implication/boundary." : `Central claim chapters need at least three non-structural slides: ${thinClaimChapters.map((chapter) => chapter.sourceClaimId).join(", ")}`,
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
id: "claim_chapter_evidence_visible",
|
|
1023
|
+
status: evidenceInvisibleChapters.length === 0 ? "pass" : "blocker",
|
|
1024
|
+
message: evidenceInvisibleChapters.length === 0 ? "Evidence-required claim chapters show bound evidence or an explicit evidence gap." : `Evidence-required claim chapters hide evidence gaps: ${evidenceInvisibleChapters.map((chapter) => chapter.sourceClaimId).join(", ")}`,
|
|
1025
|
+
},
|
|
1026
|
+
{
|
|
1027
|
+
id: "claim_chapter_not_padded_by_filler",
|
|
1028
|
+
status: paddedClaimChapters.length === 0 ? "pass" : "blocker",
|
|
1029
|
+
message: paddedClaimChapters.length === 0 ? "Claim chapters may use structural TOC dividers, but are not padded by placeholder, repeated thesis, or generic bridge slides." : `Claim chapters contain filler slides: ${paddedClaimChapters.map((chapter) => chapter.sourceClaimId).join(", ")}`,
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
id: "claim_chapter_boundary_visible",
|
|
1033
|
+
status: boundaryMissingChapters.length === 0 ? "pass" : "blocker",
|
|
1034
|
+
message: boundaryMissingChapters.length === 0 ? "Central claim chapter boundaries, risks, objections, and caveats remain visible when recorded." : `Central claim chapters hide recorded boundaries: ${boundaryMissingChapters.map((chapter) => chapter.sourceClaimId).join(", ")}`,
|
|
1035
|
+
},
|
|
447
1036
|
{
|
|
448
1037
|
id: "chapter_structure_present",
|
|
449
1038
|
status: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? "pass" : "blocker",
|
|
@@ -492,6 +1081,17 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
492
1081
|
]
|
|
493
1082
|
}
|
|
494
1083
|
|
|
1084
|
+
function nonStructuralClaimSlides(chapter: DeckPlanChapter, slides: SlideSpec[]): SlideSpec[] {
|
|
1085
|
+
const indexes = new Set(chapter.slideIndexes)
|
|
1086
|
+
return slides.filter((slide) => indexes.has(slide.index) && slide.qa && slide.title !== "Decision Ask" && !slide.components.includes("toc"))
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
function isFillerSlide(slide: SlideSpec): boolean {
|
|
1090
|
+
const text = `${slide.title}\n${slide.purpose}\n${slide.content.headline}\n${(slide.content.bullets ?? []).join("\n")}`.toLowerCase()
|
|
1091
|
+
if (slide.components.includes("hero") && slide.qa) return true
|
|
1092
|
+
return ["section divider", "bridge slide", "repeat thesis", "repeated thesis", "generic overview"].some((marker) => text.includes(marker))
|
|
1093
|
+
}
|
|
1094
|
+
|
|
495
1095
|
function deckPlanCoverage(narrative: NarrativeStateV1, slides: SlideSpec[]): { requiredClaimIds: string[]; coveredClaimIds: string[]; missingClaimIds: string[] } {
|
|
496
1096
|
const requiredClaimIds = narrative.claims
|
|
497
1097
|
.filter((claim) => claim.importance === "central" || claim.evidenceRequired)
|
|
@@ -511,6 +1111,11 @@ function titleFromClaim(claim: NarrativeClaim): string {
|
|
|
511
1111
|
return words || claim.kind
|
|
512
1112
|
}
|
|
513
1113
|
|
|
1114
|
+
function claimChapterTitle(claim: NarrativeClaim): string {
|
|
1115
|
+
const title = titleFromClaim(claim)
|
|
1116
|
+
return title.endsWith(".") ? title.slice(0, -1) : title
|
|
1117
|
+
}
|
|
1118
|
+
|
|
514
1119
|
function hasCurrentApprovalOrOverride(narrative: NarrativeStateV1, narrativeHash: string): boolean {
|
|
515
1120
|
return narrative.approvals.some((approval) => approval.narrativeHash === narrativeHash && (approval.scope === "narrative" && approval.approvedBy === "user" || approval.scope === "render_override" || approval.approvedBy === "override"))
|
|
516
1121
|
}
|