@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,433 @@
1
+ import type { DeckSpec, DecksState, EvidenceRef, SlideClaimRefRole, SlideSpec } from "../decks-state"
2
+ import { projectWorkspaceGraph } from "../workspace-state/graph"
3
+ import { artifactNodeIdForRenderTarget } from "../workspace-state/render-targets"
4
+ import type { GraphEdge, RenderTarget } from "../workspace-state/types"
5
+ import { computeNarrativeHash } from "./hash"
6
+ import { normalizeNarrativeState } from "./normalize"
7
+ import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "./types"
8
+
9
+ export interface ClaimEvidenceBoard {
10
+ version: 1
11
+ claims: Record<NarrativeClaim["evidenceStatus"], ClaimEvidenceRecord[]>
12
+ }
13
+
14
+ export interface ClaimEvidenceRecord {
15
+ id: string
16
+ text: string
17
+ kind: NarrativeClaim["kind"]
18
+ importance: NarrativeClaim["importance"]
19
+ evidenceRequired: boolean
20
+ evidenceStatus: NarrativeClaim["evidenceStatus"]
21
+ supportedScope?: string
22
+ unsupportedScope?: string
23
+ caveats: string[]
24
+ evidence: ClaimEvidenceBindingRecord[]
25
+ }
26
+
27
+ export interface ClaimEvidenceBindingRecord {
28
+ id: string
29
+ claimId: string
30
+ source: string
31
+ findingsFile?: string
32
+ sourcePath?: string
33
+ quote?: string
34
+ location?: string
35
+ url?: string
36
+ caveat?: string
37
+ supportScope?: string
38
+ unsupportedScope?: string
39
+ strength: NarrativeEvidenceBinding["strength"]
40
+ }
41
+
42
+ export interface SourceClaimIndexRecord {
43
+ sourceKey: string
44
+ source: string
45
+ findingsFile?: string
46
+ sourcePath?: string
47
+ url?: string
48
+ claims: Array<{
49
+ claimId: string
50
+ claimText: string
51
+ evidenceId: string
52
+ strength: NarrativeEvidenceBinding["strength"]
53
+ supportScope?: string
54
+ unsupportedScope?: string
55
+ caveat?: string
56
+ }>
57
+ }
58
+
59
+ export interface ObjectionRiskClaimIndex {
60
+ objections: Array<{
61
+ id: string
62
+ text: string
63
+ claimId?: string
64
+ claimText?: string
65
+ priority: "high" | "medium" | "low"
66
+ response?: string
67
+ }>
68
+ risks: Array<{
69
+ id: string
70
+ text: string
71
+ claimId?: string
72
+ claimText?: string
73
+ severity: "high" | "medium" | "low"
74
+ mitigation?: string
75
+ }>
76
+ }
77
+
78
+ export interface ArtifactClaimRef {
79
+ artifactId: string
80
+ type: RenderTarget["type"]
81
+ outputPath?: string
82
+ contractStatus?: RenderTarget["contractStatus"]
83
+ sourceNodeIds: string[]
84
+ claimIds: string[]
85
+ narrativeIds: string[]
86
+ slideRefs: ClaimSlideRef[]
87
+ coverageStatus: "current" | "stale" | "partial" | "missing"
88
+ affectedClaimIds: string[]
89
+ missingClaimIds: string[]
90
+ staleReasons: string[]
91
+ stale: boolean
92
+ staleReason?: string
93
+ note?: string
94
+ }
95
+
96
+ export interface ClaimSlideRef {
97
+ claimId: string
98
+ claimText: string
99
+ slideIndex: number
100
+ slideTitle: string
101
+ match: "content" | "evidence" | "metadata"
102
+ role: SlideClaimRefRole
103
+ location: string
104
+ }
105
+
106
+ export function getClaimEvidenceBoard(state: DecksState): ClaimEvidenceBoard {
107
+ const narrative = canonicalNarrative(state)
108
+ const evidenceByClaim = groupEvidenceByClaim(narrative.evidenceBindings)
109
+ const claims: ClaimEvidenceBoard["claims"] = {
110
+ supported: [],
111
+ partial: [],
112
+ weak: [],
113
+ missing: [],
114
+ not_required: [],
115
+ }
116
+
117
+ for (const claim of narrative.claims) {
118
+ claims[claim.evidenceStatus].push({
119
+ id: claim.id,
120
+ text: claim.text,
121
+ kind: claim.kind,
122
+ importance: claim.importance,
123
+ evidenceRequired: claim.evidenceRequired,
124
+ evidenceStatus: claim.evidenceStatus,
125
+ supportedScope: claim.supportedScope,
126
+ unsupportedScope: claim.unsupportedScope,
127
+ caveats: claim.caveats ?? [],
128
+ evidence: evidenceByClaim.get(claim.id) ?? [],
129
+ })
130
+ }
131
+
132
+ for (const group of Object.values(claims)) group.sort((a, b) => claimSortValue(a) - claimSortValue(b) || a.text.localeCompare(b.text))
133
+ return { version: 1, claims }
134
+ }
135
+
136
+ export function getSourceClaimIndex(state: DecksState): SourceClaimIndexRecord[] {
137
+ const narrative = canonicalNarrative(state)
138
+ const claimTextById = new Map(narrative.claims.map((claim) => [claim.id, claim.text]))
139
+ const grouped = new Map<string, SourceClaimIndexRecord>()
140
+
141
+ for (const binding of narrative.evidenceBindings) {
142
+ const sourceKey = binding.findingsFile || binding.sourcePath || binding.url || binding.source
143
+ if (!sourceKey) continue
144
+ const existing = grouped.get(sourceKey) ?? {
145
+ sourceKey,
146
+ source: binding.source,
147
+ findingsFile: binding.findingsFile,
148
+ sourcePath: binding.sourcePath,
149
+ url: binding.url,
150
+ claims: [],
151
+ }
152
+ existing.claims.push({
153
+ claimId: binding.claimId,
154
+ claimText: claimTextById.get(binding.claimId) ?? binding.claimId,
155
+ evidenceId: binding.id,
156
+ strength: binding.strength,
157
+ supportScope: binding.supportScope,
158
+ unsupportedScope: binding.unsupportedScope,
159
+ caveat: binding.caveat,
160
+ })
161
+ grouped.set(sourceKey, existing)
162
+ }
163
+
164
+ return [...grouped.values()]
165
+ .map((item) => ({ ...item, claims: item.claims.sort((a, b) => a.claimText.localeCompare(b.claimText)) }))
166
+ .sort((a, b) => a.sourceKey.localeCompare(b.sourceKey))
167
+ }
168
+
169
+ export function getObjectionRiskClaimIndex(state: DecksState): ObjectionRiskClaimIndex {
170
+ const narrative = canonicalNarrative(state)
171
+ const claimTextById = new Map(narrative.claims.map((claim) => [claim.id, claim.text]))
172
+ return {
173
+ objections: narrative.objections.map((objection) => ({
174
+ id: objection.id,
175
+ text: objection.text,
176
+ claimId: objection.claimId,
177
+ claimText: objection.claimId ? claimTextById.get(objection.claimId) : undefined,
178
+ priority: objection.priority,
179
+ response: objection.response,
180
+ })),
181
+ risks: narrative.risks.map((risk) => ({
182
+ id: risk.id,
183
+ text: risk.text,
184
+ claimId: risk.claimId,
185
+ claimText: risk.claimId ? claimTextById.get(risk.claimId) : undefined,
186
+ severity: risk.severity,
187
+ mitigation: risk.mitigation,
188
+ })),
189
+ }
190
+ }
191
+
192
+ export function getArtifactClaimRefs(state: DecksState): ArtifactClaimRef[] {
193
+ const narrative = canonicalNarrative(state)
194
+ const narrativeHash = computeNarrativeHash(narrative)
195
+ const deck = maybeActiveDeck(state)
196
+ const slideRefs = deck ? getClaimSlideRefs({ ...state, narrative }, deck) : []
197
+ const slideClaimIds = [...new Set(slideRefs.map((ref) => ref.claimId))].sort()
198
+ const rendersFromByArtifact = deck ? rendersFromIndex(projectWorkspaceGraph({ ...state, narrative }).edges) : new Map<string, string[]>()
199
+ const claimIds = new Set(narrative.claims.map((claim) => claim.id))
200
+ const requiredClaimIds = narrative.claims
201
+ .filter((claim) => claim.importance === "central" || claim.evidenceRequired)
202
+ .map((claim) => claim.id)
203
+ const narrativeIds = new Set([narrative.id])
204
+ const htmlCoverageByPath = new Map<string, ClaimSlideRef[]>()
205
+
206
+ return (state.renderTargets ?? [])
207
+ .map((target) => {
208
+ const artifactId = artifactNodeIdForRenderTarget(target)
209
+ const rendersFrom = rendersFromByArtifact.get(artifactId) ?? []
210
+ const targetSlideRefs = claimSlideRefsForTarget(target, slideRefs, htmlCoverageByPath)
211
+ const artifactClaimIds = [...new Set([...target.sourceNodeIds, ...rendersFrom, ...targetSlideRefs.map((ref) => ref.claimId), ...slideClaimIds].filter((id) => claimIds.has(id)))].sort()
212
+ const artifactNarrativeIds = [...new Set([...target.sourceNodeIds, ...rendersFrom].filter((id) => narrativeIds.has(id)))].sort()
213
+ const missingClaimIds = requiredClaimIds.filter((id) => !artifactClaimIds.includes(id)).sort()
214
+ const coverage = coverageState(target, narrativeHash, artifactClaimIds, missingClaimIds, artifactNarrativeIds)
215
+ if (target.type === "html_deck" && target.outputPath) htmlCoverageByPath.set(target.outputPath, targetSlideRefs)
216
+ return {
217
+ artifactId,
218
+ type: target.type,
219
+ outputPath: target.outputPath,
220
+ contractStatus: target.contractStatus,
221
+ sourceNodeIds: target.sourceNodeIds ?? [],
222
+ claimIds: artifactClaimIds,
223
+ narrativeIds: artifactNarrativeIds,
224
+ slideRefs: targetSlideRefs,
225
+ coverageStatus: coverage.status,
226
+ affectedClaimIds: coverage.affectedClaimIds,
227
+ missingClaimIds,
228
+ staleReasons: coverage.reasons,
229
+ stale: coverage.status === "stale",
230
+ staleReason: coverage.reasons[0],
231
+ note: coverage.note,
232
+ }
233
+ })
234
+ .sort((a, b) => artifactSortValue(a.type) - artifactSortValue(b.type) || (a.outputPath ?? a.artifactId).localeCompare(b.outputPath ?? b.artifactId))
235
+ }
236
+
237
+ export function getClaimSlideRefs(state: DecksState, deck: DeckSpec = activeDeck(state)): ClaimSlideRef[] {
238
+ const narrative = canonicalNarrative(state)
239
+ const bindingsByClaim = groupRawEvidenceByClaim(narrative.evidenceBindings)
240
+ const refs: ClaimSlideRef[] = []
241
+ const seen = new Set<string>()
242
+ for (const claim of narrative.claims) {
243
+ for (const slide of deck.slides ?? []) {
244
+ const explicitRef = slide.claimRefs?.find((ref) => ref.claimId === claim.id)
245
+ if (explicitRef) {
246
+ pushSlideRef(refs, seen, claim, slide, "metadata", `claimRefs:${explicitRef.role}`, explicitRef.role)
247
+ continue
248
+ }
249
+ if (slide.claimIds?.includes(claim.id)) {
250
+ pushSlideRef(refs, seen, claim, slide, "metadata", "claimIds", "primary")
251
+ continue
252
+ }
253
+ if (slideEvidenceBindingIdsMatch(slide.evidenceBindingIds ?? [], bindingsByClaim.get(claim.id) ?? [])) {
254
+ pushSlideRef(refs, seen, claim, slide, "metadata", "evidenceBindingIds", "evidence")
255
+ continue
256
+ }
257
+ const contentMatch = slideContentMatch(slide, claim.text)
258
+ if (contentMatch) pushSlideRef(refs, seen, claim, slide, "content", contentMatch, "primary")
259
+ else if (slideEvidenceMatches(slide.evidence ?? [], bindingsByClaim.get(claim.id) ?? [])) pushSlideRef(refs, seen, claim, slide, "evidence", "evidence", "evidence")
260
+ }
261
+ }
262
+ return refs.sort((a, b) => a.slideIndex - b.slideIndex || a.claimText.localeCompare(b.claimText) || a.location.localeCompare(b.location))
263
+ }
264
+
265
+ function canonicalNarrative(state: DecksState): NarrativeStateV1 {
266
+ return state.narrative ?? normalizeNarrativeState(state)
267
+ }
268
+
269
+ function activeDeck(state: DecksState): DeckSpec {
270
+ const key = state.activeDeck || (Object.keys(state.decks).length === 1 ? Object.keys(state.decks)[0] : undefined)
271
+ if (!key || !state.decks[key]) throw new Error("No active deck is available for narrative artifact coverage.")
272
+ return state.decks[key]
273
+ }
274
+
275
+ function maybeActiveDeck(state: DecksState): DeckSpec | undefined {
276
+ const key = state.activeDeck || (Object.keys(state.decks).length === 1 ? Object.keys(state.decks)[0] : undefined)
277
+ return key ? state.decks[key] : undefined
278
+ }
279
+
280
+ function groupEvidenceByClaim(bindings: NarrativeEvidenceBinding[]): Map<string, ClaimEvidenceBindingRecord[]> {
281
+ const grouped = new Map<string, ClaimEvidenceBindingRecord[]>()
282
+ for (const binding of bindings) {
283
+ grouped.set(binding.claimId, [...(grouped.get(binding.claimId) ?? []), {
284
+ id: binding.id,
285
+ claimId: binding.claimId,
286
+ source: binding.source,
287
+ findingsFile: binding.findingsFile,
288
+ sourcePath: binding.sourcePath,
289
+ quote: binding.quote,
290
+ location: binding.location,
291
+ url: binding.url,
292
+ caveat: binding.caveat,
293
+ supportScope: binding.supportScope,
294
+ unsupportedScope: binding.unsupportedScope,
295
+ strength: binding.strength,
296
+ }])
297
+ }
298
+ return grouped
299
+ }
300
+
301
+ function groupRawEvidenceByClaim(bindings: NarrativeEvidenceBinding[]): Map<string, NarrativeEvidenceBinding[]> {
302
+ const grouped = new Map<string, NarrativeEvidenceBinding[]>()
303
+ for (const binding of bindings) grouped.set(binding.claimId, [...(grouped.get(binding.claimId) ?? []), binding])
304
+ return grouped
305
+ }
306
+
307
+ function slideContentMatch(slide: SlideSpec, claimText: string): string | undefined {
308
+ const claim = normalizeText(claimText)
309
+ if (!claim) return undefined
310
+ const candidates: Array<[string, string | undefined]> = [
311
+ ["title", slide.title],
312
+ ["purpose", slide.purpose],
313
+ ["headline", slide.content?.headline],
314
+ ["speakerNotes", slide.content?.speakerNotes],
315
+ ...((slide.content?.body ?? []).map((item, index) => [`body:${index + 1}`, item] as [string, string])),
316
+ ...((slide.content?.bullets ?? []).map((item, index) => [`bullet:${index + 1}`, item] as [string, string])),
317
+ ]
318
+ for (const [location, value] of candidates) {
319
+ const normalized = normalizeText(value)
320
+ if (!normalized) continue
321
+ if (normalized === claim || normalized.includes(claim) || claim.includes(normalized)) return location
322
+ }
323
+ return undefined
324
+ }
325
+
326
+ function slideEvidenceMatches(evidence: EvidenceRef[], bindings: NarrativeEvidenceBinding[]): boolean {
327
+ return evidence.some((item) => bindings.some((binding) => evidenceMatchesBinding(item, binding)))
328
+ }
329
+
330
+ function slideEvidenceBindingIdsMatch(ids: string[], bindings: NarrativeEvidenceBinding[]): boolean {
331
+ const slideIds = new Set(ids)
332
+ return bindings.some((binding) => slideIds.has(binding.id))
333
+ }
334
+
335
+ function evidenceMatchesBinding(evidence: EvidenceRef, binding: NarrativeEvidenceBinding): boolean {
336
+ return Boolean(
337
+ evidence.findingsFile && evidence.findingsFile === binding.findingsFile ||
338
+ evidence.sourcePath && evidence.sourcePath === binding.sourcePath ||
339
+ evidence.url && evidence.url === binding.url ||
340
+ evidence.quote && binding.quote && normalizeText(evidence.quote) === normalizeText(binding.quote) ||
341
+ evidence.source && normalizeText(evidence.source) === normalizeText(binding.source),
342
+ )
343
+ }
344
+
345
+ function pushSlideRef(refs: ClaimSlideRef[], seen: Set<string>, claim: NarrativeClaim, slide: SlideSpec, match: ClaimSlideRef["match"], location: string, role: SlideClaimRefRole): void {
346
+ const key = `${claim.id}:${slide.index}:${match}:${location}:${role}`
347
+ if (seen.has(key)) return
348
+ seen.add(key)
349
+ refs.push({ claimId: claim.id, claimText: claim.text, slideIndex: slide.index, slideTitle: slide.title, match, role, location })
350
+ }
351
+
352
+ function claimSlideRefsForTarget(target: RenderTarget, currentHtmlRefs: ClaimSlideRef[], htmlCoverageByPath: Map<string, ClaimSlideRef[]>): ClaimSlideRef[] {
353
+ const stored = parseStoredClaimSlideRefs(target)
354
+ if (stored.length > 0) return stored
355
+ if (target.type === "html_deck") return currentHtmlRefs
356
+ const sourceOutputPath = typeof target.data?.sourceOutputPath === "string" ? target.data.sourceOutputPath : undefined
357
+ if (sourceOutputPath) return htmlCoverageByPath.get(sourceOutputPath) ?? currentHtmlRefs
358
+ return currentHtmlRefs
359
+ }
360
+
361
+ function parseStoredClaimSlideRefs(target: RenderTarget): ClaimSlideRef[] {
362
+ const value = target.data?.claimSlideRefs
363
+ if (!Array.isArray(value)) return []
364
+ return value.flatMap((item): ClaimSlideRef[] => {
365
+ if (!item || typeof item !== "object") return []
366
+ const record = item as Partial<ClaimSlideRef>
367
+ if (!record.claimId || !record.claimText || typeof record.slideIndex !== "number" || !record.slideTitle) return []
368
+ return [{
369
+ claimId: record.claimId,
370
+ claimText: record.claimText,
371
+ slideIndex: record.slideIndex,
372
+ slideTitle: record.slideTitle,
373
+ match: record.match ?? "metadata",
374
+ role: record.role ?? "supporting",
375
+ location: record.location ?? "metadata",
376
+ }]
377
+ }).sort((a, b) => a.slideIndex - b.slideIndex || a.claimText.localeCompare(b.claimText))
378
+ }
379
+
380
+ function coverageState(
381
+ target: RenderTarget,
382
+ narrativeHash: string,
383
+ artifactClaimIds: string[],
384
+ missingClaimIds: string[],
385
+ artifactNarrativeIds: string[],
386
+ ): { status: ArtifactClaimRef["coverageStatus"]; reasons: string[]; affectedClaimIds: string[]; note?: string } {
387
+ const targetHash = typeof target.data?.narrativeHash === "string" ? target.data.narrativeHash : undefined
388
+ const reasons: string[] = []
389
+ if (targetHash && targetHash !== narrativeHash) reasons.push("Narrative hash changed after this artifact coverage was recorded.")
390
+ if (!targetHash) reasons.push("Artifact does not record the narrative hash used for coverage.")
391
+ if (artifactClaimIds.length === 0) reasons.push("No claim-to-slide coverage is recorded or inferred for this artifact.")
392
+ if (missingClaimIds.length > 0) reasons.push(`Artifact does not cover ${missingClaimIds.length} central or evidence-required claim${missingClaimIds.length === 1 ? "" : "s"}.`)
393
+ if (artifactNarrativeIds.length === 0 && target.type !== "pdf" && target.type !== "pptx") reasons.push("Artifact is not linked to the current canonical narrative.")
394
+
395
+ if (targetHash && targetHash !== narrativeHash) {
396
+ return {
397
+ status: "stale",
398
+ reasons,
399
+ affectedClaimIds: artifactClaimIds.length > 0 ? artifactClaimIds : missingClaimIds,
400
+ note: undefined,
401
+ }
402
+ }
403
+ if (artifactClaimIds.length === 0) return { status: "missing", reasons, affectedClaimIds: [], note: reasons[0] }
404
+ if (!targetHash || missingClaimIds.length > 0) return { status: "partial", reasons, affectedClaimIds: missingClaimIds, note: reasons[0] }
405
+ return { status: "current", reasons: [], affectedClaimIds: [], note: undefined }
406
+ }
407
+
408
+ function rendersFromIndex(edges: GraphEdge[]): Map<string, string[]> {
409
+ const grouped = new Map<string, string[]>()
410
+ for (const edge of edges) {
411
+ if (edge.type !== "renders_from" || !edge.from.startsWith("artifact:")) continue
412
+ grouped.set(edge.from, [...(grouped.get(edge.from) ?? []), edge.to])
413
+ }
414
+ return grouped
415
+ }
416
+
417
+ function claimSortValue(claim: Pick<ClaimEvidenceRecord, "importance">): number {
418
+ if (claim.importance === "central") return 0
419
+ if (claim.importance === "supporting") return 1
420
+ return 2
421
+ }
422
+
423
+ function artifactSortValue(type: RenderTarget["type"]): number {
424
+ if (type === "html_deck") return 0
425
+ if (type === "pdf") return 1
426
+ if (type === "pptx") return 2
427
+ if (type === "executive_brief" || type === "brief") return 3
428
+ return 3
429
+ }
430
+
431
+ function normalizeText(value: string | undefined): string {
432
+ return value?.replace(/\s+/g, " ").trim().toLowerCase() ?? ""
433
+ }