@cyber-dash-tech/revela 0.16.4 → 0.17.1

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.
Files changed (57) hide show
  1. package/README.md +7 -5
  2. package/README.zh-CN.md +7 -5
  3. package/lib/commands/brief.ts +9 -0
  4. package/lib/commands/help.ts +5 -2
  5. package/lib/commands/init.ts +42 -27
  6. package/lib/commands/narrative.ts +39 -6
  7. package/lib/commands/research.ts +36 -20
  8. package/lib/commands/review.ts +35 -28
  9. package/lib/ctx.ts +1 -1
  10. package/lib/decks-state.ts +38 -4
  11. package/lib/edit/prompt.ts +1 -1
  12. package/lib/hook-notifications.ts +53 -0
  13. package/lib/media/download.ts +23 -3
  14. package/lib/media/save.ts +1 -0
  15. package/lib/media/types.ts +1 -0
  16. package/lib/narrative-state/display.ts +74 -4
  17. package/lib/narrative-state/map-html.ts +242 -107
  18. package/lib/narrative-state/render-plan.ts +238 -35
  19. package/lib/narrative-state/research-binding-eval.ts +260 -0
  20. package/lib/narrative-state/research-gaps.ts +2 -88
  21. package/lib/narrative-vault/authoring-contract.ts +127 -0
  22. package/lib/narrative-vault/authoring-guard.ts +122 -0
  23. package/lib/narrative-vault/auto-compile.ts +134 -0
  24. package/lib/narrative-vault/bootstrap.ts +63 -0
  25. package/lib/narrative-vault/cache.ts +14 -0
  26. package/lib/narrative-vault/compile-mirror.ts +45 -0
  27. package/lib/narrative-vault/compile.ts +350 -0
  28. package/lib/narrative-vault/constants.ts +6 -0
  29. package/lib/narrative-vault/diagnostic-report.ts +117 -0
  30. package/lib/narrative-vault/export.ts +71 -0
  31. package/lib/narrative-vault/frontmatter.ts +41 -0
  32. package/lib/narrative-vault/hook-targets.ts +40 -0
  33. package/lib/narrative-vault/index.ts +18 -0
  34. package/lib/narrative-vault/inventory.ts +392 -0
  35. package/lib/narrative-vault/markdown-qa.ts +237 -0
  36. package/lib/narrative-vault/markdown.ts +34 -0
  37. package/lib/narrative-vault/migration.ts +52 -0
  38. package/lib/narrative-vault/mutate.ts +361 -0
  39. package/lib/narrative-vault/paths.ts +19 -0
  40. package/lib/narrative-vault/read.ts +52 -0
  41. package/lib/narrative-vault/relations.ts +32 -0
  42. package/lib/narrative-vault/source-loader.ts +19 -0
  43. package/lib/narrative-vault/timestamp.ts +32 -0
  44. package/lib/narrative-vault/types.ts +44 -0
  45. package/lib/qa/checks.ts +206 -5
  46. package/lib/qa/measure.ts +63 -1
  47. package/lib/refine/server.ts +157 -20
  48. package/lib/source-materials.ts +98 -0
  49. package/lib/tool-result.ts +34 -0
  50. package/package.json +2 -2
  51. package/plugin.ts +60 -22
  52. package/skill/NARRATIVE_SKILL.md +25 -10
  53. package/skill/SKILL.md +6 -1
  54. package/tools/decks.ts +363 -67
  55. package/tools/narrative-view.ts +16 -0
  56. package/tools/research-save.ts +3 -0
  57. package/tools/workspace-scan.ts +1 -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 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"
@@ -17,15 +17,34 @@ export interface CompileDeckPlanResult {
17
17
  narrativeHash: string
18
18
  slideCount: number
19
19
  slides: SlideSpec[]
20
+ chapters?: DeckPlanChapter[]
20
21
  qualityChecks?: DeckPlanQualityCheck[]
21
22
  }
