@cyber-dash-tech/revela 0.11.0 → 0.13.0
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 +35 -29
- package/README.zh-CN.md +35 -29
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +7 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/narrative.ts +160 -0
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +46 -3
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspection-context/compile.ts +159 -5
- package/lib/inspection-context/project.ts +20 -0
- package/lib/narrative-state/coverage.ts +100 -0
- package/lib/narrative-state/display.ts +219 -0
- package/lib/narrative-state/executive-brief.ts +246 -0
- package/lib/narrative-state/hash.ts +61 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +361 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/queries.ts +433 -0
- package/lib/narrative-state/readiness.ts +359 -0
- package/lib/narrative-state/render-plan.ts +250 -0
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +172 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +174 -2
- package/lib/workspace-state/types.ts +13 -1
- package/package.json +1 -1
- package/plugin.ts +58 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +265 -2
- package/tools/narrative-view.ts +84 -0
- package/tools/workspace-scan.ts +14 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import type { DeckSpec, DecksState, EvidenceRef, SlideSpec } from "../decks-state"
|
|
2
|
+
import {
|
|
3
|
+
stableClaimId,
|
|
4
|
+
stableClaimRelationId,
|
|
5
|
+
stableEvidenceId,
|
|
6
|
+
stableNarrativeId,
|
|
7
|
+
stableObjectionId,
|
|
8
|
+
stableResearchGapId,
|
|
9
|
+
stableRiskId,
|
|
10
|
+
} from "./hash"
|
|
11
|
+
import type {
|
|
12
|
+
AudienceIntent,
|
|
13
|
+
DecisionIntent,
|
|
14
|
+
NarrativeClaim,
|
|
15
|
+
NarrativeClaimKind,
|
|
16
|
+
NarrativeClaimRelation,
|
|
17
|
+
NarrativeClaimRelationType,
|
|
18
|
+
NarrativeEvidenceBinding,
|
|
19
|
+
NarrativeEvidenceStatus,
|
|
20
|
+
NarrativeObjection,
|
|
21
|
+
NarrativeResearchGap,
|
|
22
|
+
NarrativeRisk,
|
|
23
|
+
NarrativeStateV1,
|
|
24
|
+
NarrativeStatus,
|
|
25
|
+
NarrativeThesis,
|
|
26
|
+
} from "./types"
|
|
27
|
+
|
|
28
|
+
const MIGRATED_UPDATED_AT = "1970-01-01T00:00:00.000Z"
|
|
29
|
+
|
|
30
|
+
export function normalizeCanonicalNarrativeState(input: Partial<NarrativeStateV1> | undefined, seed = "workspace"): NarrativeStateV1 | undefined {
|
|
31
|
+
if (!input) return undefined
|
|
32
|
+
const id = input.id?.trim() || stableNarrativeId(seed)
|
|
33
|
+
const claims = dedupeById((input.claims ?? []).map(normalizeClaim).filter((claim): claim is NarrativeClaim => Boolean(claim)))
|
|
34
|
+
const claimRelations = dedupeById((input.claimRelations ?? []).map((relation) => normalizeClaimRelation(relation, claims)).filter((relation): relation is NarrativeClaimRelation => Boolean(relation)))
|
|
35
|
+
const evidenceBindings = dedupeById((input.evidenceBindings ?? []).map((binding) => normalizeEvidenceBinding(binding, claims)).filter((binding): binding is NarrativeEvidenceBinding => Boolean(binding)))
|
|
36
|
+
return {
|
|
37
|
+
version: 1,
|
|
38
|
+
id,
|
|
39
|
+
status: normalizeStatus(input.status),
|
|
40
|
+
audience: normalizeAudience(input.audience),
|
|
41
|
+
decision: normalizeDecision(input.decision),
|
|
42
|
+
thesis: normalizeThesis(input.thesis),
|
|
43
|
+
claims: claims.map((claim) => ({ ...claim, evidenceStatus: evidenceStatusForClaim(claim, evidenceBindings) })),
|
|
44
|
+
claimRelations,
|
|
45
|
+
evidenceBindings,
|
|
46
|
+
objections: dedupeById((input.objections ?? []).map(normalizeObjection).filter((objection): objection is NarrativeObjection => Boolean(objection))),
|
|
47
|
+
risks: dedupeById((input.risks ?? []).map(normalizeRisk).filter((risk): risk is NarrativeRisk => Boolean(risk))),
|
|
48
|
+
researchGaps: dedupeById((input.researchGaps ?? []).map(normalizeResearchGap).filter((gap): gap is NarrativeResearchGap => Boolean(gap))),
|
|
49
|
+
approvals: input.approvals ?? [],
|
|
50
|
+
updatedAt: input.updatedAt || MIGRATED_UPDATED_AT,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeNarrativeState(state: DecksState): NarrativeStateV1 {
|
|
55
|
+
const deck = activeDeck(state)
|
|
56
|
+
const existing = normalizeCanonicalNarrativeState(state.narrative, deck?.slug ?? state.activeDeck ?? "workspace")
|
|
57
|
+
if (existing && hasCanonicalNarrativeContent(existing)) return existing
|
|
58
|
+
return migrateDeckNarrative(deck, state.activeDeck ?? "workspace")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function migrateDeckNarrative(deck: DeckSpec | undefined, seed: string): NarrativeStateV1 {
|
|
62
|
+
const brief = deck?.narrativeBrief
|
|
63
|
+
const id = stableNarrativeId(deck?.slug || seed)
|
|
64
|
+
const claims = migrateClaims(deck)
|
|
65
|
+
const evidenceBindings = migrateEvidenceBindings(deck, claims)
|
|
66
|
+
const withEvidenceStatus = claims.map((claim) => ({ ...claim, evidenceStatus: evidenceStatusForClaim(claim, evidenceBindings) }))
|
|
67
|
+
return {
|
|
68
|
+
version: 1,
|
|
69
|
+
id,
|
|
70
|
+
status: "draft",
|
|
71
|
+
audience: {
|
|
72
|
+
primary: clean(deck?.audience),
|
|
73
|
+
beliefBefore: clean(brief?.audienceBeliefBefore),
|
|
74
|
+
beliefAfter: clean(brief?.audienceBeliefAfter),
|
|
75
|
+
},
|
|
76
|
+
decision: {
|
|
77
|
+
action: clean(brief?.decisionOrAction),
|
|
78
|
+
decisionType: inferDecisionType(brief?.decisionOrAction),
|
|
79
|
+
},
|
|
80
|
+
thesis: migrateThesis(deck),
|
|
81
|
+
claims: withEvidenceStatus,
|
|
82
|
+
claimRelations: [],
|
|
83
|
+
evidenceBindings,
|
|
84
|
+
objections: (brief?.objections ?? []).map((text) => ({ id: stableObjectionId(text), text, priority: "medium" as const })),
|
|
85
|
+
risks: (brief?.risks ?? []).map((text) => ({ id: stableRiskId(text), text, severity: "medium" as const })),
|
|
86
|
+
researchGaps: [],
|
|
87
|
+
approvals: [],
|
|
88
|
+
updatedAt: MIGRATED_UPDATED_AT,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function migrateClaims(deck: DeckSpec | undefined): NarrativeClaim[] {
|
|
93
|
+
const claims: NarrativeClaim[] = []
|
|
94
|
+
for (const text of deck?.narrativeBrief?.keyClaims ?? []) {
|
|
95
|
+
pushClaim(claims, {
|
|
96
|
+
id: stableClaimId(text),
|
|
97
|
+
kind: "recommendation",
|
|
98
|
+
text,
|
|
99
|
+
importance: "central",
|
|
100
|
+
evidenceRequired: true,
|
|
101
|
+
evidenceStatus: "missing",
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const slide of deck?.slides ?? []) {
|
|
106
|
+
for (const item of slideClaimTexts(slide)) {
|
|
107
|
+
pushClaim(claims, {
|
|
108
|
+
id: stableClaimId(item.text),
|
|
109
|
+
kind: claimKindFromSlide(slide),
|
|
110
|
+
text: item.text,
|
|
111
|
+
importance: item.origin === "title" || item.origin === "purpose" ? "background" : "supporting",
|
|
112
|
+
evidenceRequired: isEvidenceRequiredText(item.text, slide),
|
|
113
|
+
evidenceStatus: "missing",
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return claims
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function migrateEvidenceBindings(deck: DeckSpec | undefined, claims: NarrativeClaim[]): NarrativeEvidenceBinding[] {
|
|
121
|
+
const bindings: NarrativeEvidenceBinding[] = []
|
|
122
|
+
for (const slide of deck?.slides ?? []) {
|
|
123
|
+
const slideClaims = slideClaimTexts(slide)
|
|
124
|
+
.map((item) => claims.find((claim) => claim.text === item.text))
|
|
125
|
+
.filter((claim): claim is NarrativeClaim => Boolean(claim))
|
|
126
|
+
const targetClaims = slideClaims.length > 0 ? slideClaims : claims.filter((claim) => claim.importance === "central")
|
|
127
|
+
for (const evidence of slide.evidence ?? []) {
|
|
128
|
+
for (const claim of targetClaims) {
|
|
129
|
+
const binding = evidenceToBinding(evidence, claim.id)
|
|
130
|
+
if (binding) pushBinding(bindings, binding)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return bindings
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function evidenceToBinding(evidence: EvidenceRef, claimId: string): NarrativeEvidenceBinding | undefined {
|
|
138
|
+
const source = clean(evidence.source || evidence.sourcePath || evidence.findingsFile || evidence.url)
|
|
139
|
+
if (!source) return undefined
|
|
140
|
+
const seed = [source, evidence.sourcePath, evidence.findingsFile, evidence.quote, evidence.location, evidence.page, evidence.url, evidence.caveat].filter(Boolean).join("|")
|
|
141
|
+
return {
|
|
142
|
+
id: stableEvidenceId(claimId, seed),
|
|
143
|
+
claimId,
|
|
144
|
+
source,
|
|
145
|
+
sourcePath: clean(evidence.sourcePath),
|
|
146
|
+
findingsFile: clean(evidence.findingsFile),
|
|
147
|
+
quote: clean(evidence.quote),
|
|
148
|
+
location: clean(evidence.location || evidence.page),
|
|
149
|
+
url: clean(evidence.url),
|
|
150
|
+
caveat: clean(evidence.caveat),
|
|
151
|
+
strength: evidence.quote || evidence.location || evidence.page || evidence.url || evidence.findingsFile || evidence.sourcePath ? "partial" : "weak",
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function slideClaimTexts(slide: SlideSpec): Array<{ origin: string; text: string }> {
|
|
156
|
+
return [
|
|
157
|
+
{ origin: "title", text: clean(slide.title) },
|
|
158
|
+
{ origin: "purpose", text: clean(slide.purpose) },
|
|
159
|
+
{ origin: "headline", text: clean(slide.content?.headline) },
|
|
160
|
+
...(slide.content?.body ?? []).map((text) => ({ origin: "body", text: clean(text) })),
|
|
161
|
+
...(slide.content?.bullets ?? []).map((text) => ({ origin: "bullet", text: clean(text) })),
|
|
162
|
+
].filter((item) => item.text.length > 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function migrateThesis(deck: DeckSpec | undefined): NarrativeThesis | undefined {
|
|
166
|
+
const statement = clean(deck?.narrativeBrief?.narrativeArc) || clean(deck?.goal)
|
|
167
|
+
if (!statement) return undefined
|
|
168
|
+
return { id: `thesis:${stableClaimId(statement).replace(/^claim:/, "")}`, statement, confidence: "medium" }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function normalizeAudience(input: Partial<AudienceIntent> | undefined): AudienceIntent {
|
|
172
|
+
return {
|
|
173
|
+
primary: clean(input?.primary),
|
|
174
|
+
secondary: (input?.secondary ?? []).map(clean).filter(Boolean),
|
|
175
|
+
beliefBefore: clean(input?.beliefBefore),
|
|
176
|
+
beliefAfter: clean(input?.beliefAfter),
|
|
177
|
+
decisionContext: clean(input?.decisionContext),
|
|
178
|
+
successCriteria: (input?.successCriteria ?? []).map(clean).filter(Boolean),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeDecision(input: Partial<DecisionIntent> | undefined): DecisionIntent {
|
|
183
|
+
return {
|
|
184
|
+
action: clean(input?.action),
|
|
185
|
+
owner: clean(input?.owner),
|
|
186
|
+
deadline: clean(input?.deadline),
|
|
187
|
+
decisionType: input?.decisionType,
|
|
188
|
+
consequenceOfNoDecision: clean(input?.consequenceOfNoDecision),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizeThesis(input: Partial<NarrativeThesis> | undefined): NarrativeThesis | undefined {
|
|
193
|
+
const statement = clean(input?.statement)
|
|
194
|
+
if (!statement) return undefined
|
|
195
|
+
return {
|
|
196
|
+
id: input?.id?.trim() || `thesis:${stableClaimId(statement).replace(/^claim:/, "")}`,
|
|
197
|
+
statement,
|
|
198
|
+
confidence: input?.confidence ?? "medium",
|
|
199
|
+
caveat: clean(input?.caveat),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function normalizeClaim(input: Partial<NarrativeClaim>): NarrativeClaim | undefined {
|
|
204
|
+
const text = clean(input.text)
|
|
205
|
+
if (!text) return undefined
|
|
206
|
+
return {
|
|
207
|
+
id: input.id?.trim() || stableClaimId(text),
|
|
208
|
+
kind: input.kind ?? "evidence",
|
|
209
|
+
text,
|
|
210
|
+
importance: input.importance ?? "supporting",
|
|
211
|
+
evidenceRequired: input.evidenceRequired ?? true,
|
|
212
|
+
evidenceStatus: input.evidenceStatus ?? "missing",
|
|
213
|
+
supportedScope: clean(input.supportedScope),
|
|
214
|
+
unsupportedScope: clean(input.unsupportedScope),
|
|
215
|
+
caveats: (input.caveats ?? []).map(clean).filter(Boolean),
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeClaimRelation(input: Partial<NarrativeClaimRelation>, claims: NarrativeClaim[]): NarrativeClaimRelation | undefined {
|
|
220
|
+
const fromClaimId = clean(input.fromClaimId)
|
|
221
|
+
const toClaimId = clean(input.toClaimId)
|
|
222
|
+
if (!fromClaimId || !toClaimId || fromClaimId === toClaimId) return undefined
|
|
223
|
+
if (!claims.some((claim) => claim.id === fromClaimId) || !claims.some((claim) => claim.id === toClaimId)) return undefined
|
|
224
|
+
const relation = normalizeClaimRelationType(input.relation)
|
|
225
|
+
return {
|
|
226
|
+
id: input.id?.trim() || stableClaimRelationId(fromClaimId, toClaimId, relation),
|
|
227
|
+
fromClaimId,
|
|
228
|
+
toClaimId,
|
|
229
|
+
relation,
|
|
230
|
+
rationale: clean(input.rationale),
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function normalizeClaimRelationType(input: NarrativeClaimRelationType | undefined): NarrativeClaimRelationType {
|
|
235
|
+
if (input === "supports" || input === "depends_on" || input === "contrasts_with" || input === "constrains" || input === "answers") return input
|
|
236
|
+
return "leads_to"
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function normalizeEvidenceBinding(input: Partial<NarrativeEvidenceBinding>, claims: NarrativeClaim[]): NarrativeEvidenceBinding | undefined {
|
|
240
|
+
const source = clean(input.source || input.sourcePath || input.findingsFile || input.url)
|
|
241
|
+
const claimId = clean(input.claimId)
|
|
242
|
+
if (!source || !claimId || !claims.some((claim) => claim.id === claimId)) return undefined
|
|
243
|
+
const seed = [source, input.sourcePath, input.findingsFile, input.quote, input.location, input.url, input.caveat].filter(Boolean).join("|")
|
|
244
|
+
return {
|
|
245
|
+
id: input.id?.trim() || stableEvidenceId(claimId, seed),
|
|
246
|
+
claimId,
|
|
247
|
+
source,
|
|
248
|
+
sourcePath: clean(input.sourcePath),
|
|
249
|
+
findingsFile: clean(input.findingsFile),
|
|
250
|
+
quote: clean(input.quote),
|
|
251
|
+
location: clean(input.location),
|
|
252
|
+
url: clean(input.url),
|
|
253
|
+
caveat: clean(input.caveat),
|
|
254
|
+
supportScope: clean(input.supportScope),
|
|
255
|
+
unsupportedScope: clean(input.unsupportedScope),
|
|
256
|
+
strength: input.strength ?? "weak",
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeObjection(input: Partial<NarrativeObjection>): NarrativeObjection | undefined {
|
|
261
|
+
const text = clean(input.text)
|
|
262
|
+
if (!text) return undefined
|
|
263
|
+
return { id: input.id?.trim() || stableObjectionId(text), text, claimId: clean(input.claimId), priority: input.priority ?? "medium", response: clean(input.response) }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizeRisk(input: Partial<NarrativeRisk>): NarrativeRisk | undefined {
|
|
267
|
+
const text = clean(input.text)
|
|
268
|
+
if (!text) return undefined
|
|
269
|
+
return { id: input.id?.trim() || stableRiskId(text), text, claimId: clean(input.claimId), severity: input.severity ?? "medium", mitigation: clean(input.mitigation) }
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizeResearchGap(input: Partial<NarrativeResearchGap>): NarrativeResearchGap | undefined {
|
|
273
|
+
const question = clean(input.question)
|
|
274
|
+
if (!question) return undefined
|
|
275
|
+
const targetType = input.targetType ?? "narrative"
|
|
276
|
+
const targetId = clean(input.targetId)
|
|
277
|
+
const now = clean(input.updatedAt) || clean(input.createdAt) || MIGRATED_UPDATED_AT
|
|
278
|
+
const status = input.status ?? "open"
|
|
279
|
+
return {
|
|
280
|
+
id: input.id?.trim() || stableResearchGapId([targetType, targetId, question].filter(Boolean).join("|")),
|
|
281
|
+
targetType,
|
|
282
|
+
targetId,
|
|
283
|
+
question,
|
|
284
|
+
status,
|
|
285
|
+
priority: input.priority ?? "medium",
|
|
286
|
+
findingsFile: clean(input.findingsFile),
|
|
287
|
+
evidenceBindingIds: (input.evidenceBindingIds ?? []).map(clean).filter(Boolean),
|
|
288
|
+
createdFromIssueType: input.createdFromIssueType,
|
|
289
|
+
notes: clean(input.notes),
|
|
290
|
+
createdAt: clean(input.createdAt) || now,
|
|
291
|
+
updatedAt: now,
|
|
292
|
+
closedAt: status === "closed" ? clean(input.closedAt) || now : clean(input.closedAt),
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function evidenceStatusForClaim(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): NarrativeEvidenceStatus {
|
|
297
|
+
if (!claim.evidenceRequired) return "not_required"
|
|
298
|
+
const claimBindings = bindings.filter((binding) => binding.claimId === claim.id)
|
|
299
|
+
if (claimBindings.some((binding) => binding.strength === "strong")) return "supported"
|
|
300
|
+
if (claimBindings.some((binding) => binding.strength === "partial")) return "partial"
|
|
301
|
+
if (claimBindings.some((binding) => binding.strength === "weak")) return "weak"
|
|
302
|
+
return "missing"
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function claimKindFromSlide(slide: SlideSpec): NarrativeClaimKind {
|
|
306
|
+
if (slide.narrativeRole === "recommendation") return "recommendation"
|
|
307
|
+
if (slide.narrativeRole === "risk") return "risk"
|
|
308
|
+
if (slide.narrativeRole === "ask") return "ask"
|
|
309
|
+
if (slide.narrativeRole === "tension") return "problem"
|
|
310
|
+
if (slide.narrativeRole === "context") return "context"
|
|
311
|
+
return "evidence"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function isEvidenceRequiredText(text: string, slide: SlideSpec): boolean {
|
|
315
|
+
if (slide.narrativeRole === "ask" || slide.narrativeRole === "close" || slide.narrativeRole === "appendix") return false
|
|
316
|
+
return /\d|%|\$|market|growth|cagr|tam|risk|recommend|approve|should|must|increase|decrease|增长|市场|风险|建议|投资|批准/i.test(text)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function inferDecisionType(action: string | undefined): DecisionIntent["decisionType"] {
|
|
320
|
+
const text = clean(action).toLowerCase()
|
|
321
|
+
if (!text) return undefined
|
|
322
|
+
if (/approve|批准/.test(text)) return "approve"
|
|
323
|
+
if (/invest|投资/.test(text)) return "invest"
|
|
324
|
+
if (/prioriti[sz]e|优先/.test(text)) return "prioritize"
|
|
325
|
+
if (/align|共识/.test(text)) return "align"
|
|
326
|
+
if (/choose|select|选择/.test(text)) return "choose"
|
|
327
|
+
if (/understand|理解/.test(text)) return "understand"
|
|
328
|
+
return "other"
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function normalizeStatus(status: NarrativeStatus | undefined): NarrativeStatus {
|
|
332
|
+
return status ?? "draft"
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function activeDeck(state: DecksState): DeckSpec | undefined {
|
|
336
|
+
if (state.activeDeck && state.decks[state.activeDeck]) return state.decks[state.activeDeck]
|
|
337
|
+
const keys = Object.keys(state.decks ?? {})
|
|
338
|
+
return keys.length === 1 ? state.decks[keys[0]] : undefined
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function hasCanonicalNarrativeContent(narrative: NarrativeStateV1): boolean {
|
|
342
|
+
return Boolean(narrative.audience.primary || narrative.audience.beliefBefore || narrative.audience.beliefAfter || narrative.decision.action || narrative.thesis || narrative.claims.length > 0)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function pushClaim(claims: NarrativeClaim[], claim: NarrativeClaim): void {
|
|
346
|
+
if (claims.some((item) => item.text === claim.text)) return
|
|
347
|
+
claims.push(claim)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function pushBinding(bindings: NarrativeEvidenceBinding[], binding: NarrativeEvidenceBinding): void {
|
|
351
|
+
if (bindings.some((item) => item.id === binding.id)) return
|
|
352
|
+
bindings.push(binding)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function dedupeById<T extends { id: string }>(items: T[]): T[] {
|
|
356
|
+
return [...new Map(items.map((item) => [item.id, item])).values()]
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function clean(value: string | undefined): string {
|
|
360
|
+
return value?.trim() ?? ""
|
|
361
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { NarrativeBrief } from "../decks-state"
|
|
2
|
+
import type { NarrativeStateV1 } from "./types"
|
|
3
|
+
|
|
4
|
+
export function narrativeToBrief(narrative: NarrativeStateV1): NarrativeBrief {
|
|
5
|
+
return {
|
|
6
|
+
audienceBeliefBefore: narrative.audience.beliefBefore || undefined,
|
|
7
|
+
audienceBeliefAfter: narrative.audience.beliefAfter || undefined,
|
|
8
|
+
decisionOrAction: narrative.decision.action || undefined,
|
|
9
|
+
narrativeArc: narrative.thesis?.statement,
|
|
10
|
+
keyClaims: narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.text),
|
|
11
|
+
objections: narrative.objections.map((objection) => objection.text),
|
|
12
|
+
risks: narrative.risks.map((risk) => risk.text),
|
|
13
|
+
}
|
|
14
|
+
}
|