@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.
- package/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/brief.ts +9 -0
- package/lib/commands/help.ts +5 -2
- package/lib/commands/init.ts +42 -27
- package/lib/commands/narrative.ts +39 -6
- package/lib/commands/research.ts +36 -20
- package/lib/commands/review.ts +35 -28
- package/lib/ctx.ts +1 -1
- package/lib/decks-state.ts +38 -4
- package/lib/edit/prompt.ts +1 -1
- package/lib/hook-notifications.ts +53 -0
- 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/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +238 -35
- package/lib/narrative-state/research-binding-eval.ts +260 -0
- package/lib/narrative-state/research-gaps.ts +2 -88
- package/lib/narrative-vault/authoring-contract.ts +127 -0
- package/lib/narrative-vault/authoring-guard.ts +122 -0
- package/lib/narrative-vault/auto-compile.ts +134 -0
- package/lib/narrative-vault/bootstrap.ts +63 -0
- package/lib/narrative-vault/cache.ts +14 -0
- package/lib/narrative-vault/compile-mirror.ts +45 -0
- package/lib/narrative-vault/compile.ts +350 -0
- package/lib/narrative-vault/constants.ts +6 -0
- package/lib/narrative-vault/diagnostic-report.ts +117 -0
- package/lib/narrative-vault/export.ts +71 -0
- package/lib/narrative-vault/frontmatter.ts +41 -0
- package/lib/narrative-vault/hook-targets.ts +40 -0
- package/lib/narrative-vault/index.ts +18 -0
- package/lib/narrative-vault/inventory.ts +392 -0
- package/lib/narrative-vault/markdown-qa.ts +237 -0
- package/lib/narrative-vault/markdown.ts +34 -0
- package/lib/narrative-vault/migration.ts +52 -0
- package/lib/narrative-vault/mutate.ts +361 -0
- package/lib/narrative-vault/paths.ts +19 -0
- package/lib/narrative-vault/read.ts +52 -0
- package/lib/narrative-vault/relations.ts +32 -0
- package/lib/narrative-vault/source-loader.ts +19 -0
- package/lib/narrative-vault/timestamp.ts +32 -0
- package/lib/narrative-vault/types.ts +44 -0
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/server.ts +157 -20
- package/lib/source-materials.ts +98 -0
- package/lib/tool-result.ts +34 -0
- package/package.json +2 -2
- package/plugin.ts +60 -22
- package/skill/NARRATIVE_SKILL.md +25 -10
- package/skill/SKILL.md +6 -1
- package/tools/decks.ts +363 -67
- package/tools/narrative-view.ts +16 -0
- package/tools/research-save.ts +3 -0
- 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
|
|
51
|
-
const
|
|
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
|
-
|
|
95
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
168
|
+
const slide = riskObjectionSlide(slides.length + 1, narrative, evidenceByClaim)
|
|
169
|
+
slides.push(slide)
|
|
170
|
+
assignSlideToChapter(chapters, "risk", slide)
|
|
140
171
|
}
|
|
141
172
|
|
|
142
|
-
|
|
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:
|
|
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[]):
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
if (
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
327
|
-
if (claim.kind === "recommendation" || claim.kind === "ask") return ["box", "text-panel", "steps"]
|
|
328
|
-
return
|
|
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",
|