@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
package/lib/inspect/prompt.ts
CHANGED
|
@@ -4,23 +4,31 @@ export function buildInspectionPrompt(input: {
|
|
|
4
4
|
requestId: string
|
|
5
5
|
file: string
|
|
6
6
|
projection: InspectionPromptProjection
|
|
7
|
+
language?: string
|
|
7
8
|
}): string {
|
|
9
|
+
const language = normalizeInspectLanguage(input.language)
|
|
8
10
|
return `A user selected slide content in Revela Evidence Inspector. The selection may contain one referenced element, a whole slide, or multiple referenced elements selected with Cmd/Ctrl-click.
|
|
9
11
|
|
|
10
12
|
Target file: ${input.file}
|
|
11
13
|
Inspection request id: ${input.requestId}
|
|
14
|
+
Display language: ${language}
|
|
12
15
|
|
|
13
|
-
Use the structured projection below to produce the final inspector cards. This is LLM judgment with grounded boundaries:
|
|
16
|
+
Use the structured projection below to produce the final inspector cards. This is LLM judgment with grounded boundaries: explain the selected object's narrative reading context, bounded exploratory reading context, purpose, and source credibility only. Do not edit files. Do not mutate DECKS.json. Do not invent claim ids, evidence binding ids, sources, quotes, URLs, page references, caveats, objections, risks, artifact coverage, or evidence not present in the projection.
|
|
17
|
+
|
|
18
|
+
Language boundary: the selected display language affects only human-readable card copy. Preserve all claim ids, canonical claim ids, evidence binding ids, source paths, findings files, URLs, numbers, quoted/source facts, caveats, artifact ids, and coverage statuses exactly as grounded in the projection. If the display language is Auto, use projection.deck.language when available; otherwise follow the user's/browser context or default to English.
|
|
14
19
|
|
|
15
20
|
Return the result only by calling the \`revela-inspection-result\` tool with this request id. Do not answer in chat.
|
|
16
21
|
|
|
17
22
|
Required card model:
|
|
23
|
+
- Narrative Reading: when the projection includes a matched claim, preserve its claim id, canonical claim id, evidence binding ids, supported scope, unsupported scope, caveats, related objections, related risks, and artifact coverage. Artifact coverage must come only from projection.cards.artifacts; do not invent where a claim appears or whether an artifact is stale/current/partial/missing. If canonical narrative linkage is missing, say so and fall back to the matched slide claim; do not invent canonical ids.
|
|
24
|
+
- Candidate boundary: when projection.match.claim is absent but projection.match.candidateClaims is present, explain the selected child element only within those candidate claim boundaries. You may describe that the child element functions as a detail, prerequisite, source note, risk cue, or evidence cue inside the slide, but you must not select one candidate claim id by semantic guess. If projection.match.confidence is none or candidateClaims is empty, explain the mapping gap instead of inventing a plausible claim.
|
|
25
|
+
- Exploratory Reading: provide bounded, non-official reading cues for objection prep, audience reframing, appendix leads, and meeting prep only from the projection. Mark official as false. Keep missing evidence, caveats, unsupported scope, and stale artifacts visible. Do not make exploratory text sound like approved artifact content, and do not turn this into chat or a fix plan.
|
|
18
26
|
- Purpose: explain why this selected content appears here, what job it serves in the slide purpose, narrative role, deck goal, audience, or narrative brief, and why it matters.
|
|
19
27
|
- Source: if the selection contains a factual claim, number, comparison, conclusion, or recommendation, judge source credibility. Use not_needed for structural, transitional, or purely explanatory content that does not need evidence. Include source trace, warnings, gaps, and caveats here.
|
|
20
28
|
|
|
21
29
|
Boundaries:
|
|
22
30
|
- Do not hunt for problems. If it works, say it works.
|
|
23
|
-
- Do not recommend edits or fixes; this inspector view only explains purpose and source credibility.
|
|
31
|
+
- Do not recommend edits or fixes; this inspector view only explains narrative context, bounded exploratory reading context, purpose, and source credibility.
|
|
24
32
|
- Do not turn every caveat into a problem.
|
|
25
33
|
- If confidence is low, use unclear or unknown instead of pretending certainty.
|
|
26
34
|
|
|
@@ -30,3 +38,8 @@ Projection JSON:
|
|
|
30
38
|
${JSON.stringify(input.projection, null, 2)}
|
|
31
39
|
\`\`\``
|
|
32
40
|
}
|
|
41
|
+
|
|
42
|
+
function normalizeInspectLanguage(language: string | undefined): string {
|
|
43
|
+
const value = typeof language === "string" ? language.trim() : ""
|
|
44
|
+
return value || "Auto"
|
|
45
|
+
}
|
package/lib/inspect/requests.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InspectionPromptProjection } from "../inspection-context/project"
|
|
2
|
-
import type
|
|
2
|
+
import { buildDeterministicInspectionResult, type InspectionResult } from "../inspection-context/result"
|
|
3
3
|
|
|
4
4
|
export type InspectRequestStatus = "pending" | "completed" | "failed" | "expired"
|
|
5
5
|
|
|
@@ -53,11 +53,30 @@ export function completeInspectRequest(requestId: string, result: InspectionResu
|
|
|
53
53
|
if (!request) throw new Error(`Unknown inspection request: ${requestId}`)
|
|
54
54
|
if (request.status !== "pending") throw new Error(`Inspection request is not pending: ${request.status}`)
|
|
55
55
|
request.status = "completed"
|
|
56
|
-
request.result =
|
|
56
|
+
request.result = normalizeInspectionResult(request.projection, result, requestId)
|
|
57
57
|
request.updatedAt = Date.now()
|
|
58
58
|
return request
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function normalizeInspectionResult(
|
|
62
|
+
projection: InspectionPromptProjection,
|
|
63
|
+
result: InspectionResult,
|
|
64
|
+
requestId: string,
|
|
65
|
+
): InspectionResult {
|
|
66
|
+
const deterministic = buildDeterministicInspectionResult(projection, { requestId })
|
|
67
|
+
return {
|
|
68
|
+
...result,
|
|
69
|
+
requestId,
|
|
70
|
+
cards: {
|
|
71
|
+
reading: result.cards.reading ?? deterministic.cards.reading,
|
|
72
|
+
exploratory: result.cards.exploratory ?? deterministic.cards.exploratory,
|
|
73
|
+
purpose: result.cards.purpose,
|
|
74
|
+
source: result.cards.source,
|
|
75
|
+
},
|
|
76
|
+
stale: result.stale ?? deterministic.stale,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
export function failInspectRequest(requestId: string, error: string): PendingInspectRequest | undefined {
|
|
62
81
|
const request = getInspectRequest(requestId)
|
|
63
82
|
if (!request || request.status !== "pending") return request
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, NarrativeRole, SlideSpec, SourceMaterial } from "../decks-state"
|
|
2
|
+
import { getArtifactClaimRefs, type ArtifactClaimRef, type ClaimSlideRef } from "../narrative-state/queries"
|
|
3
|
+
import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
|
|
2
4
|
|
|
3
|
-
export type InspectionClaimOrigin = "title" | "headline" | "body" | "bullet" | "purpose"
|
|
5
|
+
export type InspectionClaimOrigin = "narrative" | "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
6
|
export type InspectionGapType = "missing_evidence" | "weak_evidence"
|
|
5
7
|
export type InspectionEvidenceSupport = "supported" | "weak" | "unknown"
|
|
6
8
|
|
|
@@ -13,11 +15,19 @@ export interface InspectionContext {
|
|
|
13
15
|
outputPath: string
|
|
14
16
|
narrativeBrief?: NarrativeBrief
|
|
15
17
|
sourceMaterials: InspectionSourceMaterial[]
|
|
18
|
+
narrative?: InspectionNarrativeStateContext
|
|
16
19
|
slides: InspectionSlideContext[]
|
|
17
20
|
gaps: InspectionGap[]
|
|
18
21
|
appendixCandidates: InspectionAppendixCandidate[]
|
|
19
22
|
objectionContext: InspectionNarrativeContext[]
|
|
20
23
|
riskContext: InspectionNarrativeContext[]
|
|
24
|
+
artifactCoverage: InspectionArtifactCoverage[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InspectionNarrativeStateContext {
|
|
28
|
+
id: string
|
|
29
|
+
status: string
|
|
30
|
+
claimCount: number
|
|
21
31
|
}
|
|
22
32
|
|
|
23
33
|
export interface InspectionSourceMaterial extends SourceMaterial {
|
|
@@ -46,6 +56,7 @@ export interface InspectionSlideText {
|
|
|
46
56
|
|
|
47
57
|
export interface InspectionClaimCandidate {
|
|
48
58
|
id: string
|
|
59
|
+
canonicalClaimId?: string
|
|
49
60
|
slideIndex: number
|
|
50
61
|
slideTitle: string
|
|
51
62
|
origin: InspectionClaimOrigin
|
|
@@ -54,9 +65,18 @@ export interface InspectionClaimCandidate {
|
|
|
54
65
|
evidenceSupport: InspectionEvidenceSupport
|
|
55
66
|
evidence: InspectionEvidenceTrace[]
|
|
56
67
|
gaps: InspectionGap[]
|
|
68
|
+
evidenceBindingIds: string[]
|
|
69
|
+
supportedScope?: string
|
|
70
|
+
unsupportedScope?: string
|
|
71
|
+
caveats: string[]
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
export interface InspectionEvidenceTrace extends EvidenceRef {
|
|
75
|
+
evidenceBindingId?: string
|
|
76
|
+
claimId?: string
|
|
77
|
+
supportScope?: string
|
|
78
|
+
unsupportedScope?: string
|
|
79
|
+
strength?: NarrativeEvidenceBinding["strength"]
|
|
60
80
|
slideIndex: number
|
|
61
81
|
slideTitle: string
|
|
62
82
|
hasDetail: boolean
|
|
@@ -80,19 +100,44 @@ export interface InspectionAppendixCandidate {
|
|
|
80
100
|
|
|
81
101
|
export interface InspectionNarrativeContext {
|
|
82
102
|
text: string
|
|
83
|
-
source: "narrativeBrief" | "slide"
|
|
103
|
+
source: "narrative" | "narrativeBrief" | "slide"
|
|
84
104
|
slideIndex?: number
|
|
85
105
|
slideTitle?: string
|
|
86
106
|
}
|
|
87
107
|
|
|
108
|
+
export interface InspectionArtifactCoverage {
|
|
109
|
+
artifactId: string
|
|
110
|
+
type: ArtifactClaimRef["type"]
|
|
111
|
+
outputPath?: string
|
|
112
|
+
coverageStatus: ArtifactClaimRef["coverageStatus"]
|
|
113
|
+
claimIds: string[]
|
|
114
|
+
affectedClaimIds: string[]
|
|
115
|
+
missingClaimIds: string[]
|
|
116
|
+
stale: boolean
|
|
117
|
+
staleReason?: string
|
|
118
|
+
staleReasons: string[]
|
|
119
|
+
note?: string
|
|
120
|
+
slideRefs: InspectionArtifactSlideRef[]
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface InspectionArtifactSlideRef {
|
|
124
|
+
claimId: string
|
|
125
|
+
slideIndex: number
|
|
126
|
+
slideTitle: string
|
|
127
|
+
role: ClaimSlideRef["role"]
|
|
128
|
+
match: ClaimSlideRef["match"]
|
|
129
|
+
location: string
|
|
130
|
+
}
|
|
131
|
+
|
|
88
132
|
export function compileInspectionContext(state: DecksState, slug?: string): InspectionContext {
|
|
89
133
|
const deck = activeDeck(state, slug)
|
|
134
|
+
const narrative = state.narrative
|
|
90
135
|
const evidence = collectEvidence(deck)
|
|
91
136
|
const sourceMaterials = compileSourceMaterials(state.workspace.sourceMaterials ?? [], evidence)
|
|
92
137
|
const slides = deck.slides
|
|
93
138
|
.slice()
|
|
94
139
|
.sort((a, b) => a.index - b.index)
|
|
95
|
-
.map((slide) => compileSlide(slide))
|
|
140
|
+
.map((slide) => compileSlide(slide, narrative))
|
|
96
141
|
const gaps = slides.flatMap((slide) => slide.claims.flatMap((claim) => claim.gaps))
|
|
97
142
|
|
|
98
143
|
return {
|
|
@@ -103,24 +148,56 @@ export function compileInspectionContext(state: DecksState, slug?: string): Insp
|
|
|
103
148
|
language: deck.language,
|
|
104
149
|
outputPath: deck.outputPath,
|
|
105
150
|
narrativeBrief: deck.narrativeBrief,
|
|
151
|
+
narrative: narrative ? { id: narrative.id, status: narrative.status, claimCount: narrative.claims.length } : undefined,
|
|
106
152
|
sourceMaterials,
|
|
107
153
|
slides,
|
|
108
154
|
gaps,
|
|
109
155
|
appendixCandidates: compileAppendixCandidates(slides),
|
|
110
|
-
objectionContext: compileNarrativeList(deck, "objections"),
|
|
111
|
-
riskContext: compileNarrativeList(deck, "risks"),
|
|
156
|
+
objectionContext: compileNarrativeList(deck, "objections", narrative),
|
|
157
|
+
riskContext: compileNarrativeList(deck, "risks", narrative),
|
|
158
|
+
artifactCoverage: compileArtifactCoverage(state),
|
|
112
159
|
}
|
|
113
160
|
}
|
|
114
161
|
|
|
162
|
+
function compileArtifactCoverage(state: DecksState): InspectionArtifactCoverage[] {
|
|
163
|
+
return getArtifactClaimRefs(state).map((artifact) => ({
|
|
164
|
+
artifactId: artifact.artifactId,
|
|
165
|
+
type: artifact.type,
|
|
166
|
+
outputPath: artifact.outputPath,
|
|
167
|
+
coverageStatus: artifact.coverageStatus,
|
|
168
|
+
claimIds: artifact.claimIds,
|
|
169
|
+
affectedClaimIds: artifact.affectedClaimIds,
|
|
170
|
+
missingClaimIds: artifact.missingClaimIds,
|
|
171
|
+
stale: artifact.stale,
|
|
172
|
+
staleReason: artifact.staleReason,
|
|
173
|
+
staleReasons: artifact.staleReasons,
|
|
174
|
+
note: artifact.note,
|
|
175
|
+
slideRefs: artifact.slideRefs.map((ref) => ({
|
|
176
|
+
claimId: ref.claimId,
|
|
177
|
+
slideIndex: ref.slideIndex,
|
|
178
|
+
slideTitle: ref.slideTitle,
|
|
179
|
+
role: ref.role,
|
|
180
|
+
match: ref.match,
|
|
181
|
+
location: ref.location,
|
|
182
|
+
})),
|
|
183
|
+
}))
|
|
184
|
+
}
|
|
185
|
+
|
|
115
186
|
function activeDeck(state: DecksState, slug?: string): DeckSpec {
|
|
116
187
|
const key = slug || state.activeDeck || (Object.keys(state.decks).length === 1 ? Object.keys(state.decks)[0] : undefined)
|
|
117
188
|
if (!key || !state.decks[key]) throw new Error("No active deck is available for inspection context compilation.")
|
|
118
189
|
return state.decks[key]
|
|
119
190
|
}
|
|
120
191
|
|
|
121
|
-
function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
192
|
+
function compileSlide(slide: SlideSpec, narrative: NarrativeStateV1 | undefined): InspectionSlideContext {
|
|
122
193
|
const evidence = slide.evidence.map((item) => compileEvidence(slide, item))
|
|
123
|
-
const
|
|
194
|
+
const canonicalClaims = narrative ? canonicalClaimCandidates(slide, narrative, evidence) : []
|
|
195
|
+
const canonicalText = new Set(canonicalClaims.map((claim) => normalizeText(claim.text)))
|
|
196
|
+
const heuristicClaims = claimCandidates(slide)
|
|
197
|
+
.filter((claim) => !canonicalText.has(normalizeText(claim.text)))
|
|
198
|
+
.map((claim, position) => compileClaim(slide, claim, position, evidence))
|
|
199
|
+
const claims = [...canonicalClaims, ...heuristicClaims]
|
|
200
|
+
const claimCaveats = canonicalClaims.flatMap((claim) => claim.caveats)
|
|
124
201
|
return {
|
|
125
202
|
index: slide.index,
|
|
126
203
|
title: slide.title,
|
|
@@ -136,7 +213,61 @@ function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
|
136
213
|
},
|
|
137
214
|
claims,
|
|
138
215
|
evidence,
|
|
139
|
-
caveats:
|
|
216
|
+
caveats: dedupeText([
|
|
217
|
+
...evidence.map((item) => item.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
218
|
+
...claimCaveats,
|
|
219
|
+
]),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function canonicalClaimCandidates(slide: SlideSpec, narrative: NarrativeStateV1, slideEvidence: InspectionEvidenceTrace[]): InspectionClaimCandidate[] {
|
|
224
|
+
const claimRefs = slide.claimRefs ?? []
|
|
225
|
+
const metadataClaimIds = new Set([
|
|
226
|
+
...claimRefs.map((ref) => ref.claimId),
|
|
227
|
+
...(slide.claimIds ?? []),
|
|
228
|
+
].filter(Boolean))
|
|
229
|
+
const evidenceBindingIds = new Set(slide.evidenceBindingIds ?? [])
|
|
230
|
+
for (const binding of narrative.evidenceBindings) {
|
|
231
|
+
if (evidenceBindingIds.has(binding.id)) metadataClaimIds.add(binding.claimId)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return narrative.claims
|
|
235
|
+
.filter((claim) => metadataClaimIds.has(claim.id))
|
|
236
|
+
.map((claim) => compileCanonicalClaim(slide, claim, narrative.evidenceBindings, slideEvidence, evidenceBindingIds))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function compileCanonicalClaim(
|
|
240
|
+
slide: SlideSpec,
|
|
241
|
+
claim: NarrativeClaim,
|
|
242
|
+
bindings: NarrativeEvidenceBinding[],
|
|
243
|
+
slideEvidence: InspectionEvidenceTrace[],
|
|
244
|
+
slideEvidenceBindingIds: Set<string>,
|
|
245
|
+
): InspectionClaimCandidate {
|
|
246
|
+
const allClaimBindings = bindings.filter((binding) => binding.claimId === claim.id)
|
|
247
|
+
const selectedBindings = allClaimBindings.filter((binding) => slideEvidenceBindingIds.size === 0 || slideEvidenceBindingIds.has(binding.id))
|
|
248
|
+
const evidenceBindings = selectedBindings.length > 0 ? selectedBindings : allClaimBindings
|
|
249
|
+
const evidence = evidenceBindings.length > 0
|
|
250
|
+
? evidenceBindings.map((binding) => compileEvidenceBinding(slide, binding))
|
|
251
|
+
: slideEvidence
|
|
252
|
+
const gaps = canonicalClaimGaps(slide, claim, evidence)
|
|
253
|
+
return {
|
|
254
|
+
id: claim.id,
|
|
255
|
+
canonicalClaimId: claim.id,
|
|
256
|
+
slideIndex: slide.index,
|
|
257
|
+
slideTitle: slide.title,
|
|
258
|
+
origin: "narrative",
|
|
259
|
+
text: claim.text,
|
|
260
|
+
evidenceSensitive: claim.evidenceRequired || isEvidenceSensitiveClaim(claim.text),
|
|
261
|
+
evidenceSupport: narrativeEvidenceSupport(claim, evidence),
|
|
262
|
+
evidence,
|
|
263
|
+
gaps,
|
|
264
|
+
evidenceBindingIds: evidenceBindings.map((binding) => binding.id),
|
|
265
|
+
supportedScope: claim.supportedScope,
|
|
266
|
+
unsupportedScope: claim.unsupportedScope,
|
|
267
|
+
caveats: dedupeText([
|
|
268
|
+
...(claim.caveats ?? []),
|
|
269
|
+
...evidenceBindings.map((binding) => binding.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
270
|
+
]),
|
|
140
271
|
}
|
|
141
272
|
}
|
|
142
273
|
|
|
@@ -159,9 +290,36 @@ function compileClaim(
|
|
|
159
290
|
evidenceSupport: evidenceSupport(evidence),
|
|
160
291
|
evidence,
|
|
161
292
|
gaps,
|
|
293
|
+
evidenceBindingIds: [],
|
|
294
|
+
caveats: [],
|
|
162
295
|
}
|
|
163
296
|
}
|
|
164
297
|
|
|
298
|
+
function canonicalClaimGaps(slide: SlideSpec, claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
299
|
+
if (!claim.evidenceRequired) return []
|
|
300
|
+
if (claim.evidenceStatus === "missing" || evidence.length === 0) {
|
|
301
|
+
return [{
|
|
302
|
+
type: "missing_evidence",
|
|
303
|
+
slideIndex: slide.index,
|
|
304
|
+
slideTitle: slide.title,
|
|
305
|
+
claimId: claim.id,
|
|
306
|
+
claimText: claim.text,
|
|
307
|
+
message: "Canonical narrative claim requires evidence but has no bound evidence trace.",
|
|
308
|
+
}]
|
|
309
|
+
}
|
|
310
|
+
if (claim.evidenceStatus === "weak" || evidence.some((item) => !item.hasDetail)) {
|
|
311
|
+
return [{
|
|
312
|
+
type: "weak_evidence",
|
|
313
|
+
slideIndex: slide.index,
|
|
314
|
+
slideTitle: slide.title,
|
|
315
|
+
claimId: claim.id,
|
|
316
|
+
claimText: claim.text,
|
|
317
|
+
message: "Canonical narrative claim has weak or source-only evidence trace.",
|
|
318
|
+
}]
|
|
319
|
+
}
|
|
320
|
+
return []
|
|
321
|
+
}
|
|
322
|
+
|
|
165
323
|
function claimGaps(slide: SlideSpec, claimId: string, claimText: string, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
166
324
|
if (evidence.length === 0) {
|
|
167
325
|
return [{
|
|
@@ -192,6 +350,12 @@ function evidenceSupport(evidence: InspectionEvidenceTrace[]): InspectionEvidenc
|
|
|
192
350
|
return "supported"
|
|
193
351
|
}
|
|
194
352
|
|
|
353
|
+
function narrativeEvidenceSupport(claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionEvidenceSupport {
|
|
354
|
+
if (claim.evidenceStatus === "supported" || claim.evidenceStatus === "not_required") return "supported"
|
|
355
|
+
if (claim.evidenceStatus === "partial" || claim.evidenceStatus === "weak") return "weak"
|
|
356
|
+
return evidenceSupport(evidence)
|
|
357
|
+
}
|
|
358
|
+
|
|
195
359
|
function claimCandidates(slide: SlideSpec): Array<{ origin: InspectionClaimOrigin; text: string }> {
|
|
196
360
|
const claims: Array<{ origin: InspectionClaimOrigin; text: string }> = []
|
|
197
361
|
pushClaim(claims, "title", slide.title)
|
|
@@ -218,6 +382,29 @@ function compileEvidence(slide: SlideSpec, evidence: EvidenceRef): InspectionEvi
|
|
|
218
382
|
}
|
|
219
383
|
}
|
|
220
384
|
|
|
385
|
+
function compileEvidenceBinding(slide: SlideSpec, binding: NarrativeEvidenceBinding): InspectionEvidenceTrace {
|
|
386
|
+
const evidence: EvidenceRef = {
|
|
387
|
+
source: binding.source,
|
|
388
|
+
sourcePath: binding.sourcePath,
|
|
389
|
+
findingsFile: binding.findingsFile,
|
|
390
|
+
quote: binding.quote,
|
|
391
|
+
location: binding.location,
|
|
392
|
+
url: binding.url,
|
|
393
|
+
caveat: binding.caveat,
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
...evidence,
|
|
397
|
+
evidenceBindingId: binding.id,
|
|
398
|
+
claimId: binding.claimId,
|
|
399
|
+
supportScope: binding.supportScope,
|
|
400
|
+
unsupportedScope: binding.unsupportedScope,
|
|
401
|
+
strength: binding.strength,
|
|
402
|
+
slideIndex: slide.index,
|
|
403
|
+
slideTitle: slide.title,
|
|
404
|
+
hasDetail: hasEvidenceDetail(evidence),
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
221
408
|
function collectEvidence(deck: DeckSpec): InspectionEvidenceTrace[] {
|
|
222
409
|
return deck.slides.flatMap((slide) => slide.evidence.map((item) => compileEvidence(slide, item)))
|
|
223
410
|
}
|
|
@@ -253,7 +440,10 @@ function appendixReason(slide: InspectionSlideContext): string {
|
|
|
253
440
|
return "Slide has recorded evidence that may be useful for source excerpts or backup detail."
|
|
254
441
|
}
|
|
255
442
|
|
|
256
|
-
function compileNarrativeList(deck: DeckSpec, key: "objections" | "risks"): InspectionNarrativeContext[] {
|
|
443
|
+
function compileNarrativeList(deck: DeckSpec, key: "objections" | "risks", narrative: NarrativeStateV1 | undefined): InspectionNarrativeContext[] {
|
|
444
|
+
const fromNarrative = key === "objections"
|
|
445
|
+
? (narrative?.objections ?? []).map((item) => ({ text: item.text, source: "narrative" as const }))
|
|
446
|
+
: (narrative?.risks ?? []).map((item) => ({ text: item.text, source: "narrative" as const }))
|
|
257
447
|
const fromBrief = (deck.narrativeBrief?.[key] ?? []).map((text) => ({ text, source: "narrativeBrief" as const }))
|
|
258
448
|
const role = key === "risks" ? "risk" : undefined
|
|
259
449
|
const fromSlides = deck.slides
|
|
@@ -264,7 +454,19 @@ function compileNarrativeList(deck: DeckSpec, key: "objections" | "risks"): Insp
|
|
|
264
454
|
slideIndex: slide.index,
|
|
265
455
|
slideTitle: slide.title,
|
|
266
456
|
})))
|
|
267
|
-
return [...fromBrief, ...fromSlides]
|
|
457
|
+
return dedupeNarrativeContext([...fromNarrative, ...fromBrief, ...fromSlides])
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function dedupeNarrativeContext(values: InspectionNarrativeContext[]): InspectionNarrativeContext[] {
|
|
461
|
+
const seen = new Set<string>()
|
|
462
|
+
const result: InspectionNarrativeContext[] = []
|
|
463
|
+
for (const value of values) {
|
|
464
|
+
const key = `${normalizeText(value.text)}:${value.slideIndex ?? "global"}`
|
|
465
|
+
if (seen.has(key)) continue
|
|
466
|
+
seen.add(key)
|
|
467
|
+
result.push(value)
|
|
468
|
+
}
|
|
469
|
+
return result
|
|
268
470
|
}
|
|
269
471
|
|
|
270
472
|
function slideTextList(slide: SlideSpec): string[] {
|
|
@@ -303,6 +505,24 @@ function cleanOptionalText(value: string | undefined): string | undefined {
|
|
|
303
505
|
return text || undefined
|
|
304
506
|
}
|
|
305
507
|
|
|
508
|
+
function dedupeText(values: string[]): string[] {
|
|
509
|
+
const seen = new Set<string>()
|
|
510
|
+
const result: string[] = []
|
|
511
|
+
for (const value of values) {
|
|
512
|
+
const cleaned = cleanOptionalText(value)
|
|
513
|
+
if (!cleaned) continue
|
|
514
|
+
const key = normalizeText(cleaned)
|
|
515
|
+
if (seen.has(key)) continue
|
|
516
|
+
seen.add(key)
|
|
517
|
+
result.push(cleaned)
|
|
518
|
+
}
|
|
519
|
+
return result
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeText(value: string | undefined): string {
|
|
523
|
+
return value?.replace(/\s+/g, " ").trim().toLowerCase() ?? ""
|
|
524
|
+
}
|
|
525
|
+
|
|
306
526
|
const EVIDENCE_SENSITIVE_TERMS = [
|
|
307
527
|
/\bmarket size\b/,
|
|
308
528
|
/\bcagr\b/,
|
|
@@ -45,6 +45,7 @@ export interface InspectionElementSnapshot {
|
|
|
45
45
|
export interface InspectionElementMatch {
|
|
46
46
|
slide?: InspectionSlideContext
|
|
47
47
|
claim?: InspectionClaimCandidate
|
|
48
|
+
candidateClaims?: InspectionClaimCandidate[]
|
|
48
49
|
evidence: InspectionEvidenceTrace[]
|
|
49
50
|
gaps: InspectionGap[]
|
|
50
51
|
caveats: string[]
|
|
@@ -55,8 +56,12 @@ export interface InspectionElementMatch {
|
|
|
55
56
|
|
|
56
57
|
export function matchInspectionElement(context: InspectionContext, snapshot: InspectionElementSnapshot): InspectionElementMatch {
|
|
57
58
|
const selectedText = normalizeText(snapshot.text)
|
|
59
|
+
const surroundingText = normalizeText(snapshot.nearbyText || snapshot.outerHTMLExcerpt)
|
|
58
60
|
const candidateSlides = candidateSlidesForSnapshot(context, snapshot)
|
|
59
61
|
|
|
62
|
+
const anchoredClaim = findAnchoredClaimMatch(candidateSlides, snapshot)
|
|
63
|
+
if (anchoredClaim) return claimMatch(context, anchoredClaim.slide, anchoredClaim.claim, "high", "Matched explicit claim anchor from selection snapshot.")
|
|
64
|
+
|
|
60
65
|
if (selectedText) {
|
|
61
66
|
const exactClaim = findClaimMatch(candidateSlides, selectedText, "exact")
|
|
62
67
|
if (exactClaim) return claimMatch(context, exactClaim.slide, exactClaim.claim, "high", "Exact normalized text match.")
|
|
@@ -77,8 +82,27 @@ export function matchInspectionElement(context: InspectionContext, snapshot: Ins
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
if (surroundingText && surroundingText !== selectedText) {
|
|
86
|
+
const contextualClaim = findClaimMatch(candidateSlides, surroundingText, "contains")
|
|
87
|
+
if (contextualClaim) return claimMatch(context, contextualClaim.slide, contextualClaim.claim, "medium", "Matched claim using surrounding slide context.")
|
|
88
|
+
}
|
|
89
|
+
|
|
80
90
|
const slide = candidateSlides[0]
|
|
81
|
-
if (slide)
|
|
91
|
+
if (slide) {
|
|
92
|
+
const canonicalClaims = slide.claims.filter((claim) => claim.canonicalClaimId || claim.origin === "narrative")
|
|
93
|
+
if (canonicalClaims.length === 1) {
|
|
94
|
+
return claimMatch(context, slide, canonicalClaims[0], "medium", "Selected element matched the slide; the slide has one canonical narrative claim candidate.", canonicalClaims)
|
|
95
|
+
}
|
|
96
|
+
return slideMatch(
|
|
97
|
+
context,
|
|
98
|
+
slide,
|
|
99
|
+
snapshot.slideIndex ? "medium" : "low",
|
|
100
|
+
canonicalClaims.length > 1
|
|
101
|
+
? "Matched slide only; multiple canonical claim candidates are available, so no claim id was chosen by semantic guess."
|
|
102
|
+
: snapshot.slideIndex ? "Matched by slideIndex only." : "No claim text matched; returning first candidate slide.",
|
|
103
|
+
canonicalClaims,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
82
106
|
|
|
83
107
|
return {
|
|
84
108
|
evidence: [],
|
|
@@ -114,6 +138,37 @@ function findClaimMatch(
|
|
|
114
138
|
return undefined
|
|
115
139
|
}
|
|
116
140
|
|
|
141
|
+
function findAnchoredClaimMatch(
|
|
142
|
+
slides: InspectionSlideContext[],
|
|
143
|
+
snapshot: InspectionElementSnapshot,
|
|
144
|
+
): { slide: InspectionSlideContext; claim: InspectionClaimCandidate } | undefined {
|
|
145
|
+
const claimIds = explicitClaimIds(snapshot)
|
|
146
|
+
if (claimIds.length === 0) return undefined
|
|
147
|
+
for (const slide of slides) {
|
|
148
|
+
for (const claim of slide.claims) {
|
|
149
|
+
const ids = [claim.id, claim.canonicalClaimId].filter((item): item is string => Boolean(item))
|
|
150
|
+
if (ids.some((id) => claimIds.includes(id))) return { slide, claim }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function explicitClaimIds(snapshot: InspectionElementSnapshot): string[] {
|
|
157
|
+
const values = [
|
|
158
|
+
snapshot.selector,
|
|
159
|
+
snapshot.domPath,
|
|
160
|
+
snapshot.outerHTMLExcerpt,
|
|
161
|
+
...(snapshot.elements ?? []).flatMap((item) => [item.selector, item.domPath, item.outerHTMLExcerpt]),
|
|
162
|
+
]
|
|
163
|
+
const ids: string[] = []
|
|
164
|
+
for (const value of values) {
|
|
165
|
+
if (!value) continue
|
|
166
|
+
for (const match of value.matchAll(/data-claim-id\s*=\s*["']([^"']+)["']/gi)) ids.push(match[1])
|
|
167
|
+
for (const match of value.matchAll(/data-claim-id=([^\]\s>"']+)/gi)) ids.push(match[1])
|
|
168
|
+
}
|
|
169
|
+
return dedupe(ids.map((item) => item.trim()).filter(Boolean))
|
|
170
|
+
}
|
|
171
|
+
|
|
117
172
|
function conservativeContains(claimText: string, selectedText: string): boolean {
|
|
118
173
|
if (selectedText.length < 12 && claimText.length < 12) return false
|
|
119
174
|
return claimText.includes(selectedText) || selectedText.includes(claimText)
|
|
@@ -125,10 +180,12 @@ function claimMatch(
|
|
|
125
180
|
claim: InspectionClaimCandidate,
|
|
126
181
|
confidence: InspectionMatchConfidence,
|
|
127
182
|
reason: string,
|
|
183
|
+
candidateClaims: InspectionClaimCandidate[] = [],
|
|
128
184
|
): InspectionElementMatch {
|
|
129
185
|
return {
|
|
130
186
|
slide,
|
|
131
187
|
claim,
|
|
188
|
+
candidateClaims: candidateClaims.length ? candidateClaims : [claim],
|
|
132
189
|
evidence: claim.evidence,
|
|
133
190
|
gaps: claim.gaps,
|
|
134
191
|
caveats: slide.caveats,
|
|
@@ -143,9 +200,11 @@ function slideMatch(
|
|
|
143
200
|
slide: InspectionSlideContext,
|
|
144
201
|
confidence: InspectionMatchConfidence,
|
|
145
202
|
reason: string,
|
|
203
|
+
candidateClaims: InspectionClaimCandidate[] = [],
|
|
146
204
|
): InspectionElementMatch {
|
|
147
205
|
return {
|
|
148
206
|
slide,
|
|
207
|
+
candidateClaims,
|
|
149
208
|
evidence: slide.evidence,
|
|
150
209
|
gaps: slide.claims.flatMap((claim) => claim.gaps),
|
|
151
210
|
caveats: slide.caveats,
|
|
@@ -167,3 +226,14 @@ function normalizeText(text: string | undefined): string {
|
|
|
167
226
|
.trim()
|
|
168
227
|
.toLowerCase()
|
|
169
228
|
}
|
|
229
|
+
|
|
230
|
+
function dedupe(values: string[]): string[] {
|
|
231
|
+
const seen = new Set<string>()
|
|
232
|
+
const result: string[] = []
|
|
233
|
+
for (const value of values) {
|
|
234
|
+
if (seen.has(value)) continue
|
|
235
|
+
seen.add(value)
|
|
236
|
+
result.push(value)
|
|
237
|
+
}
|
|
238
|
+
return result
|
|
239
|
+
}
|