22
23
 
24
+ export interface DeckPlanChapter {
25
+ title: string
26
+ role: "context" | "tension" | "evidence" | "recommendation" | "risk" | "ask"
27
+ slideIndexes: number[]
28
+ claimIds: string[]
29
+ evidenceBindingIds: string[]
30
+ }
31
+
23
32
  export interface DeckPlanQualityCheck {
24
33
  id: string
25
34
  status: "pass" | "warning" | "blocker"
26
35
  message: string
27
36
  }
28
37
 
38
+ type VisualIntentKind = "hero" | "toc" | "metric-stat" | "evidence-table" | "comparison-grid" | "risk-matrix" | "steps" | "text-only"
39
+
40
+ interface VisualIntent {
41
+ kind: VisualIntentKind
42
+ component: string
43
+ rationale: string
44
+ dataSignals: string[]
45
+ evidenceBindingIds: string[]
46
+ }
47
+
29
48
  export function compileDeckPlanFromNarrative(state: DecksState, options: CompileDeckPlanOptions = {}): { state: DecksState; result: CompileDeckPlanResult } {
30
49
  const narrative = normalizeNarrativeState(state)
31
50
  const narrativeHash = computeNarrativeHash(narrative)
@@ -47,8 +66,9 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
47
66
  const deckKey = state.activeDeck ?? Object.keys(state.decks)[0]
48
67
  const deck = deckKey ? state.decks[deckKey] : undefined
49
68
  const slug = deck?.slug ?? state.activeDeck ?? "deck"
50
- const slides = buildSlides(narrative)
51
- const qualityChecks = checkPlanQuality(narrative, slides)
69
+ const plan = buildDeckPlan(narrative)
70
+ const { slides, chapters } = plan
71
+ const qualityChecks = checkPlanQuality(narrative, slides, chapters)
52
72
  const planCoverage = deckPlanCoverage(narrative, slides)
53
73
  const requiredInputs: Partial<RequiredInputs> = {
54
74
  topicClarified: true,
@@ -91,8 +111,9 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
91
111
  ...(htmlTarget.data ?? {}),
92
112
  narrativeId: narrative.id,
93
113
  narrativeHash,
94
- planQualityChecks: qualityChecks,
95
- requiredClaimIds: planCoverage.requiredClaimIds,
114
+ planQualityChecks: qualityChecks,
115
+ planChapters: chapters,
116
+ requiredClaimIds: planCoverage.requiredClaimIds,
96
117
  coveredClaimIds: planCoverage.coveredClaimIds,
97
118
  missingClaimIds: planCoverage.missingClaimIds,
98
119
  claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
@@ -115,36 +136,49 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
115
136
  narrativeHash,
116
137
  slideCount: slides.length,
117
138
  slides,
139
+ chapters,
118
140
  qualityChecks,
119
141
  },
120
142
  }
121
143
  }
122
144
 
123
- function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
145
+ function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chapters: DeckPlanChapter[] } {
124
146
  const slides: SlideSpec[] = []
125
147
  const evidenceByClaim = evidenceBindingsByClaim(narrative.evidenceBindings)
126
148
  const centralClaims = orderedClaims(narrative, (claim) => claim.importance === "central")
127
149
  const supportingClaims = orderedClaims(narrative, (claim) => claim.importance !== "central")
128
- const chapters = deriveChapters(narrative, centralClaims, supportingClaims)
150
+ const chapters = deriveChapters(narrative, centralClaims, supportingClaims).map((chapter) => ({ ...chapter }))
129
151
 
130
152
  slides.push(coverSlide(slides.length + 1, narrative))
153
+ assignSlideToChapter(chapters, "context", slides[slides.length - 1])
131
154
  slides.push(tocSlide(slides.length + 1, chapters))
132
155
 
133
156
  for (const claim of centralClaims) {
134
- slides.push(claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? []))
157
+ const slide = claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? [])
158
+ slides.push(slide)
159
+ assignSlideToChapter(chapters, chapterRoleForClaim(claim), slide)
160
+ }
161
+ if (supportingClaims.length > 0) {
162
+ const slide = supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim)
163
+ slides.push(slide)
164
+ assignSlideToChapter(chapters, "evidence", slide)
135
165
  }
