@cyber-dash-tech/revela 0.15.4 → 0.16.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commands/narrative.ts +9 -3
- package/lib/commands/research.ts +25 -18
- package/lib/commands/review.ts +2 -1
- package/lib/decks-state.ts +142 -4
- package/lib/narrative-state/display.ts +40 -0
- package/lib/narrative-state/map-html.ts +133 -6
- package/lib/narrative-state/map.ts +333 -16
- package/lib/narrative-state/render-plan.ts +254 -84
- package/lib/narrative-state/research-gaps.ts +332 -0
- package/package.json +1 -1
- package/tools/decks.ts +7 -2
- package/tools/narrative-view.ts +11 -1
|
@@ -17,6 +17,13 @@ export interface CompileDeckPlanResult {
|
|
|
17
17
|
narrativeHash: string
|
|
18
18
|
slideCount: number
|
|
19
19
|
slides: SlideSpec[]
|
|
20
|
+
qualityChecks?: DeckPlanQualityCheck[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DeckPlanQualityCheck {
|
|
24
|
+
id: string
|
|
25
|
+
status: "pass" | "warning" | "blocker"
|
|
26
|
+
message: string
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
export function compileDeckPlanFromNarrative(state: DecksState, options: CompileDeckPlanOptions = {}): { state: DecksState; result: CompileDeckPlanResult } {
|
|
@@ -41,6 +48,8 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
41
48
|
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
42
49
|
const slug = deck?.slug ?? state.activeDeck ?? "deck"
|
|
43
50
|
const slides = buildSlides(narrative)
|
|
51
|
+
const qualityChecks = checkPlanQuality(narrative, slides)
|
|
52
|
+
const planCoverage = deckPlanCoverage(narrative, slides)
|
|
44
53
|
const requiredInputs: Partial<RequiredInputs> = {
|
|
45
54
|
topicClarified: true,
|
|
46
55
|
audienceClarified: Boolean(narrative.audience.primary),
|
|
@@ -70,6 +79,7 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
70
79
|
status: "pending",
|
|
71
80
|
narrativeHash,
|
|
72
81
|
planHash: deckPlanHash(plannedDeck.slides),
|
|
82
|
+
qualityChecks,
|
|
73
83
|
}
|
|
74
84
|
plannedDeck.requiredInputs = { ...plannedDeck.requiredInputs, slidePlanConfirmed: false }
|
|
75
85
|
plannedDeck.writeReadiness = { status: "blocked", blockers: [] }
|
|
@@ -81,6 +91,10 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
81
91
|
...(htmlTarget.data ?? {}),
|
|
82
92
|
narrativeId: narrative.id,
|
|
83
93
|
narrativeHash,
|
|
94
|
+
planQualityChecks: qualityChecks,
|
|
95
|
+
requiredClaimIds: planCoverage.requiredClaimIds,
|
|
96
|
+
coveredClaimIds: planCoverage.coveredClaimIds,
|
|
97
|
+
missingClaimIds: planCoverage.missingClaimIds,
|
|
84
98
|
claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
|
|
85
99
|
claimId: ref.claimId,
|
|
86
100
|
claimText: ref.claimText,
|
|
@@ -95,35 +109,50 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
95
109
|
|
|
96
110
|
return {
|
|
97
111
|
state: next,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
112
|
+
result: {
|
|
113
|
+
compiled: true,
|
|
114
|
+
skipped: false,
|
|
115
|
+
narrativeHash,
|
|
116
|
+
slideCount: slides.length,
|
|
117
|
+
slides,
|
|
118
|
+
qualityChecks,
|
|
119
|
+
},
|
|
120
|
+
}
|
|
106
121
|
}
|
|
107
122
|
|
|
108
123
|
function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
109
124
|
const slides: SlideSpec[] = []
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
125
|
+
const evidenceByClaim = evidenceBindingsByClaim(narrative.evidenceBindings)
|
|
126
|
+
const centralClaims = orderedClaims(narrative, (claim) => claim.importance === "central")
|
|
127
|
+
const supportingClaims = orderedClaims(narrative, (claim) => claim.importance !== "central")
|
|
128
|
+
const chapters = deriveChapters(narrative, centralClaims, supportingClaims)
|
|
129
|
+
|
|
130
|
+
slides.push(coverSlide(slides.length + 1, narrative))
|
|
131
|
+
slides.push(tocSlide(slides.length + 1, chapters))
|
|
132
|
+
|
|
133
|
+
for (const claim of centralClaims) {
|
|
134
|
+
slides.push(claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? []))
|
|
117
135
|
}
|
|
136
|
+
if (supportingClaims.length > 0) slides.push(supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim))
|
|
118
137
|
|
|
119
|
-
|
|
120
|
-
|
|
138
|
+
if (narrative.risks.length > 0 || narrative.objections.length > 0) {
|
|
139
|
+
slides.push(riskObjectionSlide(slides.length + 1, narrative))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
slides.push(decisionAskSlide(slides.length + 1, narrative))
|
|
143
|
+
|
|
144
|
+
return slides
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
148
|
+
return {
|
|
149
|
+
index,
|
|
121
150
|
title: "Decision Context",
|
|
122
151
|
purpose: "Frame the audience belief shift and decision required before presenting the recommendation.",
|
|
123
152
|
narrativeRole: "context",
|
|
124
153
|
layout: "cover",
|
|
125
154
|
qa: false,
|
|
126
|
-
components: [],
|
|
155
|
+
components: ["hero", "text-panel"],
|
|
127
156
|
content: {
|
|
128
157
|
headline: narrative.thesis?.statement || narrative.decision.action || "Narrative context",
|
|
129
158
|
body: [
|
|
@@ -134,76 +163,25 @@ function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
|
134
163
|
},
|
|
135
164
|
evidence: [],
|
|
136
165
|
status: "planned",
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
for (const claim of centralClaims) slides.push(claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? []))
|
|
140
|
-
if (supportingClaims.length > 0) {
|
|
141
|
-
const supportingBindings = supportingClaims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
|
|
142
|
-
slides.push({
|
|
143
|
-
index: slides.length + 1,
|
|
144
|
-
title: "Supporting Logic",
|
|
145
|
-
purpose: "Connect supporting claims to the central recommendation without overloading the main proof slides.",
|
|
146
|
-
narrativeRole: "evidence",
|
|
147
|
-
layout: "card-grid",
|
|
148
|
-
qa: true,
|
|
149
|
-
components: ["card"],
|
|
150
|
-
claimIds: supportingClaims.map((claim) => claim.id),
|
|
151
|
-
claimRefs: supportingClaims.map((claim) => ({ claimId: claim.id, role: "supporting" as const })),
|
|
152
|
-
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
153
|
-
content: {
|
|
154
|
-
headline: "Supporting claims and boundaries",
|
|
155
|
-
bullets: supportingClaims.slice(0, 5).map((claim) => claim.text),
|
|
156
|
-
},
|
|
157
|
-
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
158
|
-
status: "planned",
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (narrative.risks.length > 0 || narrative.objections.length > 0) {
|
|
163
|
-
const challengedClaimRefs = [
|
|
164
|
-
...narrative.risks.map((risk) => risk.claimId ? { claimId: risk.claimId, role: "risk" as const } : undefined).filter((ref): ref is { claimId: string; role: "risk" } => Boolean(ref)),
|
|
165
|
-
...narrative.objections.map((objection) => objection.claimId ? { claimId: objection.claimId, role: "objection" as const } : undefined).filter((ref): ref is { claimId: string; role: "objection" } => Boolean(ref)),
|
|
166
|
-
]
|
|
167
|
-
const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
|
|
168
|
-
slides.push({
|
|
169
|
-
index: slides.length + 1,
|
|
170
|
-
title: "Risks And Objections",
|
|
171
|
-
purpose: "Make caveats and stakeholder objections visible before asking for a decision.",
|
|
172
|
-
narrativeRole: "risk",
|
|
173
|
-
layout: "two-col",
|
|
174
|
-
qa: true,
|
|
175
|
-
components: ["card"],
|
|
176
|
-
claimIds: challengedClaimIds,
|
|
177
|
-
claimRefs: dedupeClaimRefs(challengedClaimRefs),
|
|
178
|
-
content: {
|
|
179
|
-
headline: "What could break the recommendation",
|
|
180
|
-
bullets: [
|
|
181
|
-
...narrative.risks.slice(0, 3).map((risk) => risk.mitigation ? `${risk.text} Mitigation: ${risk.mitigation}` : risk.text),
|
|
182
|
-
...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
|
|
183
|
-
],
|
|
184
|
-
},
|
|
185
|
-
evidence: [],
|
|
186
|
-
status: "planned",
|
|
187
|
-
})
|
|
188
166
|
}
|
|
167
|
+
}
|
|
189
168
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
169
|
+
function tocSlide(index: number, chapters: string[]): SlideSpec {
|
|
170
|
+
return {
|
|
171
|
+
index,
|
|
172
|
+
title: "Storyline",
|
|
173
|
+
purpose: "Preview the deterministic chapter structure compiled from the approved narrative state.",
|
|
174
|
+
narrativeRole: "context",
|
|
175
|
+
layout: "toc",
|
|
196
176
|
qa: false,
|
|
197
|
-
components: [],
|
|
177
|
+
components: ["toc", "text-panel"],
|
|
198
178
|
content: {
|
|
199
|
-
headline:
|
|
200
|
-
bullets:
|
|
179
|
+
headline: "How the decision story is organized",
|
|
180
|
+
bullets: chapters,
|
|
201
181
|
},
|
|
202
182
|
evidence: [],
|
|
203
183
|
status: "planned",
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return slides
|
|
184
|
+
}
|
|
207
185
|
}
|
|
208
186
|
|
|
209
187
|
function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
@@ -214,19 +192,162 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
|
|
|
214
192
|
narrativeRole: claim.kind === "risk" || claim.kind === "assumption" ? "risk" : claim.kind === "ask" ? "ask" : claim.kind === "recommendation" ? "recommendation" : "evidence",
|
|
215
193
|
layout: "two-col",
|
|
216
194
|
qa: true,
|
|
217
|
-
components:
|
|
195
|
+
components: claimComponents(claim, bindings),
|
|
218
196
|
claimIds: [claim.id],
|
|
219
|
-
claimRefs: [{ claimId: claim.id, role: "primary" }],
|
|
197
|
+
claimRefs: [{ claimId: claim.id, role: "primary", note: claimBoundaryNote(claim) }],
|
|
220
198
|
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
221
199
|
content: {
|
|
222
200
|
headline: claim.text,
|
|
223
|
-
bullets:
|
|
201
|
+
bullets: claimBullets(claim, bindings),
|
|
224
202
|
},
|
|
225
203
|
evidence: bindings.map(evidenceRefFromBinding),
|
|
226
204
|
status: "planned",
|
|
227
205
|
}
|
|
228
206
|
}
|
|
229
207
|
|
|
208
|
+
function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
|
|
209
|
+
const supportingBindings = claims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
|
|
210
|
+
return {
|
|
211
|
+
index,
|
|
212
|
+
title: "Supporting Logic",
|
|
213
|
+
purpose: "Connect supporting and background claims to the central recommendation without overloading the main proof slides.",
|
|
214
|
+
narrativeRole: "evidence",
|
|
215
|
+
layout: "card-grid",
|
|
216
|
+
qa: true,
|
|
217
|
+
components: ["box", "text-panel"],
|
|
218
|
+
claimIds: claims.map((claim) => claim.id),
|
|
219
|
+
claimRefs: claims.map((claim) => ({ claimId: claim.id, role: "supporting" as const, note: claimBoundaryNote(claim) })),
|
|
220
|
+
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
221
|
+
content: {
|
|
222
|
+
headline: "Supporting claims and boundaries",
|
|
223
|
+
bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim)]).slice(0, 8),
|
|
224
|
+
},
|
|
225
|
+
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
226
|
+
status: "planned",
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
231
|
+
const challengedClaimRefs = [
|
|
232
|
+
...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
|
+
...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
|
+
]
|
|
235
|
+
const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
|
|
236
|
+
return {
|
|
237
|
+
index,
|
|
238
|
+
title: "Risks And Objections",
|
|
239
|
+
purpose: "Make caveats and stakeholder objections visible before asking for a decision.",
|
|
240
|
+
narrativeRole: "risk",
|
|
241
|
+
layout: "two-col",
|
|
242
|
+
qa: true,
|
|
243
|
+
components: ["box", "text-panel"],
|
|
244
|
+
claimIds: challengedClaimIds,
|
|
245
|
+
claimRefs: dedupeClaimRefs(challengedClaimRefs),
|
|
246
|
+
content: {
|
|
247
|
+
headline: "What could break the recommendation",
|
|
248
|
+
bullets: [
|
|
249
|
+
...narrative.risks.slice(0, 3).map((risk) => risk.mitigation ? `${risk.text} Mitigation: ${risk.mitigation}` : risk.text),
|
|
250
|
+
...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
evidence: [],
|
|
254
|
+
status: "planned",
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function decisionAskSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
259
|
+
const askClaims = orderedClaims(narrative, (claim) => claim.kind === "ask" || claim.kind === "recommendation")
|
|
260
|
+
return {
|
|
261
|
+
index,
|
|
262
|
+
title: "Decision Ask",
|
|
263
|
+
purpose: "Close with the explicit decision or action requested from the audience.",
|
|
264
|
+
narrativeRole: "ask",
|
|
265
|
+
layout: "closing",
|
|
266
|
+
qa: false,
|
|
267
|
+
components: ["hero", "text-panel"],
|
|
268
|
+
claimIds: askClaims.map((claim) => claim.id),
|
|
269
|
+
claimRefs: askClaims.map((claim) => ({ claimId: claim.id, role: "primary" as const, note: claimBoundaryNote(claim) })),
|
|
270
|
+
content: {
|
|
271
|
+
headline: narrative.decision.action || "Confirm the decision",
|
|
272
|
+
bullets: [
|
|
273
|
+
narrative.decision.owner ? `Owner: ${narrative.decision.owner}` : undefined,
|
|
274
|
+
narrative.decision.deadline ? `Deadline: ${narrative.decision.deadline}` : undefined,
|
|
275
|
+
narrative.decision.consequenceOfNoDecision ? `If no decision: ${narrative.decision.consequenceOfNoDecision}` : undefined,
|
|
276
|
+
].filter((item): item is string => Boolean(item)),
|
|
277
|
+
},
|
|
278
|
+
evidence: [],
|
|
279
|
+
status: "planned",
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function evidenceBindingsByClaim(bindings: NarrativeEvidenceBinding[]): Map<string, NarrativeEvidenceBinding[]> {
|
|
284
|
+
const evidenceByClaim = new Map<string, NarrativeEvidenceBinding[]>()
|
|
285
|
+
for (const binding of bindings) {
|
|
286
|
+
const list = evidenceByClaim.get(binding.claimId) ?? []
|
|
287
|
+
list.push(binding)
|
|
288
|
+
evidenceByClaim.set(binding.claimId, list)
|
|
289
|
+
}
|
|
290
|
+
return evidenceByClaim
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function orderedClaims(narrative: NarrativeStateV1, predicate: (claim: NarrativeClaim) => boolean): NarrativeClaim[] {
|
|
294
|
+
const sourceOrder = new Map(narrative.claims.map((claim, index) => [claim.id, index]))
|
|
295
|
+
const relationScore = new Map<string, number>()
|
|
296
|
+
for (const relation of narrative.claimRelations ?? []) {
|
|
297
|
+
const delta = relation.relation === "leads_to" ? 3 : relation.relation === "supports" ? 2 : relation.relation === "depends_on" || relation.relation === "constrains" ? 1 : 0
|
|
298
|
+
relationScore.set(relation.toClaimId, (relationScore.get(relation.toClaimId) ?? 0) + delta)
|
|
299
|
+
}
|
|
300
|
+
return narrative.claims
|
|
301
|
+
.filter(predicate)
|
|
302
|
+
.sort((a, b) => (relationScore.get(b.id) ?? 0) - (relationScore.get(a.id) ?? 0) || (sourceOrder.get(a.id) ?? 0) - (sourceOrder.get(b.id) ?? 0))
|
|
303
|
+
}
|
|
304
|
+
|
|
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)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function addUnique(items: string[], item: string): void {
|
|
318
|
+
if (!items.includes(item)) items.push(item)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function hasClaimKind(claims: NarrativeClaim[], kinds: NarrativeClaim["kind"][]): boolean {
|
|
322
|
+
return claims.some((claim) => kinds.includes(claim.kind))
|
|
323
|
+
}
|
|
324
|
+
|
|
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"]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function claimBullets(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string[] {
|
|
332
|
+
return [
|
|
333
|
+
...claimBoundaryBullets(claim),
|
|
334
|
+
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence supports: ${binding.supportScope}` : undefined),
|
|
335
|
+
].filter((item): item is string => Boolean(item))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function claimBoundaryBullets(claim: NarrativeClaim): string[] {
|
|
339
|
+
return [
|
|
340
|
+
claim.supportedScope ? `Supported scope: ${claim.supportedScope}` : undefined,
|
|
341
|
+
claim.unsupportedScope ? `Unsupported scope: ${claim.unsupportedScope}` : undefined,
|
|
342
|
+
...(claim.caveats ?? []).map((caveat) => `Caveat: ${caveat}`),
|
|
343
|
+
].filter((item): item is string => Boolean(item))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function claimBoundaryNote(claim: NarrativeClaim): string | undefined {
|
|
347
|
+
const notes = claimBoundaryBullets(claim)
|
|
348
|
+
return notes.length > 0 ? notes.join(" ") : undefined
|
|
349
|
+
}
|
|
350
|
+
|
|
230
351
|
function dedupeClaimRefs<T extends { claimId: string; role: "risk" | "objection" }>(refs: T[]): T[] {
|
|
231
352
|
const seen = new Set<string>()
|
|
232
353
|
return refs.filter((ref) => {
|
|
@@ -249,6 +370,55 @@ function evidenceRefFromBinding(binding: NarrativeEvidenceBinding): EvidenceRef
|
|
|
249
370
|
}
|
|
250
371
|
}
|
|
251
372
|
|
|
373
|
+
function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[]): DeckPlanQualityCheck[] {
|
|
374
|
+
const coverage = deckPlanCoverage(narrative, slides)
|
|
375
|
+
const centralClaimIds = narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id)
|
|
376
|
+
const missingCentralClaims = centralClaimIds.filter((claimId) => coverage.missingClaimIds.includes(claimId))
|
|
377
|
+
const incompatibleComponents = [...new Set(slides.flatMap((slide) => slide.components).filter((component) => component === "card"))]
|
|
378
|
+
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
id: "toc_present",
|
|
382
|
+
status: slides.some((slide) => slide.components.includes("toc")) ? "pass" : "blocker",
|
|
383
|
+
message: slides.some((slide) => slide.components.includes("toc")) ? "Deck plan includes a deterministic TOC slide." : "Deck plan is missing a TOC slide.",
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "closing_ask_present",
|
|
387
|
+
status: slides.some((slide) => slide.narrativeRole === "ask" && slide.title === "Decision Ask") ? "pass" : "blocker",
|
|
388
|
+
message: slides.some((slide) => slide.narrativeRole === "ask" && slide.title === "Decision Ask") ? "Deck plan includes a closing Decision Ask slide." : "Deck plan is missing a closing Decision Ask slide.",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "central_claims_covered",
|
|
392
|
+
status: missingCentralClaims.length === 0 ? "pass" : "blocker",
|
|
393
|
+
message: missingCentralClaims.length === 0 ? "All central claims are covered by planned slides." : `Central claims missing from planned slides: ${missingCentralClaims.join(", ")}`,
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
id: "unsupported_central_claims_visible",
|
|
397
|
+
status: narrative.claims.some((claim) => claim.importance === "central" && (claim.unsupportedScope || (claim.caveats ?? []).length > 0)) ? "warning" : "pass",
|
|
398
|
+
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
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "simplified_design_grammar",
|
|
402
|
+
status: incompatibleComponents.length === 0 ? "pass" : "blocker",
|
|
403
|
+
message: incompatibleComponents.length === 0 ? "Planned slides use the simplified design grammar." : `Deck plan uses incompatible primary components: ${incompatibleComponents.join(", ")}`,
|
|
404
|
+
},
|
|
405
|
+
]
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function deckPlanCoverage(narrative: NarrativeStateV1, slides: SlideSpec[]): { requiredClaimIds: string[]; coveredClaimIds: string[]; missingClaimIds: string[] } {
|
|
409
|
+
const requiredClaimIds = narrative.claims
|
|
410
|
+
.filter((claim) => claim.importance === "central" || claim.evidenceRequired)
|
|
411
|
+
.map((claim) => claim.id)
|
|
412
|
+
.sort()
|
|
413
|
+
const required = new Set(requiredClaimIds)
|
|
414
|
+
const coveredClaimIds = [...new Set(slides.flatMap((slide) => [
|
|
415
|
+
...(slide.claimIds ?? []),
|
|
416
|
+
...(slide.claimRefs ?? []).map((ref) => ref.claimId),
|
|
417
|
+
]).filter((claimId) => required.has(claimId)))].sort()
|
|
418
|
+
const missingClaimIds = requiredClaimIds.filter((claimId) => !coveredClaimIds.includes(claimId))
|
|
419
|
+
return { requiredClaimIds, coveredClaimIds, missingClaimIds }
|
|
420
|
+
}
|
|
421
|
+
|
|
252
422
|
function titleFromClaim(claim: NarrativeClaim): string {
|
|
253
423
|
const words = claim.text.split(/\s+/).filter(Boolean).slice(0, 6).join(" ")
|
|
254
424
|
return words || claim.kind
|