@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.
Files changed (37) hide show
  1. package/README.md +35 -29
  2. package/README.zh-CN.md +35 -29
  3. package/lib/commands/brief.ts +63 -0
  4. package/lib/commands/designs.ts +2 -2
  5. package/lib/commands/domains.ts +2 -2
  6. package/lib/commands/enable.ts +19 -19
  7. package/lib/commands/help.ts +7 -3
  8. package/lib/commands/init.ts +30 -19
  9. package/lib/commands/narrative.ts +160 -0
  10. package/lib/commands/review.ts +115 -1
  11. package/lib/decks-state.ts +46 -3
  12. package/lib/edit/prompt.ts +3 -0
  13. package/lib/inspection-context/compile.ts +159 -5
  14. package/lib/inspection-context/project.ts +20 -0
  15. package/lib/narrative-state/coverage.ts +100 -0
  16. package/lib/narrative-state/display.ts +219 -0
  17. package/lib/narrative-state/executive-brief.ts +246 -0
  18. package/lib/narrative-state/hash.ts +61 -0
  19. package/lib/narrative-state/map-html.ts +348 -0
  20. package/lib/narrative-state/map.ts +282 -0
  21. package/lib/narrative-state/normalize.ts +361 -0
  22. package/lib/narrative-state/project-compat.ts +14 -0
  23. package/lib/narrative-state/queries.ts +433 -0
  24. package/lib/narrative-state/readiness.ts +359 -0
  25. package/lib/narrative-state/render-plan.ts +250 -0
  26. package/lib/narrative-state/research-gaps.ts +191 -0
  27. package/lib/narrative-state/types.ts +172 -0
  28. package/lib/prompt-builder.ts +59 -26
  29. package/lib/workspace-state/evidence-status.ts +21 -1
  30. package/lib/workspace-state/graph.ts +174 -2
  31. package/lib/workspace-state/types.ts +13 -1
  32. package/package.json +1 -1
  33. package/plugin.ts +58 -2
  34. package/skill/NARRATIVE_SKILL.md +64 -0
  35. package/tools/decks.ts +265 -2
  36. package/tools/narrative-view.ts +84 -0
  37. 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
+ }