136
- if (supportingClaims.length > 0) slides.push(supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim))
137
166
 
138
167
  if (narrative.risks.length > 0 || narrative.objections.length > 0) {
139
- slides.push(riskObjectionSlide(slides.length + 1, narrative))
168
+ const slide = riskObjectionSlide(slides.length + 1, narrative, evidenceByClaim)
169
+ slides.push(slide)
170
+ assignSlideToChapter(chapters, "risk", slide)
140
171
  }
141
172
 
142
- slides.push(decisionAskSlide(slides.length + 1, narrative))
173
+ const decisionSlide = decisionAskSlide(slides.length + 1, narrative)
174
+ slides.push(decisionSlide)
175
+ assignSlideToChapter(chapters, "ask", decisionSlide)
143
176
 
144
- return slides
177
+ return { slides, chapters }
145
178
  }
146
179
 
147
180
  function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
181
+ const visualIntent = visualIntentForStructuralSlide("hero", "Use a hero frame to anchor the decision context and belief shift before evidence detail.")
148
182
  return {
149
183
  index,
150
184
  title: "Decision Context",
@@ -160,13 +194,16 @@ function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
160
194
  narrative.audience.beliefAfter ? `After: ${narrative.audience.beliefAfter}` : "After belief needs confirmation.",
161
195
  ],
162
196
  bullets: narrative.decision.action ? [`Decision: ${narrative.decision.action}`] : [],
197
+ data: { visualIntent },
163
198
  },
199
+ visuals: visualBriefs(index, visualIntent),
164
200
  evidence: [],
165
201
  status: "planned",
166
202
  }
167
203
  }
168
204
 
169
- function tocSlide(index: number, chapters: string[]): SlideSpec {
205
+ function tocSlide(index: number, chapters: DeckPlanChapter[]): SlideSpec {
206
+ const visualIntent = visualIntentForStructuralSlide("toc", "Render the chapter sequence as a visual table of contents instead of a bullet-only agenda.")
170
207
  return {
171
208
  index,
172
209
  title: "Storyline",
@@ -177,14 +214,17 @@ function tocSlide(index: number, chapters: string[]): SlideSpec {
177
214
  components: ["toc", "text-panel"],
178
215
  content: {
179
216
  headline: "How the decision story is organized",
180
- bullets: chapters,
217
+ bullets: chapters.map((chapter) => chapter.title),
218
+ data: { chapters: chapters.map((chapter) => ({ title: chapter.title, role: chapter.role })), visualIntent },
181
219
  },
220
+ visuals: visualBriefs(index, visualIntent),
182
221
  evidence: [],
183
222
  status: "planned",
184
223
  }
185
224
  }
186
225
 
187
226
  function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
227
+ const visualIntent = visualIntentForClaim(claim, bindings)
188
228
  return {
189
229
  index,
190
230
  title: titleFromClaim(claim),
@@ -192,21 +232,24 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
192
232
  narrativeRole: claim.kind === "risk" || claim.kind === "assumption" ? "risk" : claim.kind === "ask" ? "ask" : claim.kind === "recommendation" ? "recommendation" : "evidence",
193
233
  layout: "two-col",
194
234
  qa: true,
195
- components: claimComponents(claim, bindings),
235
+ components: claimComponents(claim, bindings, visualIntent),
196
236
  claimIds: [claim.id],
197
237
  claimRefs: [{ claimId: claim.id, role: "primary", note: claimBoundaryNote(claim) }],
198
238
  evidenceBindingIds: bindings.map((binding) => binding.id),
199
239
  content: {
200
240
  headline: claim.text,
201
241
  bullets: claimBullets(claim, bindings),
242
+ data: { visualIntent },
202
243
  },
203
244
  evidence: bindings.map(evidenceRefFromBinding),
245
+ visuals: visualBriefs(index, visualIntent),
204
246
  status: "planned",
205
247
  }
206
248
  }
207
249
 
208
250
  function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
209
251
  const supportingBindings = claims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
252
+ const visualIntent = visualIntentForSupportingLogic(claims, supportingBindings)
210
253
  return {
211
254
  index,
212
255
  title: "Supporting Logic",
@@ -214,25 +257,29 @@ function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceB
214
257
  narrativeRole: "evidence",
215
258
  layout: "card-grid",
216
259
  qa: true,
217
- components: ["box", "text-panel"],
260
+ components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
218
261
  claimIds: claims.map((claim) => claim.id),
219
262
  claimRefs: claims.map((claim) => ({ claimId: claim.id, role: "supporting" as const, note: claimBoundaryNote(claim) })),
220
263
  evidenceBindingIds: supportingBindings.map((binding) => binding.id),
221
264
  content: {
222
265
  headline: "Supporting claims and boundaries",
223
- bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim)]).slice(0, 8),
266
+ bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim), evidenceGapBullet(claim, evidenceByClaim.get(claim.id) ?? [])]).filter((item): item is string => Boolean(item)).slice(0, 8),
267
+ data: { visualIntent },
224
268
  },
