@cyber-dash-tech/revela 0.12.0 → 0.14.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 +16 -16
- package/README.zh-CN.md +16 -16
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/edit.ts +7 -5
- package/lib/commands/help.ts +5 -3
- package/lib/commands/inspect.ts +7 -5
- package/lib/commands/narrative.ts +160 -0
- package/lib/decks-state.ts +33 -0
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspect/prompt.ts +15 -2
- package/lib/inspect/requests.ts +21 -2
- package/lib/inspection-context/compile.ts +230 -10
- package/lib/inspection-context/match.ts +71 -1
- package/lib/inspection-context/project.ts +131 -8
- package/lib/inspection-context/result.ts +183 -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 +9 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +54 -0
- package/lib/narrative-state/queries.ts +434 -0
- package/lib/narrative-state/readiness.ts +71 -1
- package/lib/narrative-state/render-plan.ts +44 -1
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +33 -0
- package/lib/refine/server.ts +91 -13
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +56 -2
- package/lib/workspace-state/types.ts +10 -1
- package/package.json +1 -1
- package/plugin.ts +33 -2
- package/tools/decks.ts +86 -1
- package/tools/edit.ts +10 -8
- package/tools/inspection-result.ts +37 -0
- package/tools/narrative-view.ts +84 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { NarrativeBrief, NarrativeRole } from "../decks-state"
|
|
2
|
-
import type { InspectionContext, InspectionEvidenceTrace, InspectionGap } from "./compile"
|
|
2
|
+
import type { InspectionClaimCandidate, InspectionContext, InspectionEvidenceTrace, InspectionGap } from "./compile"
|
|
3
3
|
import type { InspectionElementMatch, InspectionElementSnapshot, InspectionMatchConfidence } from "./match"
|
|
4
4
|
|
|
5
5
|
export interface InspectionPromptProjection {
|
|
@@ -13,6 +13,7 @@ export interface InspectionPromptProjection {
|
|
|
13
13
|
caveats: InspectionCaveatsProjection
|
|
14
14
|
objective: InspectionObjectiveProjection
|
|
15
15
|
appendix: InspectionAppendixProjection
|
|
16
|
+
artifacts: InspectionArtifactCoverageProjection
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -28,11 +29,15 @@ export interface InspectionProjectionElement {
|
|
|
28
29
|
scope?: "element" | "selection" | "slide"
|
|
29
30
|
slideIndex?: number
|
|
30
31
|
text?: string
|
|
32
|
+
nearbyText?: string
|
|
33
|
+
outerHTMLExcerpt?: string
|
|
31
34
|
elements?: Array<{
|
|
32
35
|
text?: string
|
|
33
36
|
tagName?: string
|
|
34
37
|
classList: string[]
|
|
35
38
|
role?: string
|
|
39
|
+
nearbyText?: string
|
|
40
|
+
outerHTMLExcerpt?: string
|
|
36
41
|
}>
|
|
37
42
|
tagName?: string
|
|
38
43
|
classList: string[]
|
|
@@ -50,11 +55,30 @@ export interface InspectionProjectionMatch {
|
|
|
50
55
|
}
|
|
51
56
|
claim?: {
|
|
52
57
|
id: string
|
|
58
|
+
canonicalClaimId?: string
|
|
53
59
|
origin: string
|
|
54
60
|
text: string
|
|
55
61
|
evidenceSensitive: boolean
|
|
56
62
|
evidenceSupport: string
|
|
63
|
+
evidenceBindingIds: string[]
|
|
64
|
+
supportedScope?: string
|
|
65
|
+
unsupportedScope?: string
|
|
66
|
+
caveats: string[]
|
|
57
67
|
}
|
|
68
|
+
candidateClaims?: InspectionProjectionClaimCandidate[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface InspectionProjectionClaimCandidate {
|
|
72
|
+
id: string
|
|
73
|
+
canonicalClaimId?: string
|
|
74
|
+
origin: string
|
|
75
|
+
text: string
|
|
76
|
+
evidenceSensitive: boolean
|
|
77
|
+
evidenceSupport: string
|
|
78
|
+
evidenceBindingIds: string[]
|
|
79
|
+
supportedScope?: string
|
|
80
|
+
unsupportedScope?: string
|
|
81
|
+
caveats: string[]
|
|
58
82
|
}
|
|
59
83
|
|
|
60
84
|
export interface InspectionSourceProjection {
|
|
@@ -95,8 +119,36 @@ export interface InspectionAppendixProjection {
|
|
|
95
119
|
relatedObjections: string[]
|
|
96
120
|
}
|
|
97
121
|
|
|
122
|
+
export interface InspectionArtifactCoverageProjection {
|
|
123
|
+
selectedClaimId?: string
|
|
124
|
+
artifacts: InspectionArtifactCoverageProjectionItem[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface InspectionArtifactCoverageProjectionItem {
|
|
128
|
+
artifactId: string
|
|
129
|
+
type: string
|
|
130
|
+
outputPath?: string
|
|
131
|
+
coverageStatus: "current" | "stale" | "partial" | "missing"
|
|
132
|
+
containsClaim: boolean
|
|
133
|
+
stale: boolean
|
|
134
|
+
staleReason?: string
|
|
135
|
+
staleReasons: string[]
|
|
136
|
+
affectedClaimIds: string[]
|
|
137
|
+
missingClaimIds: string[]
|
|
138
|
+
note?: string
|
|
139
|
+
locations: Array<{
|
|
140
|
+
slideIndex: number
|
|
141
|
+
slideTitle: string
|
|
142
|
+
role: string
|
|
143
|
+
match: string
|
|
144
|
+
location: string
|
|
145
|
+
}>
|
|
146
|
+
}
|
|
147
|
+
|
|
98
148
|
export interface InspectionEvidenceProjectionTrace {
|
|
99
149
|
source: string
|
|
150
|
+
evidenceBindingId?: string
|
|
151
|
+
claimId?: string
|
|
100
152
|
sourcePath?: string
|
|
101
153
|
findingsFile?: string
|
|
102
154
|
location?: string
|
|
@@ -104,6 +156,9 @@ export interface InspectionEvidenceProjectionTrace {
|
|
|
104
156
|
url?: string
|
|
105
157
|
quote?: string
|
|
106
158
|
caveat?: string
|
|
159
|
+
supportScope?: string
|
|
160
|
+
unsupportedScope?: string
|
|
161
|
+
strength?: string
|
|
107
162
|
extractedTextPath?: string
|
|
108
163
|
extractedManifestPath?: string
|
|
109
164
|
hasDetail: boolean
|
|
@@ -122,6 +177,7 @@ export function projectInspectionMatch(
|
|
|
122
177
|
): InspectionPromptProjection {
|
|
123
178
|
const slide = match.slide
|
|
124
179
|
const claim = match.claim
|
|
180
|
+
const candidateClaims = dedupeClaims(match.candidateClaims ?? [])
|
|
125
181
|
const traces = match.evidence.map(projectEvidenceTrace)
|
|
126
182
|
const gaps = match.gaps.map(projectGap)
|
|
127
183
|
const narrativeBrief = context.narrativeBrief
|
|
@@ -139,11 +195,15 @@ export function projectInspectionMatch(
|
|
|
139
195
|
slideIndex: snapshot.slideIndex,
|
|
140
196
|
scope: snapshot.scope,
|
|
141
197
|
text: truncateOptional(snapshot.selectedText || snapshot.text, 700),
|
|
198
|
+
nearbyText: truncateOptional(snapshot.nearbyText, 900),
|
|
199
|
+
outerHTMLExcerpt: truncateOptional(snapshot.outerHTMLExcerpt, 900),
|
|
142
200
|
elements: snapshot.elements?.slice(0, 12).map((item) => ({
|
|
143
201
|
text: truncateOptional(item.text, 320),
|
|
144
202
|
tagName: truncateOptional(item.tagName, 40),
|
|
145
203
|
classList: (item.classList ?? []).slice(0, 8).map((className) => truncate(className, 80)),
|
|
146
204
|
role: truncateOptional(item.role, 80),
|
|
205
|
+
nearbyText: truncateOptional(item.nearbyText, 420),
|
|
206
|
+
outerHTMLExcerpt: truncateOptional(item.outerHTMLExcerpt, 420),
|
|
147
207
|
})),
|
|
148
208
|
tagName: truncateOptional(snapshot.tagName, 40),
|
|
149
209
|
classList: (snapshot.classList ?? []).slice(0, 12).map((item) => truncate(item, 80)),
|
|
@@ -161,14 +221,9 @@ export function projectInspectionMatch(
|
|
|
161
221
|
}
|
|
162
222
|
: undefined,
|
|
163
223
|
claim: claim
|
|
164
|
-
?
|
|
165
|
-
id: claim.id,
|
|
166
|
-
origin: claim.origin,
|
|
167
|
-
text: truncate(claim.text, 500),
|
|
168
|
-
evidenceSensitive: claim.evidenceSensitive,
|
|
169
|
-
evidenceSupport: claim.evidenceSupport,
|
|
170
|
-
}
|
|
224
|
+
? projectClaimCandidate(claim)
|
|
171
225
|
: undefined,
|
|
226
|
+
candidateClaims: candidateClaims.map(projectClaimCandidate).slice(0, 8),
|
|
172
227
|
},
|
|
173
228
|
cards: {
|
|
174
229
|
source: {
|
|
@@ -204,13 +259,78 @@ export function projectInspectionMatch(
|
|
|
204
259
|
relatedRisks: relatedNarrativeText(context.riskContext, slide?.index),
|
|
205
260
|
relatedObjections: relatedNarrativeText(context.objectionContext, slide?.index),
|
|
206
261
|
},
|
|
262
|
+
artifacts: projectArtifactCoverage(context, claim?.canonicalClaimId ?? claim?.id),
|
|
207
263
|
},
|
|
208
264
|
}
|
|
209
265
|
}
|
|
210
266
|
|
|
267
|
+
function projectClaimCandidate(claim: InspectionClaimCandidate): InspectionProjectionClaimCandidate {
|
|
268
|
+
return {
|
|
269
|
+
id: claim.id,
|
|
270
|
+
canonicalClaimId: claim.canonicalClaimId,
|
|
271
|
+
origin: claim.origin,
|
|
272
|
+
text: truncate(claim.text, 500),
|
|
273
|
+
evidenceSensitive: claim.evidenceSensitive,
|
|
274
|
+
evidenceSupport: claim.evidenceSupport,
|
|
275
|
+
evidenceBindingIds: claim.evidenceBindingIds,
|
|
276
|
+
supportedScope: truncateOptional(claim.supportedScope, 280),
|
|
277
|
+
unsupportedScope: truncateOptional(claim.unsupportedScope, 280),
|
|
278
|
+
caveats: claim.caveats.map((item) => truncate(item, 280)).slice(0, 8),
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function dedupeClaims(claims: InspectionClaimCandidate[]): InspectionClaimCandidate[] {
|
|
283
|
+
const seen = new Set<string>()
|
|
284
|
+
const result: InspectionClaimCandidate[] = []
|
|
285
|
+
for (const claim of claims) {
|
|
286
|
+
const key = claim.canonicalClaimId || claim.id
|
|
287
|
+
if (seen.has(key)) continue
|
|
288
|
+
seen.add(key)
|
|
289
|
+
result.push(claim)
|
|
290
|
+
}
|
|
291
|
+
return result
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function projectArtifactCoverage(context: InspectionContext, selectedClaimId: string | undefined): InspectionArtifactCoverageProjection {
|
|
295
|
+
return {
|
|
296
|
+
selectedClaimId,
|
|
297
|
+
artifacts: context.artifactCoverage.map((artifact) => {
|
|
298
|
+
const locations = selectedClaimId
|
|
299
|
+
? artifact.slideRefs
|
|
300
|
+
.filter((ref) => ref.claimId === selectedClaimId)
|
|
301
|
+
.slice(0, 8)
|
|
302
|
+
.map((ref) => ({
|
|
303
|
+
slideIndex: ref.slideIndex,
|
|
304
|
+
slideTitle: truncate(ref.slideTitle, 180),
|
|
305
|
+
role: ref.role,
|
|
306
|
+
match: ref.match,
|
|
307
|
+
location: truncate(ref.location, 120),
|
|
308
|
+
}))
|
|
309
|
+
: []
|
|
310
|
+
const containsClaim = Boolean(selectedClaimId && (artifact.claimIds.includes(selectedClaimId) || locations.length > 0))
|
|
311
|
+
return {
|
|
312
|
+
artifactId: truncate(artifact.artifactId, 180),
|
|
313
|
+
type: artifact.type,
|
|
314
|
+
outputPath: truncateOptional(artifact.outputPath, 220),
|
|
315
|
+
coverageStatus: artifact.coverageStatus,
|
|
316
|
+
containsClaim,
|
|
317
|
+
stale: artifact.stale,
|
|
318
|
+
staleReason: truncateOptional(artifact.staleReason, 240),
|
|
319
|
+
staleReasons: artifact.staleReasons.map((item) => truncate(item, 240)).slice(0, 5),
|
|
320
|
+
affectedClaimIds: artifact.affectedClaimIds.map((item) => truncate(item, 160)).slice(0, 8),
|
|
321
|
+
missingClaimIds: artifact.missingClaimIds.map((item) => truncate(item, 160)).slice(0, 8),
|
|
322
|
+
note: truncateOptional(artifact.note, 240),
|
|
323
|
+
locations,
|
|
324
|
+
}
|
|
325
|
+
}).slice(0, 8),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
211
329
|
function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenceProjectionTrace {
|
|
212
330
|
return {
|
|
213
331
|
source: truncate(trace.source, 180),
|
|
332
|
+
evidenceBindingId: truncateOptional(trace.evidenceBindingId, 160),
|
|
333
|
+
claimId: truncateOptional(trace.claimId, 160),
|
|
214
334
|
sourcePath: truncateOptional(trace.sourcePath, 220),
|
|
215
335
|
findingsFile: truncateOptional(trace.findingsFile, 220),
|
|
216
336
|
location: truncateOptional(trace.location, 120),
|
|
@@ -218,6 +338,9 @@ function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenc
|
|
|
218
338
|
url: truncateOptional(trace.url, 240),
|
|
219
339
|
quote: truncateOptional(trace.quote, 500),
|
|
220
340
|
caveat: truncateOptional(trace.caveat, 280),
|
|
341
|
+
supportScope: truncateOptional(trace.supportScope, 280),
|
|
342
|
+
unsupportedScope: truncateOptional(trace.unsupportedScope, 280),
|
|
343
|
+
strength: trace.strength,
|
|
221
344
|
extractedTextPath: truncateOptional(trace.extractedTextPath, 220),
|
|
222
345
|
extractedManifestPath: truncateOptional(trace.extractedManifestPath, 220),
|
|
223
346
|
hasDetail: trace.hasDetail,
|
|
@@ -4,6 +4,8 @@ import type { InspectionPromptProjection } from "./project"
|
|
|
4
4
|
export type InspectionResultStatus = "success" | "no_match"
|
|
5
5
|
export type InspectionPurposeStatus = "clear" | "weak" | "misplaced" | "unknown"
|
|
6
6
|
export type InspectionSourceStatus = "supported" | "weak" | "unsupported" | "not_needed" | "unknown"
|
|
7
|
+
export type NarrativeReadingStatus = "matched" | "no_match"
|
|
8
|
+
export type ExploratoryReadingStatus = "available" | "limited" | "unavailable"
|
|
7
9
|
|
|
8
10
|
export interface InspectionResult {
|
|
9
11
|
version: 1
|
|
@@ -16,6 +18,8 @@ export interface InspectionResult {
|
|
|
16
18
|
}
|
|
17
19
|
matchConfidence: InspectionMatchConfidence
|
|
18
20
|
cards: {
|
|
21
|
+
reading?: NarrativeReadingCard
|
|
22
|
+
exploratory?: ExploratoryReadingCard
|
|
19
23
|
purpose: PurposeCard
|
|
20
24
|
source: SourceCard
|
|
21
25
|
}
|
|
@@ -53,6 +57,47 @@ export interface SourceCard {
|
|
|
53
57
|
rationale: string
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
export interface NarrativeReadingCard {
|
|
61
|
+
status: NarrativeReadingStatus
|
|
62
|
+
claimId?: string
|
|
63
|
+
canonicalClaimId?: string
|
|
64
|
+
claimText?: string
|
|
65
|
+
evidenceStatus?: string
|
|
66
|
+
evidenceBindingIds: string[]
|
|
67
|
+
supportedScope?: string
|
|
68
|
+
unsupportedScope?: string
|
|
69
|
+
caveats: string[]
|
|
70
|
+
relatedObjections: string[]
|
|
71
|
+
relatedRisks: string[]
|
|
72
|
+
artifactCoverage: NarrativeReadingArtifactCoverage[]
|
|
73
|
+
rationale: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface NarrativeReadingArtifactCoverage {
|
|
77
|
+
artifactId: string
|
|
78
|
+
type: string
|
|
79
|
+
outputPath?: string
|
|
80
|
+
coverageStatus: "current" | "stale" | "partial" | "missing"
|
|
81
|
+
containsClaim: boolean
|
|
82
|
+
stale: boolean
|
|
83
|
+
staleReason?: string
|
|
84
|
+
locations: string[]
|
|
85
|
+
note?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ExploratoryReadingCard {
|
|
89
|
+
status: ExploratoryReadingStatus
|
|
90
|
+
official: false
|
|
91
|
+
audience?: string
|
|
92
|
+
claimFocus?: string
|
|
93
|
+
objectionPrompts: string[]
|
|
94
|
+
audienceReframe: string
|
|
95
|
+
appendixLeads: string[]
|
|
96
|
+
meetingPrep: string[]
|
|
97
|
+
boundaries: string[]
|
|
98
|
+
rationale: string
|
|
99
|
+
}
|
|
100
|
+
|
|
56
101
|
export function buildDeterministicInspectionResult(
|
|
57
102
|
projection: InspectionPromptProjection,
|
|
58
103
|
options: { requestId?: string; staleReason?: string } = {},
|
|
@@ -72,6 +117,8 @@ export function buildDeterministicInspectionResult(
|
|
|
72
117
|
slide: slide ? { index: slide.index, title: slide.title } : undefined,
|
|
73
118
|
matchConfidence: projection.match.confidence,
|
|
74
119
|
cards: {
|
|
120
|
+
reading: narrativeReadingCard(projection, noMatch),
|
|
121
|
+
exploratory: exploratoryReadingCard(projection, noMatch),
|
|
75
122
|
purpose: {
|
|
76
123
|
status: purposeStatus(projection, noMatch),
|
|
77
124
|
role: projection.cards.objective.narrativeRole,
|
|
@@ -101,6 +148,126 @@ export function buildDeterministicInspectionResult(
|
|
|
101
148
|
}
|
|
102
149
|
}
|
|
103
150
|
|
|
151
|
+
function exploratoryReadingCard(projection: InspectionPromptProjection, noMatch: boolean): ExploratoryReadingCard {
|
|
152
|
+
const claimText = projection.match.claim?.text ?? projection.cards.evidence.matchedClaim
|
|
153
|
+
const caveats = projection.cards.caveats.caveats
|
|
154
|
+
const unsupportedScope = projection.match.claim?.unsupportedScope ?? firstPresent(projection.cards.evidence.traces.map((item) => item.unsupportedScope))
|
|
155
|
+
const objectionPrompts = exploratoryObjections(projection, unsupportedScope)
|
|
156
|
+
const appendixLeads = exploratoryAppendixLeads(projection)
|
|
157
|
+
const meetingPrep = exploratoryMeetingPrep(projection, unsupportedScope)
|
|
158
|
+
return {
|
|
159
|
+
status: noMatch ? "unavailable" : claimText ? "available" : "limited",
|
|
160
|
+
official: false,
|
|
161
|
+
audience: projection.cards.objective.audience,
|
|
162
|
+
claimFocus: claimText,
|
|
163
|
+
objectionPrompts,
|
|
164
|
+
audienceReframe: exploratoryAudienceFrame(projection, claimText),
|
|
165
|
+
appendixLeads,
|
|
166
|
+
meetingPrep,
|
|
167
|
+
boundaries: exploratoryBoundaries(projection, noMatch),
|
|
168
|
+
rationale: exploratoryRationale(projection, noMatch, objectionPrompts.length + appendixLeads.length + meetingPrep.length),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function exploratoryObjections(projection: InspectionPromptProjection, unsupportedScope: string | undefined): string[] {
|
|
173
|
+
const items = projection.cards.appendix.relatedObjections.map((item) => `Prepare for this objection using recorded support only: ${item}`)
|
|
174
|
+
if (unsupportedScope) items.push(`Expect questions about unsupported scope: ${unsupportedScope}`)
|
|
175
|
+
for (const gap of projection.cards.evidence.gaps) items.push(`Treat this as an evidence gap, not as support: ${gap.message}`)
|
|
176
|
+
return dedupe(items).slice(0, 4)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function exploratoryAudienceFrame(projection: InspectionPromptProjection, claimText: string | undefined): string {
|
|
180
|
+
const audience = projection.cards.objective.audience
|
|
181
|
+
const beliefAfter = projection.cards.objective.audienceBeliefAfter
|
|
182
|
+
const decision = projection.cards.objective.decisionOrAction
|
|
183
|
+
if (!claimText) return "No specific claim is matched, so audience reframing is limited to the selected slide context."
|
|
184
|
+
if (audience && beliefAfter) return `For ${audience}, frame this claim as support for the desired belief: ${beliefAfter}`
|
|
185
|
+
if (audience && decision) return `For ${audience}, connect this claim to the recorded decision or action: ${decision}`
|
|
186
|
+
if (audience) return `For ${audience}, keep the wording inside the recorded claim boundary: ${claimText}`
|
|
187
|
+
return "Audience-specific reframing is limited because no audience is recorded in the projection."
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function exploratoryAppendixLeads(projection: InspectionPromptProjection): string[] {
|
|
191
|
+
const sourceLeads = projection.cards.evidence.traces.map((trace) => {
|
|
192
|
+
const where = trace.location || trace.page || trace.url || trace.sourcePath || trace.findingsFile
|
|
193
|
+
return where ? `${trace.source}: ${where}` : trace.source
|
|
194
|
+
})
|
|
195
|
+
const appendixCandidates = projection.cards.appendix.candidates.map((candidate) => `Slide ${candidate.slideIndex}: ${candidate.slideTitle} (${candidate.reason})`)
|
|
196
|
+
return dedupe([...sourceLeads, ...appendixCandidates]).slice(0, 5)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function exploratoryMeetingPrep(projection: InspectionPromptProjection, unsupportedScope: string | undefined): string[] {
|
|
200
|
+
const items: string[] = []
|
|
201
|
+
for (const risk of projection.cards.appendix.relatedRisks) items.push(`Risk to be ready for: ${risk}`)
|
|
202
|
+
for (const caveat of projection.cards.caveats.caveats) items.push(`Caveat to say plainly: ${caveat}`)
|
|
203
|
+
if (unsupportedScope) items.push(`Do not overstate beyond: ${unsupportedScope}`)
|
|
204
|
+
for (const warning of projection.cards.source.weakSourceGaps) items.push(`Source trace is weak: ${warning.message}`)
|
|
205
|
+
for (const gap of projection.cards.source.missingSourceGaps) items.push(`Missing support: ${gap.message}`)
|
|
206
|
+
return dedupe(items).slice(0, 5)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function exploratoryBoundaries(projection: InspectionPromptProjection, noMatch: boolean): string[] {
|
|
210
|
+
const boundaries = [
|
|
211
|
+
"Exploratory reading is non-official and does not change canonical narrative state or artifact content.",
|
|
212
|
+
"Use only recorded claims, evidence, caveats, risks, objections, and artifact coverage from this inspection projection.",
|
|
213
|
+
]
|
|
214
|
+
if (noMatch) boundaries.push("No matched slide or claim is available, so exploratory reading cannot infer support.")
|
|
215
|
+
if (projection.cards.evidence.gaps.length > 0) boundaries.push("Evidence gaps must remain visible and cannot be filled by generated reading text.")
|
|
216
|
+
return boundaries
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function exploratoryRationale(projection: InspectionPromptProjection, noMatch: boolean, signalCount: number): string {
|
|
220
|
+
if (noMatch) return "Exploratory reading is unavailable because the selection did not match a slide or claim."
|
|
221
|
+
if (signalCount > 0) return "This bounded reading layer derives objection, audience, appendix, and meeting-prep cues from recorded inspection context only."
|
|
222
|
+
if (projection.match.claim) return "A matched claim exists, but little supporting exploratory context is recorded beyond the claim itself."
|
|
223
|
+
return "Exploratory reading is limited because the selection maps to slide context but not to a specific claim."
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function narrativeReadingCard(projection: InspectionPromptProjection, noMatch: boolean): NarrativeReadingCard {
|
|
227
|
+
const claim = projection.match.claim
|
|
228
|
+
const evidenceBindingIds = dedupe([
|
|
229
|
+
...(claim?.evidenceBindingIds ?? []),
|
|
230
|
+
...projection.cards.evidence.traces.map((item) => item.evidenceBindingId).filter((item): item is string => Boolean(item)),
|
|
231
|
+
])
|
|
232
|
+
return {
|
|
233
|
+
status: noMatch ? "no_match" : "matched",
|
|
234
|
+
claimId: claim?.id,
|
|
235
|
+
canonicalClaimId: claim?.canonicalClaimId,
|
|
236
|
+
claimText: claim?.text ?? projection.cards.evidence.matchedClaim,
|
|
237
|
+
evidenceStatus: claim?.evidenceSupport ?? projection.cards.evidence.evidenceSupport,
|
|
238
|
+
evidenceBindingIds,
|
|
239
|
+
supportedScope: claim?.supportedScope ?? firstPresent(projection.cards.evidence.traces.map((item) => item.supportScope)),
|
|
240
|
+
unsupportedScope: claim?.unsupportedScope ?? firstPresent(projection.cards.evidence.traces.map((item) => item.unsupportedScope)),
|
|
241
|
+
caveats: projection.cards.caveats.caveats,
|
|
242
|
+
relatedObjections: projection.cards.appendix.relatedObjections,
|
|
243
|
+
relatedRisks: projection.cards.appendix.relatedRisks,
|
|
244
|
+
artifactCoverage: artifactCoverage(projection),
|
|
245
|
+
rationale: narrativeReadingRationale(projection, noMatch),
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function artifactCoverage(projection: InspectionPromptProjection): NarrativeReadingArtifactCoverage[] {
|
|
250
|
+
return projection.cards.artifacts.artifacts.map((artifact) => ({
|
|
251
|
+
artifactId: artifact.artifactId,
|
|
252
|
+
type: artifact.type,
|
|
253
|
+
outputPath: artifact.outputPath,
|
|
254
|
+
coverageStatus: artifact.coverageStatus,
|
|
255
|
+
containsClaim: artifact.containsClaim,
|
|
256
|
+
stale: artifact.stale,
|
|
257
|
+
staleReason: artifact.staleReason,
|
|
258
|
+
locations: artifact.locations.map((location) => `Slide ${location.slideIndex}: ${location.slideTitle} (${location.role}, ${location.match}:${location.location})`),
|
|
259
|
+
note: artifact.note ?? artifact.staleReasons[0],
|
|
260
|
+
}))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function narrativeReadingRationale(projection: InspectionPromptProjection, noMatch: boolean): string {
|
|
264
|
+
if (noMatch) return "No matched slide or claim is available for narrative reading."
|
|
265
|
+
const claim = projection.match.claim
|
|
266
|
+
if (claim?.canonicalClaimId) return "The selection resolves to a canonical narrative claim, so Refine can show claim, evidence-boundary, caveat, objection, and risk context before any generated judgment."
|
|
267
|
+
if (claim) return "The selection resolves to a slide claim candidate. Canonical narrative linkage is not recorded for this element, so reading context falls back to slide claim and source trace."
|
|
268
|
+
return "The selection resolves to a slide but not a specific claim; narrative reading is limited to slide purpose, role, and surrounding evidence context."
|
|
269
|
+
}
|
|
270
|
+
|
|
104
271
|
function purposeStatus(projection: InspectionPromptProjection, noMatch: boolean): InspectionPurposeStatus {
|
|
105
272
|
if (noMatch) return "unknown"
|
|
106
273
|
if (projection.cards.objective.narrativeRole || projection.cards.objective.slidePurpose) return "clear"
|
|
@@ -158,3 +325,19 @@ function sourceCaveats(projection: InspectionPromptProjection): string[] {
|
|
|
158
325
|
...projection.cards.appendix.relatedObjections,
|
|
159
326
|
].slice(0, 10)
|
|
160
327
|
}
|
|
328
|
+
|
|
329
|
+
function firstPresent(values: Array<string | undefined>): string | undefined {
|
|
330
|
+
return values.find((value) => Boolean(value?.trim()))
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function dedupe(values: string[]): string[] {
|
|
334
|
+
const seen = new Set<string>()
|
|
335
|
+
const result: string[] = []
|
|
336
|
+
for (const value of values) {
|
|
337
|
+
const key = value.trim()
|
|
338
|
+
if (!key || seen.has(key)) continue
|
|
339
|
+
seen.add(key)
|
|
340
|
+
result.push(key)
|
|
341
|
+
}
|
|
342
|
+
return result
|
|
343
|
+
}
|
|
@@ -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
|
+
}
|