@cyber-dash-tech/revela 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -29
- package/README.zh-CN.md +35 -29
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +7 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/narrative.ts +160 -0
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +46 -3
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspection-context/compile.ts +159 -5
- package/lib/inspection-context/project.ts +20 -0
- package/lib/narrative-state/coverage.ts +100 -0
- package/lib/narrative-state/display.ts +219 -0
- package/lib/narrative-state/executive-brief.ts +246 -0
- package/lib/narrative-state/hash.ts +61 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +361 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/queries.ts +433 -0
- package/lib/narrative-state/readiness.ts +359 -0
- package/lib/narrative-state/render-plan.ts +250 -0
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +172 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +174 -2
- package/lib/workspace-state/types.ts +13 -1
- package/package.json +1 -1
- package/plugin.ts +58 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +265 -2
- package/tools/narrative-view.ts +84 -0
- package/tools/workspace-scan.ts +14 -1
|
@@ -0,0 +1,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
|
+
}
|