225
269
  evidence: supportingBindings.map(evidenceRefFromBinding),
270
+ visuals: visualBriefs(index, visualIntent),
226
271
  status: "planned",
227
272
  }
228
273
  }
229
274
 
230
- function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
275
+ function riskObjectionSlide(index: number, narrative: NarrativeStateV1, evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
231
276
  const challengedClaimRefs = [
232
277
  ...narrative.risks.map((risk) => risk.claimId ? { claimId: risk.claimId, role: "risk" as const } : undefined).filter((ref): ref is { claimId: string; role: "risk" } => Boolean(ref)),
233
278
  ...narrative.objections.map((objection) => objection.claimId ? { claimId: objection.claimId, role: "objection" as const } : undefined).filter((ref): ref is { claimId: string; role: "objection" } => Boolean(ref)),
234
279
  ]
235
280
  const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
281
+ const challengedBindings = challengedClaimIds.flatMap((claimId) => evidenceByClaim.get(claimId) ?? [])
282
+ const visualIntent = visualIntentForRiskObjection(narrative, challengedBindings)
236
283
  return {
237
284
  index,
238
285
  title: "Risks And Objections",
@@ -240,23 +287,27 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSp
240
287
  narrativeRole: "risk",
241
288
  layout: "two-col",
242
289
  qa: true,
243
- components: ["box", "text-panel"],
290
+ components: componentsForVisualIntent(["box", "text-panel"], visualIntent),
244
291
  claimIds: challengedClaimIds,
245
292
  claimRefs: dedupeClaimRefs(challengedClaimRefs),
293
+ evidenceBindingIds: challengedBindings.map((binding) => binding.id),
246
294
  content: {
247
295
  headline: "What could break the recommendation",
248
296
  bullets: [
249
297
  ...narrative.risks.slice(0, 3).map((risk) => risk.mitigation ? `${risk.text} Mitigation: ${risk.mitigation}` : risk.text),
250
298
  ...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
251
299
  ],
300
+ data: { visualIntent },
252
301
  },
253
- evidence: [],
302
+ evidence: challengedBindings.map(evidenceRefFromBinding),
303
+ visuals: visualBriefs(index, visualIntent),
254
304
  status: "planned",
255
305
  }
256
306
  }
257
307
 
258
308
  function decisionAskSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
259
309
  const askClaims = orderedClaims(narrative, (claim) => claim.kind === "ask" || claim.kind === "recommendation")
