@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, NarrativeRole, SlideSpec, SourceMaterial } from "../decks-state"
|
|
2
|
+
import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
|
|
2
3
|
|
|
3
|
-
export type InspectionClaimOrigin = "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
|
+
export type InspectionClaimOrigin = "narrative" | "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
5
|
export type InspectionGapType = "missing_evidence" | "weak_evidence"
|
|
5
6
|
export type InspectionEvidenceSupport = "supported" | "weak" | "unknown"
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ export interface InspectionContext {
|
|
|
13
14
|
outputPath: string
|
|
14
15
|
narrativeBrief?: NarrativeBrief
|
|
15
16
|
sourceMaterials: InspectionSourceMaterial[]
|
|
17
|
+
narrative?: InspectionNarrativeStateContext
|
|
16
18
|
slides: InspectionSlideContext[]
|
|
17
19
|
gaps: InspectionGap[]
|
|
18
20
|
appendixCandidates: InspectionAppendixCandidate[]
|
|
@@ -20,6 +22,12 @@ export interface InspectionContext {
|
|
|
20
22
|
riskContext: InspectionNarrativeContext[]
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export interface InspectionNarrativeStateContext {
|
|
26
|
+
id: string
|
|
27
|
+
status: string
|
|
28
|
+
claimCount: number
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export interface InspectionSourceMaterial extends SourceMaterial {
|
|
24
32
|
linkedEvidenceCount: number
|
|
25
33
|
}
|
|
@@ -46,6 +54,7 @@ export interface InspectionSlideText {
|
|
|
46
54
|
|
|
47
55
|
export interface InspectionClaimCandidate {
|
|
48
56
|
id: string
|
|
57
|
+
canonicalClaimId?: string
|
|
49
58
|
slideIndex: number
|
|
50
59
|
slideTitle: string
|
|
51
60
|
origin: InspectionClaimOrigin
|
|
@@ -54,9 +63,18 @@ export interface InspectionClaimCandidate {
|
|
|
54
63
|
evidenceSupport: InspectionEvidenceSupport
|
|
55
64
|
evidence: InspectionEvidenceTrace[]
|
|
56
65
|
gaps: InspectionGap[]
|
|
66
|
+
evidenceBindingIds: string[]
|
|
67
|
+
supportedScope?: string
|
|
68
|
+
unsupportedScope?: string
|
|
69
|
+
caveats: string[]
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export interface InspectionEvidenceTrace extends EvidenceRef {
|
|
73
|
+
evidenceBindingId?: string
|
|
74
|
+
claimId?: string
|
|
75
|
+
supportScope?: string
|
|
76
|
+
unsupportedScope?: string
|
|
77
|
+
strength?: NarrativeEvidenceBinding["strength"]
|
|
60
78
|
slideIndex: number
|
|
61
79
|
slideTitle: string
|
|
62
80
|
hasDetail: boolean
|
|
@@ -87,12 +105,13 @@ export interface InspectionNarrativeContext {
|
|
|
87
105
|
|
|
88
106
|
export function compileInspectionContext(state: DecksState, slug?: string): InspectionContext {
|
|
89
107
|
const deck = activeDeck(state, slug)
|
|
108
|
+
const narrative = state.narrative
|
|
90
109
|
const evidence = collectEvidence(deck)
|
|
91
110
|
const sourceMaterials = compileSourceMaterials(state.workspace.sourceMaterials ?? [], evidence)
|
|
92
111
|
const slides = deck.slides
|
|
93
112
|
.slice()
|
|
94
113
|
.sort((a, b) => a.index - b.index)
|
|
95
|
-
.map((slide) => compileSlide(slide))
|
|
114
|
+
.map((slide) => compileSlide(slide, narrative))
|
|
96
115
|
const gaps = slides.flatMap((slide) => slide.claims.flatMap((claim) => claim.gaps))
|
|
97
116
|
|
|
98
117
|
return {
|
|
@@ -103,6 +122,7 @@ export function compileInspectionContext(state: DecksState, slug?: string): Insp
|
|
|
103
122
|
language: deck.language,
|
|
104
123
|
outputPath: deck.outputPath,
|
|
105
124
|
narrativeBrief: deck.narrativeBrief,
|
|
125
|
+
narrative: narrative ? { id: narrative.id, status: narrative.status, claimCount: narrative.claims.length } : undefined,
|
|
106
126
|
sourceMaterials,
|
|
107
127
|
slides,
|
|
108
128
|
gaps,
|
|
@@ -118,9 +138,15 @@ function activeDeck(state: DecksState, slug?: string): DeckSpec {
|
|
|
118
138
|
return state.decks[key]
|
|
119
139
|
}
|
|
120
140
|
|
|
121
|
-
function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
141
|
+
function compileSlide(slide: SlideSpec, narrative: NarrativeStateV1 | undefined): InspectionSlideContext {
|
|
122
142
|
const evidence = slide.evidence.map((item) => compileEvidence(slide, item))
|
|
123
|
-
const
|
|
143
|
+
const canonicalClaims = narrative ? canonicalClaimCandidates(slide, narrative, evidence) : []
|
|
144
|
+
const canonicalText = new Set(canonicalClaims.map((claim) => normalizeText(claim.text)))
|
|
145
|
+
const heuristicClaims = claimCandidates(slide)
|
|
146
|
+
.filter((claim) => !canonicalText.has(normalizeText(claim.text)))
|
|
147
|
+
.map((claim, position) => compileClaim(slide, claim, position, evidence))
|
|
148
|
+
const claims = [...canonicalClaims, ...heuristicClaims]
|
|
149
|
+
const claimCaveats = canonicalClaims.flatMap((claim) => claim.caveats)
|
|
124
150
|
return {
|
|
125
151
|
index: slide.index,
|
|
126
152
|
title: slide.title,
|
|
@@ -136,7 +162,61 @@ function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
|
136
162
|
},
|
|
137
163
|
claims,
|
|
138
164
|
evidence,
|
|
139
|
-
caveats:
|
|
165
|
+
caveats: dedupeText([
|
|
166
|
+
...evidence.map((item) => item.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
167
|
+
...claimCaveats,
|
|
168
|
+
]),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function canonicalClaimCandidates(slide: SlideSpec, narrative: NarrativeStateV1, slideEvidence: InspectionEvidenceTrace[]): InspectionClaimCandidate[] {
|
|
173
|
+
const claimRefs = slide.claimRefs ?? []
|
|
174
|
+
const metadataClaimIds = new Set([
|
|
175
|
+
...claimRefs.map((ref) => ref.claimId),
|
|
176
|
+
...(slide.claimIds ?? []),
|
|
177
|
+
].filter(Boolean))
|
|
178
|
+
const evidenceBindingIds = new Set(slide.evidenceBindingIds ?? [])
|
|
179
|
+
for (const binding of narrative.evidenceBindings) {
|
|
180
|
+
if (evidenceBindingIds.has(binding.id)) metadataClaimIds.add(binding.claimId)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return narrative.claims
|
|
184
|
+
.filter((claim) => metadataClaimIds.has(claim.id))
|
|
185
|
+
.map((claim) => compileCanonicalClaim(slide, claim, narrative.evidenceBindings, slideEvidence, evidenceBindingIds))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function compileCanonicalClaim(
|
|
189
|
+
slide: SlideSpec,
|
|
190
|
+
claim: NarrativeClaim,
|
|
191
|
+
bindings: NarrativeEvidenceBinding[],
|
|
192
|
+
slideEvidence: InspectionEvidenceTrace[],
|
|
193
|
+
slideEvidenceBindingIds: Set<string>,
|
|
194
|
+
): InspectionClaimCandidate {
|
|
195
|
+
const allClaimBindings = bindings.filter((binding) => binding.claimId === claim.id)
|
|
196
|
+
const selectedBindings = allClaimBindings.filter((binding) => slideEvidenceBindingIds.size === 0 || slideEvidenceBindingIds.has(binding.id))
|
|
197
|
+
const evidenceBindings = selectedBindings.length > 0 ? selectedBindings : allClaimBindings
|
|
198
|
+
const evidence = evidenceBindings.length > 0
|
|
199
|
+
? evidenceBindings.map((binding) => compileEvidenceBinding(slide, binding))
|
|
200
|
+
: slideEvidence
|
|
201
|
+
const gaps = canonicalClaimGaps(slide, claim, evidence)
|
|
202
|
+
return {
|
|
203
|
+
id: claim.id,
|
|
204
|
+
canonicalClaimId: claim.id,
|
|
205
|
+
slideIndex: slide.index,
|
|
206
|
+
slideTitle: slide.title,
|
|
207
|
+
origin: "narrative",
|
|
208
|
+
text: claim.text,
|
|
209
|
+
evidenceSensitive: claim.evidenceRequired || isEvidenceSensitiveClaim(claim.text),
|
|
210
|
+
evidenceSupport: narrativeEvidenceSupport(claim, evidence),
|
|
211
|
+
evidence,
|
|
212
|
+
gaps,
|
|
213
|
+
evidenceBindingIds: evidenceBindings.map((binding) => binding.id),
|
|
214
|
+
supportedScope: claim.supportedScope,
|
|
215
|
+
unsupportedScope: claim.unsupportedScope,
|
|
216
|
+
caveats: dedupeText([
|
|
217
|
+
...(claim.caveats ?? []),
|
|
218
|
+
...evidenceBindings.map((binding) => binding.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
219
|
+
]),
|
|
140
220
|
}
|
|
141
221
|
}
|
|
142
222
|
|
|
@@ -159,7 +239,34 @@ function compileClaim(
|
|
|
159
239
|
evidenceSupport: evidenceSupport(evidence),
|
|
160
240
|
evidence,
|
|
161
241
|
gaps,
|
|
242
|
+
evidenceBindingIds: [],
|
|
243
|
+
caveats: [],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function canonicalClaimGaps(slide: SlideSpec, claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
248
|
+
if (!claim.evidenceRequired) return []
|
|
249
|
+
if (claim.evidenceStatus === "missing" || evidence.length === 0) {
|
|
250
|
+
return [{
|
|
251
|
+
type: "missing_evidence",
|
|
252
|
+
slideIndex: slide.index,
|
|
253
|
+
slideTitle: slide.title,
|
|
254
|
+
claimId: claim.id,
|
|
255
|
+
claimText: claim.text,
|
|
256
|
+
message: "Canonical narrative claim requires evidence but has no bound evidence trace.",
|
|
257
|
+
}]
|
|
162
258
|
}
|
|
259
|
+
if (claim.evidenceStatus === "weak" || evidence.some((item) => !item.hasDetail)) {
|
|
260
|
+
return [{
|
|
261
|
+
type: "weak_evidence",
|
|
262
|
+
slideIndex: slide.index,
|
|
263
|
+
slideTitle: slide.title,
|
|
264
|
+
claimId: claim.id,
|
|
265
|
+
claimText: claim.text,
|
|
266
|
+
message: "Canonical narrative claim has weak or source-only evidence trace.",
|
|
267
|
+
}]
|
|
268
|
+
}
|
|
269
|
+
return []
|
|
163
270
|
}
|
|
164
271
|
|
|
165
272
|
function claimGaps(slide: SlideSpec, claimId: string, claimText: string, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
@@ -192,6 +299,12 @@ function evidenceSupport(evidence: InspectionEvidenceTrace[]): InspectionEvidenc
|
|
|
192
299
|
return "supported"
|
|
193
300
|
}
|
|
194
301
|
|
|
302
|
+
function narrativeEvidenceSupport(claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionEvidenceSupport {
|
|
303
|
+
if (claim.evidenceStatus === "supported" || claim.evidenceStatus === "not_required") return "supported"
|
|
304
|
+
if (claim.evidenceStatus === "partial" || claim.evidenceStatus === "weak") return "weak"
|
|
305
|
+
return evidenceSupport(evidence)
|
|
306
|
+
}
|
|
307
|
+
|
|
195
308
|
function claimCandidates(slide: SlideSpec): Array<{ origin: InspectionClaimOrigin; text: string }> {
|
|
196
309
|
const claims: Array<{ origin: InspectionClaimOrigin; text: string }> = []
|
|
197
310
|
pushClaim(claims, "title", slide.title)
|
|
@@ -218,6 +331,29 @@ function compileEvidence(slide: SlideSpec, evidence: EvidenceRef): InspectionEvi
|
|
|
218
331
|
}
|
|
219
332
|
}
|
|
220
333
|
|
|
334
|
+
function compileEvidenceBinding(slide: SlideSpec, binding: NarrativeEvidenceBinding): InspectionEvidenceTrace {
|
|
335
|
+
const evidence: EvidenceRef = {
|
|
336
|
+
source: binding.source,
|
|
337
|
+
sourcePath: binding.sourcePath,
|
|
338
|
+
findingsFile: binding.findingsFile,
|
|
339
|
+
quote: binding.quote,
|
|
340
|
+
location: binding.location,
|
|
341
|
+
url: binding.url,
|
|
342
|
+
caveat: binding.caveat,
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...evidence,
|
|
346
|
+
evidenceBindingId: binding.id,
|
|
347
|
+
claimId: binding.claimId,
|
|
348
|
+
supportScope: binding.supportScope,
|
|
349
|
+
unsupportedScope: binding.unsupportedScope,
|
|
350
|
+
strength: binding.strength,
|
|
351
|
+
slideIndex: slide.index,
|
|
352
|
+
slideTitle: slide.title,
|
|
353
|
+
hasDetail: hasEvidenceDetail(evidence),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
221
357
|
function collectEvidence(deck: DeckSpec): InspectionEvidenceTrace[] {
|
|
222
358
|
return deck.slides.flatMap((slide) => slide.evidence.map((item) => compileEvidence(slide, item)))
|
|
223
359
|
}
|
|
@@ -303,6 +439,24 @@ function cleanOptionalText(value: string | undefined): string | undefined {
|
|
|
303
439
|
return text || undefined
|
|
304
440
|
}
|
|
305
441
|
|
|
442
|
+
function dedupeText(values: string[]): string[] {
|
|
443
|
+
const seen = new Set<string>()
|
|
444
|
+
const result: string[] = []
|
|
445
|
+
for (const value of values) {
|
|
446
|
+
const cleaned = cleanOptionalText(value)
|
|
447
|
+
if (!cleaned) continue
|
|
448
|
+
const key = normalizeText(cleaned)
|
|
449
|
+
if (seen.has(key)) continue
|
|
450
|
+
seen.add(key)
|
|
451
|
+
result.push(cleaned)
|
|
452
|
+
}
|
|
453
|
+
return result
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeText(value: string | undefined): string {
|
|
457
|
+
return value?.replace(/\s+/g, " ").trim().toLowerCase() ?? ""
|
|
458
|
+
}
|
|
459
|
+
|
|
306
460
|
const EVIDENCE_SENSITIVE_TERMS = [
|
|
307
461
|
/\bmarket size\b/,
|
|
308
462
|
/\bcagr\b/,
|
|
@@ -50,10 +50,15 @@ export interface InspectionProjectionMatch {
|
|
|
50
50
|
}
|
|
51
51
|
claim?: {
|
|
52
52
|
id: string
|
|
53
|
+
canonicalClaimId?: string
|
|
53
54
|
origin: string
|
|
54
55
|
text: string
|
|
55
56
|
evidenceSensitive: boolean
|
|
56
57
|
evidenceSupport: string
|
|
58
|
+
evidenceBindingIds: string[]
|
|
59
|
+
supportedScope?: string
|
|
60
|
+
unsupportedScope?: string
|
|
61
|
+
caveats: string[]
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -97,6 +102,8 @@ export interface InspectionAppendixProjection {
|
|
|
97
102
|
|
|
98
103
|
export interface InspectionEvidenceProjectionTrace {
|
|
99
104
|
source: string
|
|
105
|
+
evidenceBindingId?: string
|
|
106
|
+
claimId?: string
|
|
100
107
|
sourcePath?: string
|
|
101
108
|
findingsFile?: string
|
|
102
109
|
location?: string
|
|
@@ -104,6 +111,9 @@ export interface InspectionEvidenceProjectionTrace {
|
|
|
104
111
|
url?: string
|
|
105
112
|
quote?: string
|
|
106
113
|
caveat?: string
|
|
114
|
+
supportScope?: string
|
|
115
|
+
unsupportedScope?: string
|
|
116
|
+
strength?: string
|
|
107
117
|
extractedTextPath?: string
|
|
108
118
|
extractedManifestPath?: string
|
|
109
119
|
hasDetail: boolean
|
|
@@ -163,10 +173,15 @@ export function projectInspectionMatch(
|
|
|
163
173
|
claim: claim
|
|
164
174
|
? {
|
|
165
175
|
id: claim.id,
|
|
176
|
+
canonicalClaimId: claim.canonicalClaimId,
|
|
166
177
|
origin: claim.origin,
|
|
167
178
|
text: truncate(claim.text, 500),
|
|
168
179
|
evidenceSensitive: claim.evidenceSensitive,
|
|
169
180
|
evidenceSupport: claim.evidenceSupport,
|
|
181
|
+
evidenceBindingIds: claim.evidenceBindingIds,
|
|
182
|
+
supportedScope: truncateOptional(claim.supportedScope, 280),
|
|
183
|
+
unsupportedScope: truncateOptional(claim.unsupportedScope, 280),
|
|
184
|
+
caveats: claim.caveats.map((item) => truncate(item, 280)).slice(0, 8),
|
|
170
185
|
}
|
|
171
186
|
: undefined,
|
|
172
187
|
},
|
|
@@ -211,6 +226,8 @@ export function projectInspectionMatch(
|
|
|
211
226
|
function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenceProjectionTrace {
|
|
212
227
|
return {
|
|
213
228
|
source: truncate(trace.source, 180),
|
|
229
|
+
evidenceBindingId: truncateOptional(trace.evidenceBindingId, 160),
|
|
230
|
+
claimId: truncateOptional(trace.claimId, 160),
|
|
214
231
|
sourcePath: truncateOptional(trace.sourcePath, 220),
|
|
215
232
|
findingsFile: truncateOptional(trace.findingsFile, 220),
|
|
216
233
|
location: truncateOptional(trace.location, 120),
|
|
@@ -218,6 +235,9 @@ function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenc
|
|
|
218
235
|
url: truncateOptional(trace.url, 240),
|
|
219
236
|
quote: truncateOptional(trace.quote, 500),
|
|
220
237
|
caveat: truncateOptional(trace.caveat, 280),
|
|
238
|
+
supportScope: truncateOptional(trace.supportScope, 280),
|
|
239
|
+
unsupportedScope: truncateOptional(trace.unsupportedScope, 280),
|
|
240
|
+
strength: trace.strength,
|
|
221
241
|
extractedTextPath: truncateOptional(trace.extractedTextPath, 220),
|
|
222
242
|
extractedManifestPath: truncateOptional(trace.extractedManifestPath, 220),
|
|
223
243
|
hasDetail: trace.hasDetail,
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type DecksState, type SlideClaimRef, type SlideClaimRefRole } from "../decks-state"
|
|
2
|
+
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
3
|
+
import { ensureActiveHtmlDeckRenderTarget } from "../workspace-state/render-targets"
|
|
4
|
+
import { computeNarrativeHash } from "./hash"
|
|
5
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
6
|
+
import { getClaimSlideRefs, type ClaimSlideRef } from "./queries"
|
|
7
|
+
|
|
8
|
+
export interface BackfillSlideClaimRefsResult {
|
|
9
|
+
updated: boolean
|
|
10
|
+
addedCount: number
|
|
11
|
+
slideCount: number
|
|
12
|
+
narrativeHash: string
|
|
13
|
+
refs: ClaimSlideRef[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function backfillSlideClaimRefsFromCoverage(state: DecksState): { state: DecksState; result: BackfillSlideClaimRefsResult } {
|
|
17
|
+
const narrative = normalizeNarrativeState(state)
|
|
18
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
19
|
+
const deckKey = state.activeDeck || Object.keys(state.decks)[0]
|
|
20
|
+
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
21
|
+
if (!deck) {
|
|
22
|
+
return { state: { ...state, narrative }, result: { updated: false, addedCount: 0, slideCount: 0, narrativeHash, refs: [] } }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const refs = getClaimSlideRefs({ ...state, narrative }, deck)
|
|
26
|
+
const refsBySlide = new Map<number, ClaimSlideRef[]>()
|
|
27
|
+
for (const ref of refs) refsBySlide.set(ref.slideIndex, [...(refsBySlide.get(ref.slideIndex) ?? []), ref])
|
|
28
|
+
|
|
29
|
+
let addedCount = 0
|
|
30
|
+
const slides = deck.slides.map((slide) => {
|
|
31
|
+
const existing = [...(slide.claimRefs ?? [])]
|
|
32
|
+
const seen = new Set(existing.map((ref) => `${ref.claimId}:${ref.role}`))
|
|
33
|
+
const additions: SlideClaimRef[] = []
|
|
34
|
+
for (const ref of refsBySlide.get(slide.index) ?? []) {
|
|
35
|
+
const role = backfilledRole(ref.role)
|
|
36
|
+
const key = `${ref.claimId}:${role}`
|
|
37
|
+
if (seen.has(key)) continue
|
|
38
|
+
seen.add(key)
|
|
39
|
+
additions.push({ claimId: ref.claimId, role, note: backfillNote(ref) })
|
|
40
|
+
}
|
|
41
|
+
if (additions.length === 0) return slide
|
|
42
|
+
addedCount += additions.length
|
|
43
|
+
return { ...slide, claimRefs: [...existing, ...additions] }
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const next: DecksState = {
|
|
47
|
+
...state,
|
|
48
|
+
narrative,
|
|
49
|
+
decks: {
|
|
50
|
+
...state.decks,
|
|
51
|
+
[deckKey]: {
|
|
52
|
+
...deck,
|
|
53
|
+
slides,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updatedRefs = getClaimSlideRefs(next, next.decks[deckKey])
|
|
59
|
+
const htmlTarget = ensureActiveHtmlDeckRenderTarget(next)
|
|
60
|
+
if (htmlTarget) {
|
|
61
|
+
htmlTarget.data = {
|
|
62
|
+
...(htmlTarget.data ?? {}),
|
|
63
|
+
narrativeId: narrative.id,
|
|
64
|
+
narrativeHash,
|
|
65
|
+
claimSlideRefs: updatedRefs.map((ref) => ({
|
|
66
|
+
claimId: ref.claimId,
|
|
67
|
+
claimText: ref.claimText,
|
|
68
|
+
slideIndex: ref.slideIndex,
|
|
69
|
+
slideTitle: ref.slideTitle,
|
|
70
|
+
match: ref.match,
|
|
71
|
+
role: ref.role,
|
|
72
|
+
location: ref.location,
|
|
73
|
+
})),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (addedCount > 0) {
|
|
78
|
+
recordWorkspaceAction(next, {
|
|
79
|
+
type: "artifact.coverage_backfilled",
|
|
80
|
+
actor: "revela-decks",
|
|
81
|
+
inputs: { activeDeck: deckKey, narrativeId: narrative.id },
|
|
82
|
+
outputs: { addedCount, slideCount: slides.length, narrativeHash },
|
|
83
|
+
status: "success",
|
|
84
|
+
summary: `Backfilled ${addedCount} slide claim reference${addedCount === 1 ? "" : "s"} from current artifact coverage.`,
|
|
85
|
+
nodeIds: [narrative.id, `artifact:${deck.outputPath ?? deckKey}`],
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { state: next, result: { updated: addedCount > 0, addedCount, slideCount: slides.length, narrativeHash, refs: updatedRefs } }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function backfilledRole(role: SlideClaimRefRole): SlideClaimRefRole {
|
|
93
|
+
return role
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function backfillNote(ref: ClaimSlideRef): string {
|
|
97
|
+
if (ref.match === "metadata") return `Backfilled from ${ref.location}.`
|
|
98
|
+
if (ref.match === "content") return `Backfilled from content match at ${ref.location}.`
|
|
99
|
+
return "Backfilled from slide evidence trace."
|
|
100
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { NarrativeMap, NarrativeMapClaimRelation } from "./map"
|
|
2
|
+
|
|
3
|
+
export type NarrativeViewLanguage = string
|
|
4
|
+
|
|
5
|
+
export interface NarrativeDisplayModel {
|
|
6
|
+
version: 1
|
|
7
|
+
language: NarrativeViewLanguage
|
|
8
|
+
pageTitle?: string
|
|
9
|
+
summaryLine?: string
|
|
10
|
+
labels?: Partial<NarrativeDisplayLabels>
|
|
11
|
+
claimCards?: NarrativeDisplayClaimCard[]
|
|
12
|
+
relations?: NarrativeDisplayRelation[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NarrativeDisplayLabels {
|
|
16
|
+
eyebrow: string
|
|
17
|
+
claimFlow: string
|
|
18
|
+
flowNote: string
|
|
19
|
+
selectedClaim: string
|
|
20
|
+
claim: string
|
|
21
|
+
claimId: string
|
|
22
|
+
status: string
|
|
23
|
+
supportedScope: string
|
|
24
|
+
unsupportedScope: string
|
|
25
|
+
incomingRelations: string
|
|
26
|
+
outgoingRelations: string
|
|
27
|
+
evidence: string
|
|
28
|
+
objections: string
|
|
29
|
+
risks: string
|
|
30
|
+
researchGaps: string
|
|
31
|
+
coveredSlides: string
|
|
32
|
+
noClaims: string
|
|
33
|
+
none: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NarrativeDisplayClaimCard {
|
|
37
|
+
claimId: string
|
|
38
|
+
displayTitle?: string
|
|
39
|
+
roleLabel?: string
|
|
40
|
+
narrativeJob?: string
|
|
41
|
+
evidenceSummary?: string
|
|
42
|
+
riskOrGapSummary?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NarrativeDisplayRelation {
|
|
46
|
+
fromClaimId: string
|
|
47
|
+
toClaimId: string
|
|
48
|
+
relation: NarrativeMapClaimRelation["relation"]
|
|
49
|
+
displayLabel?: string
|
|
50
|
+
displayRationale?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ValidatedNarrativeDisplayModel {
|
|
54
|
+
version: 1
|
|
55
|
+
language: NarrativeViewLanguage
|
|
56
|
+
pageTitle?: string
|
|
57
|
+
summaryLine?: string
|
|
58
|
+
labels: NarrativeDisplayLabels
|
|
59
|
+
claimCards: Map<string, NarrativeDisplayClaimCard>
|
|
60
|
+
relations: Map<string, NarrativeDisplayRelation>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage): NarrativeDisplayLabels {
|
|
64
|
+
if (isChineseLanguage(language)) {
|
|
65
|
+
return {
|
|
66
|
+
eyebrow: "只读主张流",
|
|
67
|
+
claimFlow: "主张推进",
|
|
68
|
+
flowNote: "点击主张查看证据、关系、风险、缺口和已覆盖页面。",
|
|
69
|
+
selectedClaim: "当前主张",
|
|
70
|
+
claim: "主张",
|
|
71
|
+
claimId: "主张 ID",
|
|
72
|
+
status: "状态",
|
|
73
|
+
supportedScope: "已支持范围",
|
|
74
|
+
unsupportedScope: "未支持范围",
|
|
75
|
+
incomingRelations: "前置关系",
|
|
76
|
+
outgoingRelations: "后续关系",
|
|
77
|
+
evidence: "证据",
|
|
78
|
+
objections: "反对意见",
|
|
79
|
+
risks: "风险",
|
|
80
|
+
researchGaps: "研究缺口",
|
|
81
|
+
coveredSlides: "已覆盖页面",
|
|
82
|
+
noClaims: "没有记录主张",
|
|
83
|
+
none: "无",
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (isJapaneseLanguage(language)) {
|
|
87
|
+
return {
|
|
88
|
+
eyebrow: "読み取り専用クレームフロー",
|
|
89
|
+
claimFlow: "クレームフロー",
|
|
90
|
+
flowNote: "クレームをクリックすると、根拠、関係、リスク、ギャップ、該当スライドを確認できます。",
|
|
91
|
+
selectedClaim: "選択中のクレーム",
|
|
92
|
+
claim: "クレーム",
|
|
93
|
+
claimId: "クレーム ID",
|
|
94
|
+
status: "ステータス",
|
|
95
|
+
supportedScope: "裏付けられた範囲",
|
|
96
|
+
unsupportedScope: "未裏付けの範囲",
|
|
97
|
+
incomingRelations: "入力関係",
|
|
98
|
+
outgoingRelations: "出力関係",
|
|
99
|
+
evidence: "根拠",
|
|
100
|
+
objections: "反論",
|
|
101
|
+
risks: "リスク",
|
|
102
|
+
researchGaps: "調査ギャップ",
|
|
103
|
+
coveredSlides: "対応スライド",
|
|
104
|
+
noClaims: "クレームは記録されていません",
|
|
105
|
+
none: "なし",
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
eyebrow: "Read-only claim flow board",
|
|
110
|
+
claimFlow: "Claim Flow",
|
|
111
|
+
flowNote: "Click a claim to inspect support, relation context, gaps, and covered slides.",
|
|
112
|
+
selectedClaim: "Selected claim",
|
|
113
|
+
claim: "Claim",
|
|
114
|
+
claimId: "Claim ID",
|
|
115
|
+
status: "Status",
|
|
116
|
+
supportedScope: "Supported scope",
|
|
117
|
+
unsupportedScope: "Unsupported scope",
|
|
118
|
+
incomingRelations: "Incoming relations",
|
|
119
|
+
outgoingRelations: "Outgoing relations",
|
|
120
|
+
evidence: "Evidence",
|
|
121
|
+
objections: "Objections",
|
|
122
|
+
risks: "Risks",
|
|
123
|
+
researchGaps: "Research gaps",
|
|
124
|
+
coveredSlides: "Covered slides",
|
|
125
|
+
noClaims: "No claims recorded",
|
|
126
|
+
none: "None",
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validateNarrativeDisplayModel(map: NarrativeMap, input: NarrativeDisplayModel | undefined, language: NarrativeViewLanguage): ValidatedNarrativeDisplayModel {
|
|
131
|
+
const defaults = defaultNarrativeDisplayLabels(language)
|
|
132
|
+
if (!input) return emptyDisplayModel(language, defaults)
|
|
133
|
+
if (input.version !== 1) throw new Error("Narrative display model version must be 1.")
|
|
134
|
+
if (input.language !== language) throw new Error(`Narrative display model language must be ${language}.`)
|
|
135
|
+
|
|
136
|
+
const claimIds = new Set(map.claimFlow.map((claim) => claim.id))
|
|
137
|
+
const relationByKey = new Map(map.claimRelations.map((relation) => [relationKey(relation), relation]))
|
|
138
|
+
const claimCards = new Map<string, NarrativeDisplayClaimCard>()
|
|
139
|
+
for (const card of input.claimCards ?? []) {
|
|
140
|
+
if (!claimIds.has(card.claimId)) throw new Error(`Unknown display claimId: ${card.claimId}`)
|
|
141
|
+
claimCards.set(card.claimId, cleanClaimCard(card))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const relations = new Map<string, NarrativeDisplayRelation>()
|
|
145
|
+
for (const relation of input.relations ?? []) {
|
|
146
|
+
const key = relationKey(relation)
|
|
147
|
+
const canonical = relationByKey.get(key)
|
|
148
|
+
if (!canonical) throw new Error(`Display relation is not present in the narrative map: ${key}`)
|
|
149
|
+
relations.set(key, cleanRelation(relation, canonical))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
version: 1,
|
|
154
|
+
language,
|
|
155
|
+
pageTitle: clean(input.pageTitle),
|
|
156
|
+
summaryLine: clean(input.summaryLine),
|
|
157
|
+
labels: mergeLabels(defaults, input.labels),
|
|
158
|
+
claimCards,
|
|
159
|
+
relations,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function emptyDisplayModel(language: NarrativeViewLanguage, labels = defaultNarrativeDisplayLabels(language)): ValidatedNarrativeDisplayModel {
|
|
164
|
+
return { version: 1, language, labels, claimCards: new Map(), relations: new Map() }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function relationKey(relation: Pick<NarrativeDisplayRelation, "fromClaimId" | "toClaimId" | "relation">): string {
|
|
168
|
+
return `${relation.fromClaimId}\u0000${relation.toClaimId}\u0000${relation.relation}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isChineseLanguage(language: string): boolean {
|
|
172
|
+
const normalized = language.trim().toLowerCase()
|
|
173
|
+
return normalized === "zh" || normalized === "zh-cn" || normalized === "cn" || normalized === "chinese" || normalized.includes("中文") || normalized.includes("简体")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function isJapaneseLanguage(language: string): boolean {
|
|
177
|
+
const normalized = language.trim().toLowerCase()
|
|
178
|
+
return normalized === "ja" || normalized === "ja-jp" || normalized === "jp" || normalized === "japanese" || normalized.includes("日本")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function mergeLabels(defaults: NarrativeDisplayLabels, overrides: Partial<NarrativeDisplayLabels> | undefined): NarrativeDisplayLabels {
|
|
182
|
+
const merged: NarrativeDisplayLabels = { ...defaults }
|
|
183
|
+
if (!overrides) return merged
|
|
184
|
+
for (const key of Object.keys(defaults) as Array<keyof NarrativeDisplayLabels>) {
|
|
185
|
+
merged[key] = clean(overrides[key]) ?? defaults[key]
|
|
186
|
+
}
|
|
187
|
+
return merged
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cleanClaimCard(card: NarrativeDisplayClaimCard): NarrativeDisplayClaimCard {
|
|
191
|
+
return {
|
|
192
|
+
claimId: card.claimId,
|
|
193
|
+
displayTitle: clean(card.displayTitle),
|
|
194
|
+
roleLabel: clean(card.roleLabel),
|
|
195
|
+
narrativeJob: clean(card.narrativeJob),
|
|
196
|
+
evidenceSummary: clean(card.evidenceSummary),
|
|
197
|
+
riskOrGapSummary: clean(card.riskOrGapSummary),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cleanRelation(relation: NarrativeDisplayRelation, canonical: NarrativeMapClaimRelation): NarrativeDisplayRelation {
|
|
202
|
+
const displayLabel = clean(relation.displayLabel)
|
|
203
|
+
const displayRationale = clean(relation.displayRationale)
|
|
204
|
+
if (displayLabel && canonical.inferred) throw new Error("Display label cannot replace inferred, non-canonical claim relation status.")
|
|
205
|
+
if (displayRationale && canonical.inferred) throw new Error("Display rationale cannot replace inferred, non-canonical claim relation rationale.")
|
|
206
|
+
if (displayRationale && !canonical.rationale?.trim()) throw new Error("Display rationale requires canonical claim relation rationale.")
|
|
207
|
+
return {
|
|
208
|
+
fromClaimId: relation.fromClaimId,
|
|
209
|
+
toClaimId: relation.toClaimId,
|
|
210
|
+
relation: relation.relation,
|
|
211
|
+
displayLabel,
|
|
212
|
+
displayRationale,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function clean(value: string | undefined): string | undefined {
|
|
217
|
+
const text = value?.trim()
|
|
218
|
+
return text || undefined
|
|
219
|
+
}
|