@cyber-dash-tech/revela 0.11.0 → 0.12.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/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +5 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +13 -3
- package/lib/narrative-state/hash.ts +52 -0
- package/lib/narrative-state/normalize.ts +307 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/readiness.ts +289 -0
- package/lib/narrative-state/render-plan.ts +207 -0
- package/lib/narrative-state/types.ts +139 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/graph.ts +120 -2
- package/lib/workspace-state/types.ts +3 -0
- package/package.json +1 -1
- package/plugin.ts +27 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +180 -2
- package/tools/workspace-scan.ts +14 -1
package/lib/decks-state.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
latestReviewSnapshotForTarget,
|
|
18
18
|
} from "./workspace-state/review-snapshots"
|
|
19
19
|
import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
|
|
20
|
+
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
|
|
21
|
+
import type { NarrativeStateV1 } from "./narrative-state/types"
|
|
20
22
|
|
|
21
23
|
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
22
24
|
|
|
@@ -28,6 +30,7 @@ export type NarrativeRole = "context" | "tension" | "evidence" | "recommendation
|
|
|
28
30
|
export interface DecksState {
|
|
29
31
|
version: 1
|
|
30
32
|
activeDeck?: string
|
|
33
|
+
narrative?: NarrativeStateV1
|
|
31
34
|
workspace: {
|
|
32
35
|
brief?: string
|
|
33
36
|
sourceMaterials: SourceMaterial[]
|
|
@@ -349,15 +352,15 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
|
|
|
349
352
|
}
|
|
350
353
|
|
|
351
354
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
352
|
-
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize:
|
|
355
|
+
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
353
356
|
}
|
|
354
357
|
|
|
355
358
|
export function writeDecksState(workspaceRoot: string, state: DecksState): void {
|
|
356
|
-
writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize:
|
|
359
|
+
writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
357
360
|
}
|
|
358
361
|
|
|
359
362
|
export function readOrCreateDecksState(workspaceRoot: string): DecksState {
|
|
360
|
-
return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize:
|
|
363
|
+
return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
361
364
|
}
|
|
362
365
|
|
|
363
366
|
export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
|
|
@@ -717,6 +720,7 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
717
720
|
const state: DecksState = {
|
|
718
721
|
version: 1,
|
|
719
722
|
activeDeck: input.activeDeck ? normalizeSlug(input.activeDeck) : undefined,
|
|
723
|
+
narrative: normalizeCanonicalNarrativeState(input.narrative, input.activeDeck || "workspace"),
|
|
720
724
|
workspace: {
|
|
721
725
|
brief: input.workspace?.brief,
|
|
722
726
|
sourceMaterials: input.workspace?.sourceMaterials ?? [],
|
|
@@ -745,6 +749,12 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
745
749
|
return state
|
|
746
750
|
}
|
|
747
751
|
|
|
752
|
+
function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
|
|
753
|
+
const state = normalizeDecksState(input)
|
|
754
|
+
if (!state.narrative && currentDeckKey(state)) state.narrative = normalizeNarrativeState(state)
|
|
755
|
+
return state
|
|
756
|
+
}
|
|
757
|
+
|
|
748
758
|
function currentDeckKey(state: DecksState): string | undefined {
|
|
749
759
|
if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
|
|
750
760
|
const keys = Object.keys(state.decks)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import type { NarrativeStateV1 } from "./types"
|
|
3
|
+
|
|
4
|
+
export function stableNarrativeId(seed: string): string {
|
|
5
|
+
return `narrative:${stableHash(seed || "workspace")}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function stableClaimId(text: string): string {
|
|
9
|
+
return `claim:${stableHash(text)}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function stableEvidenceId(claimId: string, seed: string): string {
|
|
13
|
+
return `evidence:${claimId}:${stableHash(seed)}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stableObjectionId(text: string): string {
|
|
17
|
+
return `objection:${stableHash(text)}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stableRiskId(text: string): string {
|
|
21
|
+
return `risk:${stableHash(text)}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function computeNarrativeHash(narrative: NarrativeStateV1): string {
|
|
25
|
+
return stableHash(stableStringify({
|
|
26
|
+
version: narrative.version,
|
|
27
|
+
id: narrative.id,
|
|
28
|
+
audience: narrative.audience,
|
|
29
|
+
decision: narrative.decision,
|
|
30
|
+
thesis: narrative.thesis,
|
|
31
|
+
claims: narrative.claims,
|
|
32
|
+
evidenceBindings: narrative.evidenceBindings,
|
|
33
|
+
objections: narrative.objections,
|
|
34
|
+
risks: narrative.risks,
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function stableHash(input: unknown): string {
|
|
39
|
+
const text = typeof input === "string" ? input : stableStringify(input)
|
|
40
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 12)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableStringify(value: unknown): string {
|
|
44
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
47
|
+
.filter(([, item]) => item !== undefined)
|
|
48
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
49
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(value)
|
|
52
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import type { DeckSpec, DecksState, EvidenceRef, SlideSpec } from "../decks-state"
|
|
2
|
+
import {
|
|
3
|
+
stableClaimId,
|
|
4
|
+
stableEvidenceId,
|
|
5
|
+
stableNarrativeId,
|
|
6
|
+
stableObjectionId,
|
|
7
|
+
stableRiskId,
|
|
8
|
+
} from "./hash"
|
|
9
|
+
import type {
|
|
10
|
+
AudienceIntent,
|
|
11
|
+
DecisionIntent,
|
|
12
|
+
NarrativeClaim,
|
|
13
|
+
NarrativeClaimKind,
|
|
14
|
+
NarrativeEvidenceBinding,
|
|
15
|
+
NarrativeEvidenceStatus,
|
|
16
|
+
NarrativeObjection,
|
|
17
|
+
NarrativeRisk,
|
|
18
|
+
NarrativeStateV1,
|
|
19
|
+
NarrativeStatus,
|
|
20
|
+
NarrativeThesis,
|
|
21
|
+
} from "./types"
|
|
22
|
+
|
|
23
|
+
const MIGRATED_UPDATED_AT = "1970-01-01T00:00:00.000Z"
|
|
24
|
+
|
|
25
|
+
export function normalizeCanonicalNarrativeState(input: Partial<NarrativeStateV1> | undefined, seed = "workspace"): NarrativeStateV1 | undefined {
|
|
26
|
+
if (!input) return undefined
|
|
27
|
+
const id = input.id?.trim() || stableNarrativeId(seed)
|
|
28
|
+
const claims = dedupeById((input.claims ?? []).map(normalizeClaim).filter((claim): claim is NarrativeClaim => Boolean(claim)))
|
|
29
|
+
const evidenceBindings = dedupeById((input.evidenceBindings ?? []).map((binding) => normalizeEvidenceBinding(binding, claims)).filter((binding): binding is NarrativeEvidenceBinding => Boolean(binding)))
|
|
30
|
+
return {
|
|
31
|
+
version: 1,
|
|
32
|
+
id,
|
|
33
|
+
status: normalizeStatus(input.status),
|
|
34
|
+
audience: normalizeAudience(input.audience),
|
|
35
|
+
decision: normalizeDecision(input.decision),
|
|
36
|
+
thesis: normalizeThesis(input.thesis),
|
|
37
|
+
claims: claims.map((claim) => ({ ...claim, evidenceStatus: evidenceStatusForClaim(claim, evidenceBindings) })),
|
|
38
|
+
evidenceBindings,
|
|
39
|
+
objections: dedupeById((input.objections ?? []).map(normalizeObjection).filter((objection): objection is NarrativeObjection => Boolean(objection))),
|
|
40
|
+
risks: dedupeById((input.risks ?? []).map(normalizeRisk).filter((risk): risk is NarrativeRisk => Boolean(risk))),
|
|
41
|
+
approvals: input.approvals ?? [],
|
|
42
|
+
updatedAt: input.updatedAt || MIGRATED_UPDATED_AT,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeNarrativeState(state: DecksState): NarrativeStateV1 {
|
|
47
|
+
const deck = activeDeck(state)
|
|
48
|
+
const existing = normalizeCanonicalNarrativeState(state.narrative, deck?.slug ?? state.activeDeck ?? "workspace")
|
|
49
|
+
if (existing && hasCanonicalNarrativeContent(existing)) return existing
|
|
50
|
+
return migrateDeckNarrative(deck, state.activeDeck ?? "workspace")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function migrateDeckNarrative(deck: DeckSpec | undefined, seed: string): NarrativeStateV1 {
|
|
54
|
+
const brief = deck?.narrativeBrief
|
|
55
|
+
const id = stableNarrativeId(deck?.slug || seed)
|
|
56
|
+
const claims = migrateClaims(deck)
|
|
57
|
+
const evidenceBindings = migrateEvidenceBindings(deck, claims)
|
|
58
|
+
const withEvidenceStatus = claims.map((claim) => ({ ...claim, evidenceStatus: evidenceStatusForClaim(claim, evidenceBindings) }))
|
|
59
|
+
return {
|
|
60
|
+
version: 1,
|
|
61
|
+
id,
|
|
62
|
+
status: "draft",
|
|
63
|
+
audience: {
|
|
64
|
+
primary: clean(deck?.audience),
|
|
65
|
+
beliefBefore: clean(brief?.audienceBeliefBefore),
|
|
66
|
+
beliefAfter: clean(brief?.audienceBeliefAfter),
|
|
67
|
+
},
|
|
68
|
+
decision: {
|
|
69
|
+
action: clean(brief?.decisionOrAction),
|
|
70
|
+
decisionType: inferDecisionType(brief?.decisionOrAction),
|
|
71
|
+
},
|
|
72
|
+
thesis: migrateThesis(deck),
|
|
73
|
+
claims: withEvidenceStatus,
|
|
74
|
+
evidenceBindings,
|
|
75
|
+
objections: (brief?.objections ?? []).map((text) => ({ id: stableObjectionId(text), text, priority: "medium" as const })),
|
|
76
|
+
risks: (brief?.risks ?? []).map((text) => ({ id: stableRiskId(text), text, severity: "medium" as const })),
|
|
77
|
+
approvals: [],
|
|
78
|
+
updatedAt: MIGRATED_UPDATED_AT,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function migrateClaims(deck: DeckSpec | undefined): NarrativeClaim[] {
|
|
83
|
+
const claims: NarrativeClaim[] = []
|
|
84
|
+
for (const text of deck?.narrativeBrief?.keyClaims ?? []) {
|
|
85
|
+
pushClaim(claims, {
|
|
86
|
+
id: stableClaimId(text),
|
|
87
|
+
kind: "recommendation",
|
|
88
|
+
text,
|
|
89
|
+
importance: "central",
|
|
90
|
+
evidenceRequired: true,
|
|
91
|
+
evidenceStatus: "missing",
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const slide of deck?.slides ?? []) {
|
|
96
|
+
for (const item of slideClaimTexts(slide)) {
|
|
97
|
+
pushClaim(claims, {
|
|
98
|
+
id: stableClaimId(item.text),
|
|
99
|
+
kind: claimKindFromSlide(slide),
|
|
100
|
+
text: item.text,
|
|
101
|
+
importance: item.origin === "title" || item.origin === "purpose" ? "background" : "supporting",
|
|
102
|
+
evidenceRequired: isEvidenceRequiredText(item.text, slide),
|
|
103
|
+
evidenceStatus: "missing",
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return claims
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function migrateEvidenceBindings(deck: DeckSpec | undefined, claims: NarrativeClaim[]): NarrativeEvidenceBinding[] {
|
|
111
|
+
const bindings: NarrativeEvidenceBinding[] = []
|
|
112
|
+
for (const slide of deck?.slides ?? []) {
|
|
113
|
+
const slideClaims = slideClaimTexts(slide)
|
|
114
|
+
.map((item) => claims.find((claim) => claim.text === item.text))
|
|
115
|
+
.filter((claim): claim is NarrativeClaim => Boolean(claim))
|
|
116
|
+
const targetClaims = slideClaims.length > 0 ? slideClaims : claims.filter((claim) => claim.importance === "central")
|
|
117
|
+
for (const evidence of slide.evidence ?? []) {
|
|
118
|
+
for (const claim of targetClaims) {
|
|
119
|
+
const binding = evidenceToBinding(evidence, claim.id)
|
|
120
|
+
if (binding) pushBinding(bindings, binding)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return bindings
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function evidenceToBinding(evidence: EvidenceRef, claimId: string): NarrativeEvidenceBinding | undefined {
|
|
128
|
+
const source = clean(evidence.source || evidence.sourcePath || evidence.findingsFile || evidence.url)
|
|
129
|
+
if (!source) return undefined
|
|
130
|
+
const seed = [source, evidence.sourcePath, evidence.findingsFile, evidence.quote, evidence.location, evidence.page, evidence.url, evidence.caveat].filter(Boolean).join("|")
|
|
131
|
+
return {
|
|
132
|
+
id: stableEvidenceId(claimId, seed),
|
|
133
|
+
claimId,
|
|
134
|
+
source,
|
|
135
|
+
sourcePath: clean(evidence.sourcePath),
|
|
136
|
+
findingsFile: clean(evidence.findingsFile),
|
|
137
|
+
quote: clean(evidence.quote),
|
|
138
|
+
location: clean(evidence.location || evidence.page),
|
|
139
|
+
url: clean(evidence.url),
|
|
140
|
+
caveat: clean(evidence.caveat),
|
|
141
|
+
strength: evidence.quote || evidence.location || evidence.page || evidence.url || evidence.findingsFile || evidence.sourcePath ? "partial" : "weak",
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function slideClaimTexts(slide: SlideSpec): Array<{ origin: string; text: string }> {
|
|
146
|
+
return [
|
|
147
|
+
{ origin: "title", text: clean(slide.title) },
|
|
148
|
+
{ origin: "purpose", text: clean(slide.purpose) },
|
|
149
|
+
{ origin: "headline", text: clean(slide.content?.headline) },
|
|
150
|
+
...(slide.content?.body ?? []).map((text) => ({ origin: "body", text: clean(text) })),
|
|
151
|
+
...(slide.content?.bullets ?? []).map((text) => ({ origin: "bullet", text: clean(text) })),
|
|
152
|
+
].filter((item) => item.text.length > 0)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function migrateThesis(deck: DeckSpec | undefined): NarrativeThesis | undefined {
|
|
156
|
+
const statement = clean(deck?.narrativeBrief?.narrativeArc) || clean(deck?.goal)
|
|
157
|
+
if (!statement) return undefined
|
|
158
|
+
return { id: `thesis:${stableClaimId(statement).replace(/^claim:/, "")}`, statement, confidence: "medium" }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function normalizeAudience(input: Partial<AudienceIntent> | undefined): AudienceIntent {
|
|
162
|
+
return {
|
|
163
|
+
primary: clean(input?.primary),
|
|
164
|
+
secondary: (input?.secondary ?? []).map(clean).filter(Boolean),
|
|
165
|
+
beliefBefore: clean(input?.beliefBefore),
|
|
166
|
+
beliefAfter: clean(input?.beliefAfter),
|
|
167
|
+
decisionContext: clean(input?.decisionContext),
|
|
168
|
+
successCriteria: (input?.successCriteria ?? []).map(clean).filter(Boolean),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeDecision(input: Partial<DecisionIntent> | undefined): DecisionIntent {
|
|
173
|
+
return {
|
|
174
|
+
action: clean(input?.action),
|
|
175
|
+
owner: clean(input?.owner),
|
|
176
|
+
deadline: clean(input?.deadline),
|
|
177
|
+
decisionType: input?.decisionType,
|
|
178
|
+
consequenceOfNoDecision: clean(input?.consequenceOfNoDecision),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeThesis(input: Partial<NarrativeThesis> | undefined): NarrativeThesis | undefined {
|
|
183
|
+
const statement = clean(input?.statement)
|
|
184
|
+
if (!statement) return undefined
|
|
185
|
+
return {
|
|
186
|
+
id: input?.id?.trim() || `thesis:${stableClaimId(statement).replace(/^claim:/, "")}`,
|
|
187
|
+
statement,
|
|
188
|
+
confidence: input?.confidence ?? "medium",
|
|
189
|
+
caveat: clean(input?.caveat),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeClaim(input: Partial<NarrativeClaim>): NarrativeClaim | undefined {
|
|
194
|
+
const text = clean(input.text)
|
|
195
|
+
if (!text) return undefined
|
|
196
|
+
return {
|
|
197
|
+
id: input.id?.trim() || stableClaimId(text),
|
|
198
|
+
kind: input.kind ?? "evidence",
|
|
199
|
+
text,
|
|
200
|
+
importance: input.importance ?? "supporting",
|
|
201
|
+
evidenceRequired: input.evidenceRequired ?? true,
|
|
202
|
+
evidenceStatus: input.evidenceStatus ?? "missing",
|
|
203
|
+
supportedScope: clean(input.supportedScope),
|
|
204
|
+
unsupportedScope: clean(input.unsupportedScope),
|
|
205
|
+
caveats: (input.caveats ?? []).map(clean).filter(Boolean),
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeEvidenceBinding(input: Partial<NarrativeEvidenceBinding>, claims: NarrativeClaim[]): NarrativeEvidenceBinding | undefined {
|
|
210
|
+
const source = clean(input.source || input.sourcePath || input.findingsFile || input.url)
|
|
211
|
+
const claimId = clean(input.claimId)
|
|
212
|
+
if (!source || !claimId || !claims.some((claim) => claim.id === claimId)) return undefined
|
|
213
|
+
const seed = [source, input.sourcePath, input.findingsFile, input.quote, input.location, input.url, input.caveat].filter(Boolean).join("|")
|
|
214
|
+
return {
|
|
215
|
+
id: input.id?.trim() || stableEvidenceId(claimId, seed),
|
|
216
|
+
claimId,
|
|
217
|
+
source,
|
|
218
|
+
sourcePath: clean(input.sourcePath),
|
|
219
|
+
findingsFile: clean(input.findingsFile),
|
|
220
|
+
quote: clean(input.quote),
|
|
221
|
+
location: clean(input.location),
|
|
222
|
+
url: clean(input.url),
|
|
223
|
+
caveat: clean(input.caveat),
|
|
224
|
+
supportScope: clean(input.supportScope),
|
|
225
|
+
unsupportedScope: clean(input.unsupportedScope),
|
|
226
|
+
strength: input.strength ?? "weak",
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeObjection(input: Partial<NarrativeObjection>): NarrativeObjection | undefined {
|
|
231
|
+
const text = clean(input.text)
|
|
232
|
+
if (!text) return undefined
|
|
233
|
+
return { id: input.id?.trim() || stableObjectionId(text), text, claimId: clean(input.claimId), priority: input.priority ?? "medium", response: clean(input.response) }
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function normalizeRisk(input: Partial<NarrativeRisk>): NarrativeRisk | undefined {
|
|
237
|
+
const text = clean(input.text)
|
|
238
|
+
if (!text) return undefined
|
|
239
|
+
return { id: input.id?.trim() || stableRiskId(text), text, claimId: clean(input.claimId), severity: input.severity ?? "medium", mitigation: clean(input.mitigation) }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function evidenceStatusForClaim(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): NarrativeEvidenceStatus {
|
|
243
|
+
if (!claim.evidenceRequired) return "not_required"
|
|
244
|
+
const claimBindings = bindings.filter((binding) => binding.claimId === claim.id)
|
|
245
|
+
if (claimBindings.some((binding) => binding.strength === "strong")) return "supported"
|
|
246
|
+
if (claimBindings.some((binding) => binding.strength === "partial")) return "partial"
|
|
247
|
+
if (claimBindings.some((binding) => binding.strength === "weak")) return "weak"
|
|
248
|
+
return "missing"
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function claimKindFromSlide(slide: SlideSpec): NarrativeClaimKind {
|
|
252
|
+
if (slide.narrativeRole === "recommendation") return "recommendation"
|
|
253
|
+
if (slide.narrativeRole === "risk") return "risk"
|
|
254
|
+
if (slide.narrativeRole === "ask") return "ask"
|
|
255
|
+
if (slide.narrativeRole === "tension") return "problem"
|
|
256
|
+
if (slide.narrativeRole === "context") return "context"
|
|
257
|
+
return "evidence"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isEvidenceRequiredText(text: string, slide: SlideSpec): boolean {
|
|
261
|
+
if (slide.narrativeRole === "ask" || slide.narrativeRole === "close" || slide.narrativeRole === "appendix") return false
|
|
262
|
+
return /\d|%|\$|market|growth|cagr|tam|risk|recommend|approve|should|must|increase|decrease|增长|市场|风险|建议|投资|批准/i.test(text)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function inferDecisionType(action: string | undefined): DecisionIntent["decisionType"] {
|
|
266
|
+
const text = clean(action).toLowerCase()
|
|
267
|
+
if (!text) return undefined
|
|
268
|
+
if (/approve|批准/.test(text)) return "approve"
|
|
269
|
+
if (/invest|投资/.test(text)) return "invest"
|
|
270
|
+
if (/prioriti[sz]e|优先/.test(text)) return "prioritize"
|
|
271
|
+
if (/align|共识/.test(text)) return "align"
|
|
272
|
+
if (/choose|select|选择/.test(text)) return "choose"
|
|
273
|
+
if (/understand|理解/.test(text)) return "understand"
|
|
274
|
+
return "other"
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeStatus(status: NarrativeStatus | undefined): NarrativeStatus {
|
|
278
|
+
return status ?? "draft"
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function activeDeck(state: DecksState): DeckSpec | undefined {
|
|
282
|
+
if (state.activeDeck && state.decks[state.activeDeck]) return state.decks[state.activeDeck]
|
|
283
|
+
const keys = Object.keys(state.decks ?? {})
|
|
284
|
+
return keys.length === 1 ? state.decks[keys[0]] : undefined
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function hasCanonicalNarrativeContent(narrative: NarrativeStateV1): boolean {
|
|
288
|
+
return Boolean(narrative.audience.primary || narrative.audience.beliefBefore || narrative.audience.beliefAfter || narrative.decision.action || narrative.thesis || narrative.claims.length > 0)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function pushClaim(claims: NarrativeClaim[], claim: NarrativeClaim): void {
|
|
292
|
+
if (claims.some((item) => item.text === claim.text)) return
|
|
293
|
+
claims.push(claim)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function pushBinding(bindings: NarrativeEvidenceBinding[], binding: NarrativeEvidenceBinding): void {
|
|
297
|
+
if (bindings.some((item) => item.id === binding.id)) return
|
|
298
|
+
bindings.push(binding)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function dedupeById<T extends { id: string }>(items: T[]): T[] {
|
|
302
|
+
return [...new Map(items.map((item) => [item.id, item])).values()]
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function clean(value: string | undefined): string {
|
|
306
|
+
return value?.trim() ?? ""
|
|
307
|
+
}
|
|
@@ -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
|
+
}
|