310
+ const visualIntent = visualIntentForStructuralSlide("steps", "Show the requested decision, owner, deadline, and consequence as an action sequence rather than a dense closing paragraph.")
260
311
  return {
261
312
  index,
262
313
  title: "Decision Ask",
@@ -274,8 +325,10 @@ function decisionAskSlide(index: number, narrative: NarrativeStateV1): SlideSpec
274
325
  narrative.decision.deadline ? `Deadline: ${narrative.decision.deadline}` : undefined,
275
326
  narrative.decision.consequenceOfNoDecision ? `If no decision: ${narrative.decision.consequenceOfNoDecision}` : undefined,
276
327
  ].filter((item): item is string => Boolean(item)),
328
+ data: { visualIntent },
277
329
  },
278
330
  evidence: [],
331
+ visuals: visualBriefs(index, visualIntent),
279
332
  status: "planned",
280
333
  }
281
334
  }
@@ -302,39 +355,163 @@ function orderedClaims(narrative: NarrativeStateV1, predicate: (claim: Narrative
302
355
  .sort((a, b) => (relationScore.get(b.id) ?? 0) - (relationScore.get(a.id) ?? 0) || (sourceOrder.get(a.id) ?? 0) - (sourceOrder.get(b.id) ?? 0))
303
356
  }
304
357
 
305
- function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeClaim[], supportingClaims: NarrativeClaim[]): string[] {
306
- const chapters: string[] = []
307
- addUnique(chapters, narrative.audience.decisionContext ? "Decision context" : "Context and belief shift")
308
- if (hasClaimKind([...centralClaims, ...supportingClaims], ["problem", "opportunity"])) addUnique(chapters, "Tension and opportunity")
309
- if (centralClaims.some((claim) => claim.kind === "evidence") || supportingClaims.some((claim) => claim.kind === "evidence")) addUnique(chapters, "Evidence and proof")
310
- if (centralClaims.some((claim) => claim.kind === "recommendation" || claim.kind === "ask") || narrative.decision.action) addUnique(chapters, "Recommendation and decision")
311
- if (narrative.risks.length > 0 || narrative.objections.length > 0 || centralClaims.some((claim) => claim.unsupportedScope || (claim.caveats ?? []).length > 0)) addUnique(chapters, "Risks and boundaries")
312
- addUnique(chapters, "Decision ask")
313
- if (chapters.length < 3) addUnique(chapters, "Evidence and proof")
314
- return chapters.slice(0, 5)
358
+ function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeClaim[], supportingClaims: NarrativeClaim[]): DeckPlanChapter[] {
359
+ const claims = [...centralClaims, ...supportingClaims]
360
+ const chapters: DeckPlanChapter[] = []
361
+ addChapter(chapters, narrative.audience.decisionContext ? "Decision context" : "Context and belief shift", "context")
362
+ if (hasClaimKind(claims, ["problem", "opportunity"])) addChapter(chapters, "Tension and opportunity", "tension")
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") || narrative.decision.action) addChapter(chapters, "Recommendation and decision", "recommendation")
365
+ 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
+ addChapter(chapters, "Decision ask", "ask")
367
+ if (chapters.length < 3) addChapter(chapters, "Evidence and proof", "evidence")
368
+ while (chapters.length > 5) {
369
+ const tensionIndex = chapters.findIndex((chapter) => chapter.role === "tension")
370
+ if (tensionIndex >= 0) chapters.splice(tensionIndex, 1)
371
+ else chapters.splice(Math.max(1, chapters.length - 2), 1)
372
+ }
373
+ return chapters
374
+ }
375
+
376
+ function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanChapter["role"]): void {
377
+ if (chapters.some((chapter) => chapter.role === role || chapter.title === title)) return
378
+ chapters.push({ title, role, slideIndexes: [], claimIds: [], evidenceBindingIds: [] })
379
+ }
380
+
381
+ function assignSlideToChapter(chapters: DeckPlanChapter[], role: DeckPlanChapter["role"], slide: SlideSpec): void {
382
+ const chapter = chapters.find((item) => item.role === role) ?? chapters.find((item) => item.role === "evidence") ?? chapters[chapters.length - 1]
383
+ if (!chapter) return
384
+ chapter.slideIndexes.push(slide.index)
385
+ for (const claimId of slide.claimIds ?? []) addUnique(chapter.claimIds, claimId)
386
+ for (const ref of slide.claimRefs ?? []) addUnique(chapter.claimIds, ref.claimId)
387
+ for (const bindingId of slide.evidenceBindingIds ?? []) addUnique(chapter.evidenceBindingIds, bindingId)
315
388
  }
