@cyber-dash-tech/revela 0.17.1 → 0.17.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 +47 -565
- package/README.zh-CN.md +49 -533
- 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 +6 -6
- package/lib/commands/init.ts +19 -9
- package/lib/commands/narrative.ts +40 -19
- 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 +79 -95
- 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/narrative-state/deck-plan-artifact.ts +584 -0
- package/lib/narrative-state/render-plan.ts +527 -38
- 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/refine/open.ts +2 -2
- package/package.json +1 -1
- package/plugin.ts +2 -2
- package/skill/NARRATIVE_SKILL.md +23 -20
- package/skill/SKILL.md +95 -36
- package/tools/decks.ts +83 -51
- package/tools/narrative-view.ts +8 -10
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deckPlanHash, upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec, type VisualBrief } 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,7 +64,54 @@ 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
|
+
|
|
38
113
|
type VisualIntentKind = "hero" | "toc" | "metric-stat" | "evidence-table" | "comparison-grid" | "risk-matrix" | "steps" | "text-only"
|
|
114
|
+
type ClaimChapterSlideKind = "framing" | "evidence" | "implication"
|
|
39
115
|
|
|
40
116
|
interface VisualIntent {
|
|
41
117
|
kind: VisualIntentKind
|
|
@@ -66,10 +142,8 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
66
142
|
const deckKey = state.activeDeck ?? Object.keys(state.decks)[0]
|
|
67
143
|
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
68
144
|
const slug = deck?.slug ?? state.activeDeck ?? "deck"
|
|
69
|
-
const
|
|
70
|
-
const
|
|
71
|
-
const qualityChecks = checkPlanQuality(narrative, slides, chapters)
|
|
72
|
-
const planCoverage = deckPlanCoverage(narrative, slides)
|
|
145
|
+
const planningPacket = buildDeckPlanningPacket(narrative, narrativeHash, deck?.outputPath ?? `decks/${slug}.html`)
|
|
146
|
+
const deckPlanRequirements = buildDeckPlanRequirements(narrativeHash)
|
|
73
147
|
const requiredInputs: Partial<RequiredInputs> = {
|
|
74
148
|
topicClarified: true,
|
|
75
149
|
audienceClarified: Boolean(narrative.audience.primary),
|
|
@@ -93,13 +167,12 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
93
167
|
} as RequiredInputs,
|
|
94
168
|
writeReadiness: deck?.writeReadiness ?? { status: "blocked" as const, blockers: [] },
|
|
95
169
|
})
|
|
96
|
-
next = upsertSlides(next, slug, slides)
|
|
97
170
|
const plannedDeck = next.decks[slug]
|
|
98
171
|
plannedDeck.planReview = {
|
|
99
172
|
status: "pending",
|
|
100
173
|
narrativeHash,
|
|
101
|
-
planHash:
|
|
102
|
-
qualityChecks,
|
|
174
|
+
planHash: "pending-deck-plan-md",
|
|
175
|
+
qualityChecks: [],
|
|
103
176
|
}
|
|
104
177
|
plannedDeck.requiredInputs = { ...plannedDeck.requiredInputs, slidePlanConfirmed: false }
|
|
105
178
|
plannedDeck.writeReadiness = { status: "blocked", blockers: [] }
|
|
@@ -109,13 +182,13 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
109
182
|
if (htmlTarget) {
|
|
110
183
|
htmlTarget.data = {
|
|
111
184
|
...(htmlTarget.data ?? {}),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
requiredClaimIds:
|
|
117
|
-
coveredClaimIds:
|
|
118
|
-
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),
|
|
119
192
|
claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
|
|
120
193
|
claimId: ref.claimId,
|
|
121
194
|
claimText: ref.claimText,
|
|
@@ -134,14 +207,169 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
134
207
|
compiled: true,
|
|
135
208
|
skipped: false,
|
|
136
209
|
narrativeHash,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
210
|
+
planArtifactPath: "deck-plan/index.md",
|
|
211
|
+
slideCount: 0,
|
|
212
|
+
slides: [],
|
|
213
|
+
chapters: [],
|
|
214
|
+
qualityChecks: [],
|
|
215
|
+
planningPacket,
|
|
216
|
+
deckPlanRequirements,
|
|
141
217
|
},
|
|
142
218
|
}
|
|
143
219
|
}
|
|
144
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
|
+
|
|
145
373
|
function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chapters: DeckPlanChapter[] } {
|
|
146
374
|
const slides: SlideSpec[] = []
|
|
147
375
|
const evidenceByClaim = evidenceBindingsByClaim(narrative.evidenceBindings)
|
|
@@ -154,9 +382,18 @@ function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chap
|
|
|
154
382
|
slides.push(tocSlide(slides.length + 1, chapters))
|
|
155
383
|
|
|
156
384
|
for (const claim of centralClaims) {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
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
|
+
}
|
|
160
397
|
}
|
|
161
398
|
if (supportingClaims.length > 0) {
|
|
162
399
|
const slide = supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim)
|
|
@@ -223,6 +460,45 @@ function tocSlide(index: number, chapters: DeckPlanChapter[]): SlideSpec {
|
|
|
223
460
|
}
|
|
224
461
|
}
|
|
225
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
|
+
|
|
226
502
|
function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
227
503
|
const visualIntent = visualIntentForClaim(claim, bindings)
|
|
228
504
|
return {
|
|
@@ -247,9 +523,105 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
|
|
|
247
523
|
}
|
|
248
524
|
}
|
|
249
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" },
|
|
555
|
+
},
|
|
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),
|
|
617
|
+
status: "planned",
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
250
621
|
function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
|
|
251
622
|
const supportingBindings = claims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
|
|
252
623
|
const visualIntent = visualIntentForSupportingLogic(claims, supportingBindings)
|
|
624
|
+
const notes = claims.map((claim) => internalEvidenceDiagnostics(claim, evidenceByClaim.get(claim.id) ?? [])).filter(Boolean).join("\n\n") || undefined
|
|
253
625
|
return {
|
|
254
626
|
index,
|
|
255
627
|
title: "Supporting Logic",
|
|
@@ -263,7 +635,8 @@ function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceB
|
|
|
263
635
|
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
264
636
|
content: {
|
|
265
637
|
headline: "Supporting claims and boundaries",
|
|
266
|
-
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,
|
|
267
640
|
data: { visualIntent },
|
|
268
641
|
},
|
|
269
642
|
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
@@ -359,20 +732,27 @@ function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeCla
|
|
|
359
732
|
const claims = [...centralClaims, ...supportingClaims]
|
|
360
733
|
const chapters: DeckPlanChapter[] = []
|
|
361
734
|
addChapter(chapters, narrative.audience.decisionContext ? "Decision context" : "Context and belief shift", "context")
|
|
362
|
-
|
|
363
|
-
if (claims.some((claim) => claim.kind === "evidence") || narrative.evidenceBindings.length > 0) addChapter(chapters, "Evidence and proof", "evidence")
|
|
364
|
-
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")
|
|
365
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")
|
|
366
739
|
addChapter(chapters, "Decision ask", "ask")
|
|
367
740
|
if (chapters.length < 3) addChapter(chapters, "Evidence and proof", "evidence")
|
|
368
741
|
while (chapters.length > 5) {
|
|
369
|
-
const
|
|
370
|
-
if (
|
|
371
|
-
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
|
|
372
745
|
}
|
|
373
746
|
return chapters
|
|
374
747
|
}
|
|
375
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
|
+
|
|
376
756
|
function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanChapter["role"]): void {
|
|
377
757
|
if (chapters.some((chapter) => chapter.role === role || chapter.title === title)) return
|
|
378
758
|
chapters.push({ title, role, slideIndexes: [], claimIds: [], evidenceBindingIds: [] })
|
|
@@ -381,6 +761,10 @@ function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanCh
|
|
|
381
761
|
function assignSlideToChapter(chapters: DeckPlanChapter[], role: DeckPlanChapter["role"], slide: SlideSpec): void {
|
|
382
762
|
const chapter = chapters.find((item) => item.role === role) ?? chapters.find((item) => item.role === "evidence") ?? chapters[chapters.length - 1]
|
|
383
763
|
if (!chapter) return
|
|
764
|
+
assignSlideToSpecificChapter(chapter, slide)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function assignSlideToSpecificChapter(chapter: DeckPlanChapter, slide: SlideSpec): void {
|
|
384
768
|
chapter.slideIndexes.push(slide.index)
|
|
385
769
|
for (const claimId of slide.claimIds ?? []) addUnique(chapter.claimIds, claimId)
|
|
386
770
|
for (const ref of slide.claimRefs ?? []) addUnique(chapter.claimIds, ref.claimId)
|
|
@@ -470,8 +854,36 @@ function visualIntentForRiskObjection(narrative: NarrativeStateV1, bindings: Nar
|
|
|
470
854
|
}
|
|
471
855
|
}
|
|
472
856
|
|
|
473
|
-
function visualIntentForStructuralSlide(kind: Extract<VisualIntentKind, "hero" | "toc" | "steps">, rationale: string): VisualIntent {
|
|
474
|
-
return { kind, component: kind === "toc" ? "toc" : kind === "steps" ? "steps" : "hero", rationale, dataSignals: [], evidenceBindingIds: [] }
|
|
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
|
+
}
|
|
475
887
|
}
|
|
476
888
|
|
|
477
889
|
function componentsForVisualIntent(base: string[], visualIntent: VisualIntent): string[] {
|
|
@@ -502,21 +914,27 @@ function numericSignals(text: string): string[] {
|
|
|
502
914
|
function claimBullets(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
|
|
503
915
|
return [
|
|
504
916
|
...claimBoundaryBullets(claim),
|
|
505
|
-
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence
|
|
506
|
-
|
|
917
|
+
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence points to: ${binding.supportScope}` : undefined),
|
|
918
|
+
audienceEvidenceGapBullet(claim, bindings),
|
|
507
919
|
].filter((item): item is string => Boolean(item))
|
|
508
920
|
}
|
|
509
921
|
|
|
510
|
-
function
|
|
922
|
+
function audienceEvidenceGapBullet(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string | undefined {
|
|
511
923
|
if (!claim.evidenceRequired || bindings.length > 0) return undefined
|
|
512
|
-
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"))
|
|
513
931
|
}
|
|
514
932
|
|
|
515
933
|
function claimBoundaryBullets(claim: NarrativeClaim): string[] {
|
|
516
934
|
return [
|
|
517
|
-
claim.supportedScope ? `
|
|
518
|
-
claim.unsupportedScope ? `
|
|
519
|
-
...(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}`),
|
|
520
938
|
].filter((item): item is string => Boolean(item))
|
|
521
939
|
}
|
|
522
940
|
|
|
@@ -525,6 +943,18 @@ function claimBoundaryNote(claim: NarrativeClaim): string | undefined {
|
|
|
525
943
|
return notes.length > 0 ? notes.join(" ") : undefined
|
|
526
944
|
}
|
|
527
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
|
+
|
|
528
958
|
function dedupeClaimRefs<T extends { claimId: string; role: "risk" | "objection" }>(refs: T[]): T[] {
|
|
529
959
|
const seen = new Set<string>()
|
|
530
960
|
return refs.filter((ref) => {
|
|
@@ -551,6 +981,24 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
551
981
|
const coverage = deckPlanCoverage(narrative, slides)
|
|
552
982
|
const centralClaimIds = narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id)
|
|
553
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
|
+
})
|
|
554
1002
|
const incompatibleComponents = [...new Set(slides.flatMap((slide) => slide.components).filter((component) => component === "card"))]
|
|
555
1003
|
const toc = slides.find((slide) => slide.components.includes("toc"))
|
|
556
1004
|
const tocBullets = toc?.content.bullets ?? []
|
|
@@ -560,6 +1008,31 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
560
1008
|
const risksOrObjectionsVisible = narrative.risks.length === 0 && narrative.objections.length === 0 || slides.some((slide) => slide.narrativeRole === "risk")
|
|
561
1009
|
|
|
562
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
|
+
},
|
|
563
1036
|
{
|
|
564
1037
|
id: "chapter_structure_present",
|
|
565
1038
|
status: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? "pass" : "blocker",
|
|
@@ -608,6 +1081,17 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chap
|
|
|
608
1081
|
]
|
|
609
1082
|
}
|
|
610
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
|
+
|
|
611
1095
|
function deckPlanCoverage(narrative: NarrativeStateV1, slides: SlideSpec[]): { requiredClaimIds: string[]; coveredClaimIds: string[]; missingClaimIds: string[] } {
|
|
612
1096
|
const requiredClaimIds = narrative.claims
|
|
613
1097
|
.filter((claim) => claim.importance === "central" || claim.evidenceRequired)
|
|
@@ -627,6 +1111,11 @@ function titleFromClaim(claim: NarrativeClaim): string {
|
|
|
627
1111
|
return words || claim.kind
|
|
628
1112
|
}
|
|
629
1113
|
|
|
1114
|
+
function claimChapterTitle(claim: NarrativeClaim): string {
|
|
1115
|
+
const title = titleFromClaim(claim)
|
|
1116
|
+
return title.endsWith(".") ? title.slice(0, -1) : title
|
|
1117
|
+
}
|
|
1118
|
+
|
|
630
1119
|
function hasCurrentApprovalOrOverride(narrative: NarrativeStateV1, narrativeHash: string): boolean {
|
|
631
1120
|
return narrative.approvals.some((approval) => approval.narrativeHash === narrativeHash && (approval.scope === "narrative" && approval.approvedBy === "user" || approval.scope === "render_override" || approval.approvedBy === "override"))
|
|
632
1121
|
}
|