316
389
 
317
390
  function addUnique(items: string[], item: string): void {
318
391
  if (!items.includes(item)) items.push(item)
319
392
  }
320
393
 
394
+ function chapterRoleForClaim(claim: NarrativeClaim): DeckPlanChapter["role"] {
395
+ if (claim.kind === "problem" || claim.kind === "opportunity") return "tension"
396
+ if (claim.kind === "recommendation") return "recommendation"
397
+ if (claim.kind === "ask") return "ask"
398
+ if (claim.kind === "risk" || claim.kind === "assumption") return "risk"
399
+ if (claim.kind === "context") return "context"
400
+ return "evidence"
401
+ }
402
+
321
403
  function hasClaimKind(claims: NarrativeClaim[], kinds: NarrativeClaim["kind"][]): boolean {
322
404
  return claims.some((claim) => kinds.includes(claim.kind))
323
405
  }
324
406
 
325
- function claimComponents(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
326
- if (bindings.some((binding) => binding.quote?.trim())) return ["box", "text-panel", "quote"]
327
- if (claim.kind === "recommendation" || claim.kind === "ask") return ["box", "text-panel", "steps"]
328
- return ["box", "text-panel"]
407
+ function claimComponents(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[], visualIntent: VisualIntent): string[] {
408
+ const base = bindings.some((binding) => binding.quote?.trim()) ? ["box", "text-panel", "quote"] : ["box", "text-panel"]
409
+ if ((claim.kind === "recommendation" || claim.kind === "ask") && visualIntent.kind === "text-only") return ["box", "text-panel", "steps"]
410
+ return componentsForVisualIntent(base, visualIntent)
411
+ }
412
+
413
+ function visualIntentForClaim(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): VisualIntent {
414
+ const evidenceBindingIds = bindings.map((binding) => binding.id)
415
+ const dataSignals = dataSignalsFromBindings(bindings)
416
+ if (bindings.length >= 2) {
417
+ return {
418
+ kind: "evidence-table",
419
+ component: "data-table",
420
+ rationale: "Compare multiple evidence bindings with source, support scope, and caveat columns so the slide is not a bullet stack.",
421
+ dataSignals,
422
+ evidenceBindingIds,
423
+ }
424
+ }
425
+ if (dataSignals.length > 0) {
426
+ return {
427
+ kind: "metric-stat",
428
+ component: "stat-card",
429
+ rationale: "Promote the strongest quantitative evidence signal into a metric card, with the source quote retained for traceability.",
430
+ dataSignals,
431
+ evidenceBindingIds,
432
+ }
433
+ }
434
+ if (claim.kind === "recommendation" || claim.kind === "ask") {
435
+ return {
436
+ kind: "steps",
437
+ component: "steps",
438
+ rationale: "Show the recommendation as phased actions or decision gates rather than a paragraph.",
439
+ dataSignals,
440
+ evidenceBindingIds,
441
+ }
442
+ }
443
+ return {
444
+ kind: "text-only",
445
+ component: "box",
446
+ rationale: "No quantified or multi-source visual signal is available; use semantic evidence boxes and keep boundaries explicit.",
447
+ dataSignals,
448
+ evidenceBindingIds,
449
+ }
450
+ }
451
+
452
+ function visualIntentForSupportingLogic(claims: NarrativeClaim[], bindings: NarrativeEvidenceBinding[]): VisualIntent {
453
+ const dataSignals = dataSignalsFromBindings(bindings)
454
+ return {
455
+ kind: claims.length >= 3 || bindings.length >= 2 ? "comparison-grid" : "evidence-table",
456
+ component: "data-table",
457
+ rationale: "Organize supporting claims as a comparison grid with evidence status and boundaries, avoiding a long undifferentiated bullet list.",
458
+ dataSignals,
459
+ evidenceBindingIds: bindings.map((binding) => binding.id),
460
+ }
461
+ }
462
+
463
+ function visualIntentForRiskObjection(narrative: NarrativeStateV1, bindings: NarrativeEvidenceBinding[]): VisualIntent {
464
+ return {
465
+ kind: "risk-matrix",
466
+ component: "data-table",
467
+ rationale: "Pair each risk or objection with mitigation or response in a compact matrix so caveats stay visible without becoming prose-heavy.",
468
+ dataSignals: [...narrative.risks.map((risk) => risk.severity), ...narrative.objections.map((objection) => objection.priority)].filter(Boolean),
469
+ evidenceBindingIds: bindings.map((binding) => binding.id),
470
+ }
471
+ }
472
+
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: [] }
475
+ }
476
+
477
+ function componentsForVisualIntent(base: string[], visualIntent: VisualIntent): string[] {
478
+ const next = [...base]
479
+ if (visualIntent.component && !next.includes(visualIntent.component)) next.push(visualIntent.component)
480
+ return next
481
+ }
482
+
483
+ function visualBriefs(slideIndex: number, visualIntent: VisualIntent): VisualBrief[] {
484
+ return [{
485
+ id: `visual:${slideIndex}:${visualIntent.kind}`,
486
+ purpose: visualIntent.kind,
487
+ brief: `${visualIntent.rationale} Use ${visualIntent.component} and preserve cited evidence boundaries${visualIntent.dataSignals.length > 0 ? `; visible signals: ${visualIntent.dataSignals.slice(0, 4).join(", ")}.` : "."}`,
488
+ }]
489
+ }
490
+
491
+ function dataSignalsFromBindings(bindings: NarrativeEvidenceBinding[]): string[] {
492
+ const signals = bindings.flatMap((binding) => numericSignals([binding.quote, binding.supportScope, binding.source, binding.location].filter(Boolean).join(" ")))
493
+ return [...new Set(signals)].slice(0, 6)
494
+ }
495
+
496
+ function numericSignals(text: string): string[] {
497
+ return [...text.matchAll(/(?:[$€£¥]\s*)?\d+(?:\.\d+)?\s*(?:%|bps|x|k|m|bn|billion|million|year|years|yr|yrs)?/gi)]
498
+ .map((match) => match[0].trim())
499
+ .filter(Boolean)
329
500
  }
330
501
 
331
502
  function claimBullets(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
332
503
  return [
333
504
  ...claimBoundaryBullets(claim),
334
505
  ...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence supports: ${binding.supportScope}` : undefined),
506
+ evidenceGapBullet(claim, bindings),
335
507
  ].filter((item): item is string => Boolean(item))
336
508
  }
337
509
 
510
+ function evidenceGapBullet(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string | undefined {
511
+ if (!claim.evidenceRequired || bindings.length > 0) return undefined
512
+ return `Evidence gap: ${claim.evidenceStatus === "missing" ? "no binding yet" : "support remains incomplete"}.`
513
+ }
514
+
338
515
  function claimBoundaryBullets(claim: NarrativeClaim): string[] {
339
516
  return [
340
517
  claim.supportedScope ? `Supported scope: ${claim.supportedScope}` : undefined,
@@ -370,13 +547,29 @@ function evidenceRefFromBinding(binding: NarrativeEvidenceBinding): EvidenceRef
370
547
  }
371
548
  }
372
549
 
373
- function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[]): DeckPlanQualityCheck[] {
550
+ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chapters: DeckPlanChapter[]): DeckPlanQualityCheck[] {
374
551
  const coverage = deckPlanCoverage(narrative, slides)
375
552
  const centralClaimIds = narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id)
376
553
  const missingCentralClaims = centralClaimIds.filter((claimId) => coverage.missingClaimIds.includes(claimId))
377
554
  const incompatibleComponents = [...new Set(slides.flatMap((slide) => slide.components).filter((component) => component === "card"))]
555
+ const toc = slides.find((slide) => slide.components.includes("toc"))
556
+ const tocBullets = toc?.content.bullets ?? []
557
+ const chapterTitles = chapters.map((chapter) => chapter.title)
558
+ const evidenceRequiredWithoutBindings = narrative.claims.filter((claim) => claim.evidenceRequired && !narrative.evidenceBindings.some((binding) => binding.claimId === claim.id))
559
+ const invisibleEvidenceGaps = evidenceRequiredWithoutBindings.filter((claim) => coverage.missingClaimIds.includes(claim.id))
560
+ const risksOrObjectionsVisible = narrative.risks.length === 0 && narrative.objections.length === 0 || slides.some((slide) => slide.narrativeRole === "risk")
378
561
 
379
562
  return [
563
+ {
564
+ id: "chapter_structure_present",
565
+ status: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? "pass" : "blocker",
566
+ message: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? `Deck plan includes ${chapters.length} deterministic chapters with slide ranges.` : "Deck plan must include 3-5 deterministic chapters, each mapped to at least one slide.",
567
+ },
568
+ {
569
+ id: "toc_matches_chapters",
570
+ status: chapterTitles.length > 0 && chapterTitles.every((title) => tocBullets.includes(title)) ? "pass" : "blocker",
571
+ message: chapterTitles.length > 0 && chapterTitles.every((title) => tocBullets.includes(title)) ? "TOC headings match the deterministic chapter plan." : "TOC headings do not match the deterministic chapter plan.",
572
+ },
380
573
  {
381
574
  id: "toc_present",
382
575
  status: slides.some((slide) => slide.components.includes("toc")) ? "pass" : "blocker",
@@ -397,6 +590,16 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[]): Dec
397
590
  status: narrative.claims.some((claim) => claim.importance === "central" && (claim.unsupportedScope || (claim.caveats ?? []).length > 0)) ? "warning" : "pass",
398
591
  message: narrative.claims.some((claim) => claim.importance === "central" && (claim.unsupportedScope || (claim.caveats ?? []).length > 0)) ? "Central claim boundaries are visible and should remain explicit in the rendered artifact." : "No unsupported central claim boundaries were found.",
399
592
  },
593
+ {
594
+ id: "evidence_required_claims_have_evidence_or_visible_gap",
595
+ status: invisibleEvidenceGaps.length === 0 ? evidenceRequiredWithoutBindings.length > 0 ? "warning" : "pass" : "blocker",
596
+ message: invisibleEvidenceGaps.length > 0 ? `Evidence-required claims missing from planned slides: ${invisibleEvidenceGaps.map((claim) => claim.id).join(", ")}` : evidenceRequiredWithoutBindings.length > 0 ? `Evidence gaps remain visible for claims: ${evidenceRequiredWithoutBindings.map((claim) => claim.id).join(", ")}` : "Every evidence-required claim has at least one evidence binding.",
597
+ },
598
+ {
599
+ id: "risk_or_objection_visible",
600
+ status: risksOrObjectionsVisible ? "pass" : "warning",
601
+ message: risksOrObjectionsVisible ? "Risks and objections are visible when present." : "Narrative risks or objections exist but no risk/objection slide is planned.",
602
+ },
400
603
  {
401
604
  id: "simplified_design_grammar",
402
605
  status: incompatibleComponents.length === 0 ? "pass" : "blocker",