@cyber-dash-tech/revela 0.8.9 → 0.10.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 +29 -4
- package/README.zh-CN.md +29 -4
- package/designs/monet/DESIGN.md +9 -9
- package/designs/starter/DESIGN.md +8 -8
- package/designs/summit/DESIGN.md +9 -9
- package/lib/agents/narrative-reviewer-prompt.ts +143 -0
- package/lib/commands/help.ts +2 -0
- package/lib/commands/inspect.ts +23 -0
- package/lib/commands/refine.ts +26 -0
- package/lib/commands/review.ts +18 -6
- package/lib/decks-state.ts +601 -6
- package/lib/inspect/open.ts +61 -0
- package/lib/inspect/prompt.ts +32 -0
- package/lib/inspect/request.ts +70 -0
- package/lib/inspect/requests.ts +86 -0
- package/lib/inspect/server.ts +1063 -0
- package/lib/inspect/slide-index.ts +12 -0
- package/lib/inspection-context/compile.ts +346 -0
- package/lib/inspection-context/match.ts +169 -0
- package/lib/inspection-context/project.ts +263 -0
- package/lib/inspection-context/result.ts +160 -0
- package/lib/refine/open.ts +68 -0
- package/lib/refine/server.ts +1581 -0
- package/package.json +1 -1
- package/plugin.ts +47 -2
- package/skill/SKILL.md +46 -8
- package/tools/decks.ts +23 -2
- package/tools/inspection-context.ts +22 -0
- package/tools/inspection-result.ts +63 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function canonicalInspectSlideIndex(input: {
|
|
2
|
+
dataSlideIndex?: string | number | null
|
|
3
|
+
domOrdinal?: number
|
|
4
|
+
}): number | undefined {
|
|
5
|
+
if (input.dataSlideIndex === undefined || input.dataSlideIndex === null || input.dataSlideIndex === "") {
|
|
6
|
+
return typeof input.domOrdinal === "number" && input.domOrdinal >= 0 ? input.domOrdinal + 1 : undefined
|
|
7
|
+
}
|
|
8
|
+
const explicit = Number(input.dataSlideIndex)
|
|
9
|
+
if (!Number.isNaN(explicit)) return explicit
|
|
10
|
+
if (typeof input.domOrdinal === "number" && input.domOrdinal >= 0) return input.domOrdinal + 1
|
|
11
|
+
return undefined
|
|
12
|
+
}
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, NarrativeRole, SlideSpec, SourceMaterial } from "../decks-state"
|
|
2
|
+
|
|
3
|
+
export type InspectionClaimOrigin = "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
|
+
export type InspectionGapType = "missing_evidence" | "weak_evidence"
|
|
5
|
+
export type InspectionEvidenceSupport = "supported" | "weak" | "unknown"
|
|
6
|
+
|
|
7
|
+
export interface InspectionContext {
|
|
8
|
+
version: 1
|
|
9
|
+
slug: string
|
|
10
|
+
goal: string
|
|
11
|
+
audience?: string
|
|
12
|
+
language?: string
|
|
13
|
+
outputPath: string
|
|
14
|
+
narrativeBrief?: NarrativeBrief
|
|
15
|
+
sourceMaterials: InspectionSourceMaterial[]
|
|
16
|
+
slides: InspectionSlideContext[]
|
|
17
|
+
gaps: InspectionGap[]
|
|
18
|
+
appendixCandidates: InspectionAppendixCandidate[]
|
|
19
|
+
objectionContext: InspectionNarrativeContext[]
|
|
20
|
+
riskContext: InspectionNarrativeContext[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InspectionSourceMaterial extends SourceMaterial {
|
|
24
|
+
linkedEvidenceCount: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface InspectionSlideContext {
|
|
28
|
+
index: number
|
|
29
|
+
title: string
|
|
30
|
+
purpose?: string
|
|
31
|
+
narrativeRole?: NarrativeRole
|
|
32
|
+
layout: string
|
|
33
|
+
components: string[]
|
|
34
|
+
text: InspectionSlideText
|
|
35
|
+
claims: InspectionClaimCandidate[]
|
|
36
|
+
evidence: InspectionEvidenceTrace[]
|
|
37
|
+
caveats: string[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface InspectionSlideText {
|
|
41
|
+
headline?: string
|
|
42
|
+
body: string[]
|
|
43
|
+
bullets: string[]
|
|
44
|
+
speakerNotes?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface InspectionClaimCandidate {
|
|
48
|
+
id: string
|
|
49
|
+
slideIndex: number
|
|
50
|
+
slideTitle: string
|
|
51
|
+
origin: InspectionClaimOrigin
|
|
52
|
+
text: string
|
|
53
|
+
evidenceSensitive: boolean
|
|
54
|
+
evidenceSupport: InspectionEvidenceSupport
|
|
55
|
+
evidence: InspectionEvidenceTrace[]
|
|
56
|
+
gaps: InspectionGap[]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface InspectionEvidenceTrace extends EvidenceRef {
|
|
60
|
+
slideIndex: number
|
|
61
|
+
slideTitle: string
|
|
62
|
+
hasDetail: boolean
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface InspectionGap {
|
|
66
|
+
type: InspectionGapType
|
|
67
|
+
slideIndex: number
|
|
68
|
+
slideTitle: string
|
|
69
|
+
claimId: string
|
|
70
|
+
claimText: string
|
|
71
|
+
message: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface InspectionAppendixCandidate {
|
|
75
|
+
slideIndex: number
|
|
76
|
+
slideTitle: string
|
|
77
|
+
reason: string
|
|
78
|
+
evidence: InspectionEvidenceTrace[]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface InspectionNarrativeContext {
|
|
82
|
+
text: string
|
|
83
|
+
source: "narrativeBrief" | "slide"
|
|
84
|
+
slideIndex?: number
|
|
85
|
+
slideTitle?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function compileInspectionContext(state: DecksState, slug?: string): InspectionContext {
|
|
89
|
+
const deck = activeDeck(state, slug)
|
|
90
|
+
const evidence = collectEvidence(deck)
|
|
91
|
+
const sourceMaterials = compileSourceMaterials(state.workspace.sourceMaterials ?? [], evidence)
|
|
92
|
+
const slides = deck.slides
|
|
93
|
+
.slice()
|
|
94
|
+
.sort((a, b) => a.index - b.index)
|
|
95
|
+
.map((slide) => compileSlide(slide))
|
|
96
|
+
const gaps = slides.flatMap((slide) => slide.claims.flatMap((claim) => claim.gaps))
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
version: 1,
|
|
100
|
+
slug: deck.slug,
|
|
101
|
+
goal: deck.goal,
|
|
102
|
+
audience: deck.audience,
|
|
103
|
+
language: deck.language,
|
|
104
|
+
outputPath: deck.outputPath,
|
|
105
|
+
narrativeBrief: deck.narrativeBrief,
|
|
106
|
+
sourceMaterials,
|
|
107
|
+
slides,
|
|
108
|
+
gaps,
|
|
109
|
+
appendixCandidates: compileAppendixCandidates(slides),
|
|
110
|
+
objectionContext: compileNarrativeList(deck, "objections"),
|
|
111
|
+
riskContext: compileNarrativeList(deck, "risks"),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function activeDeck(state: DecksState, slug?: string): DeckSpec {
|
|
116
|
+
const key = slug || state.activeDeck || (Object.keys(state.decks).length === 1 ? Object.keys(state.decks)[0] : undefined)
|
|
117
|
+
if (!key || !state.decks[key]) throw new Error("No active deck is available for inspection context compilation.")
|
|
118
|
+
return state.decks[key]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
122
|
+
const evidence = slide.evidence.map((item) => compileEvidence(slide, item))
|
|
123
|
+
const claims = claimCandidates(slide).map((claim, position) => compileClaim(slide, claim, position, evidence))
|
|
124
|
+
return {
|
|
125
|
+
index: slide.index,
|
|
126
|
+
title: slide.title,
|
|
127
|
+
purpose: slide.purpose,
|
|
128
|
+
narrativeRole: slide.narrativeRole,
|
|
129
|
+
layout: slide.layout,
|
|
130
|
+
components: slide.components,
|
|
131
|
+
text: {
|
|
132
|
+
headline: cleanOptionalText(slide.content.headline),
|
|
133
|
+
body: cleanTextList(slide.content.body),
|
|
134
|
+
bullets: cleanTextList(slide.content.bullets),
|
|
135
|
+
speakerNotes: cleanOptionalText(slide.content.speakerNotes),
|
|
136
|
+
},
|
|
137
|
+
claims,
|
|
138
|
+
evidence,
|
|
139
|
+
caveats: evidence.map((item) => item.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function compileClaim(
|
|
144
|
+
slide: SlideSpec,
|
|
145
|
+
claim: { origin: InspectionClaimOrigin; text: string },
|
|
146
|
+
position: number,
|
|
147
|
+
evidence: InspectionEvidenceTrace[],
|
|
148
|
+
): InspectionClaimCandidate {
|
|
149
|
+
const id = `slide-${slide.index}-claim-${position + 1}`
|
|
150
|
+
const evidenceSensitive = isEvidenceSensitiveClaim(claim.text)
|
|
151
|
+
const gaps = evidenceSensitive ? claimGaps(slide, id, claim.text, evidence) : []
|
|
152
|
+
return {
|
|
153
|
+
id,
|
|
154
|
+
slideIndex: slide.index,
|
|
155
|
+
slideTitle: slide.title,
|
|
156
|
+
origin: claim.origin,
|
|
157
|
+
text: claim.text,
|
|
158
|
+
evidenceSensitive,
|
|
159
|
+
evidenceSupport: evidenceSupport(evidence),
|
|
160
|
+
evidence,
|
|
161
|
+
gaps,
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function claimGaps(slide: SlideSpec, claimId: string, claimText: string, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
166
|
+
if (evidence.length === 0) {
|
|
167
|
+
return [{
|
|
168
|
+
type: "missing_evidence",
|
|
169
|
+
slideIndex: slide.index,
|
|
170
|
+
slideTitle: slide.title,
|
|
171
|
+
claimId,
|
|
172
|
+
claimText,
|
|
173
|
+
message: "Evidence-sensitive claim has no slide-level evidence trace.",
|
|
174
|
+
}]
|
|
175
|
+
}
|
|
176
|
+
if (evidence.some((item) => !item.hasDetail)) {
|
|
177
|
+
return [{
|
|
178
|
+
type: "weak_evidence",
|
|
179
|
+
slideIndex: slide.index,
|
|
180
|
+
slideTitle: slide.title,
|
|
181
|
+
claimId,
|
|
182
|
+
claimText,
|
|
183
|
+
message: "Evidence-sensitive claim has source-only evidence without quote, location, URL, caveat, findings file, or source path detail.",
|
|
184
|
+
}]
|
|
185
|
+
}
|
|
186
|
+
return []
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function evidenceSupport(evidence: InspectionEvidenceTrace[]): InspectionEvidenceSupport {
|
|
190
|
+
if (evidence.length === 0) return "unknown"
|
|
191
|
+
if (evidence.some((item) => !item.hasDetail)) return "weak"
|
|
192
|
+
return "supported"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function claimCandidates(slide: SlideSpec): Array<{ origin: InspectionClaimOrigin; text: string }> {
|
|
196
|
+
const claims: Array<{ origin: InspectionClaimOrigin; text: string }> = []
|
|
197
|
+
pushClaim(claims, "title", slide.title)
|
|
198
|
+
pushClaim(claims, "purpose", slide.purpose)
|
|
199
|
+
pushClaim(claims, "headline", slide.content.headline)
|
|
200
|
+
for (const item of slide.content.body ?? []) pushClaim(claims, "body", item)
|
|
201
|
+
for (const item of slide.content.bullets ?? []) pushClaim(claims, "bullet", item)
|
|
202
|
+
return claims
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function pushClaim(claims: Array<{ origin: InspectionClaimOrigin; text: string }>, origin: InspectionClaimOrigin, text: string | undefined): void {
|
|
206
|
+
const value = cleanOptionalText(text)
|
|
207
|
+
if (!value) return
|
|
208
|
+
if (claims.some((claim) => claim.text === value)) return
|
|
209
|
+
claims.push({ origin, text: value })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function compileEvidence(slide: SlideSpec, evidence: EvidenceRef): InspectionEvidenceTrace {
|
|
213
|
+
return {
|
|
214
|
+
...evidence,
|
|
215
|
+
slideIndex: slide.index,
|
|
216
|
+
slideTitle: slide.title,
|
|
217
|
+
hasDetail: hasEvidenceDetail(evidence),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function collectEvidence(deck: DeckSpec): InspectionEvidenceTrace[] {
|
|
222
|
+
return deck.slides.flatMap((slide) => slide.evidence.map((item) => compileEvidence(slide, item)))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function compileSourceMaterials(sourceMaterials: SourceMaterial[], evidence: InspectionEvidenceTrace[]): InspectionSourceMaterial[] {
|
|
226
|
+
return sourceMaterials.map((material) => ({
|
|
227
|
+
...material,
|
|
228
|
+
linkedEvidenceCount: evidence.filter((item) => evidenceLinksSourceMaterial(item, material)).length,
|
|
229
|
+
}))
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function evidenceLinksSourceMaterial(evidence: EvidenceRef, material: SourceMaterial): boolean {
|
|
233
|
+
const path = material.path.trim()
|
|
234
|
+
if (!path) return false
|
|
235
|
+
return evidence.sourcePath === path || evidence.source === path || evidence.source.includes(path)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function compileAppendixCandidates(slides: InspectionSlideContext[]): InspectionAppendixCandidate[] {
|
|
239
|
+
return slides
|
|
240
|
+
.filter((slide) => slide.narrativeRole === "appendix" || slide.narrativeRole === "risk" || slide.evidence.length > 0 || slide.caveats.length > 0)
|
|
241
|
+
.map((slide) => ({
|
|
242
|
+
slideIndex: slide.index,
|
|
243
|
+
slideTitle: slide.title,
|
|
244
|
+
reason: appendixReason(slide),
|
|
245
|
+
evidence: slide.evidence,
|
|
246
|
+
}))
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function appendixReason(slide: InspectionSlideContext): string {
|
|
250
|
+
if (slide.narrativeRole === "appendix") return "Slide is explicitly marked as appendix material."
|
|
251
|
+
if (slide.narrativeRole === "risk") return "Risk or assumption handling may need backup detail."
|
|
252
|
+
if (slide.caveats.length > 0) return "Evidence caveats may need supporting appendix detail."
|
|
253
|
+
return "Slide has recorded evidence that may be useful for source excerpts or backup detail."
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function compileNarrativeList(deck: DeckSpec, key: "objections" | "risks"): InspectionNarrativeContext[] {
|
|
257
|
+
const fromBrief = (deck.narrativeBrief?.[key] ?? []).map((text) => ({ text, source: "narrativeBrief" as const }))
|
|
258
|
+
const role = key === "risks" ? "risk" : undefined
|
|
259
|
+
const fromSlides = deck.slides
|
|
260
|
+
.filter((slide) => role && slide.narrativeRole === role)
|
|
261
|
+
.flatMap((slide) => slideTextList(slide).map((text) => ({
|
|
262
|
+
text,
|
|
263
|
+
source: "slide" as const,
|
|
264
|
+
slideIndex: slide.index,
|
|
265
|
+
slideTitle: slide.title,
|
|
266
|
+
})))
|
|
267
|
+
return [...fromBrief, ...fromSlides]
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function slideTextList(slide: SlideSpec): string[] {
|
|
271
|
+
return [slide.content.headline, ...(slide.content.body ?? []), ...(slide.content.bullets ?? [])]
|
|
272
|
+
.map(cleanOptionalText)
|
|
273
|
+
.filter((item): item is string => Boolean(item))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function hasEvidenceDetail(evidence: EvidenceRef): boolean {
|
|
277
|
+
return Boolean(
|
|
278
|
+
evidence.quote?.trim() ||
|
|
279
|
+
evidence.page?.trim() ||
|
|
280
|
+
evidence.location?.trim() ||
|
|
281
|
+
evidence.url?.trim() ||
|
|
282
|
+
evidence.findingsFile?.trim() ||
|
|
283
|
+
evidence.sourcePath?.trim() ||
|
|
284
|
+
evidence.extractedTextPath?.trim()
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function isEvidenceSensitiveClaim(text: string): boolean {
|
|
289
|
+
const normalized = text.toLowerCase()
|
|
290
|
+
return hasNumericClaim(normalized) || EVIDENCE_SENSITIVE_TERMS.some((pattern) => pattern.test(normalized))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function hasNumericClaim(text: string): boolean {
|
|
294
|
+
return /(?:[$¥€£]\s?\d|\d+(?:\.\d+)?\s?(?:%|x|倍|万|亿|m|mn|million|b|bn|billion|k|千|年|months?|days?|users?|customers?|revenue|margin|cagr|tam|sam|som)\b|\b20\d{2}\b)/i.test(text)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function cleanTextList(values: string[] | undefined): string[] {
|
|
298
|
+
return (values ?? []).map(cleanOptionalText).filter((item): item is string => Boolean(item))
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function cleanOptionalText(value: string | undefined): string | undefined {
|
|
302
|
+
const text = String(value ?? "").trim()
|
|
303
|
+
return text || undefined
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const EVIDENCE_SENSITIVE_TERMS = [
|
|
307
|
+
/\bmarket size\b/,
|
|
308
|
+
/\bcagr\b/,
|
|
309
|
+
/\btam\b/,
|
|
310
|
+
/\bsam\b/,
|
|
311
|
+
/\bsom\b/,
|
|
312
|
+
/\brecommend(?:ation|ed)?\b/,
|
|
313
|
+
/\bshould\b/,
|
|
314
|
+
/\bmust\b/,
|
|
315
|
+
/\bgo\/?no-go\b/,
|
|
316
|
+
/\bvs\.?\b/,
|
|
317
|
+
/\bbetter than\b/,
|
|
318
|
+
/\boutperform\b/,
|
|
319
|
+
/\bleading\b/,
|
|
320
|
+
/\bcompetitor\b/,
|
|
321
|
+
/\bmarket leader\b/,
|
|
322
|
+
/\binvest(?:ment)?\b/,
|
|
323
|
+
/\brevenue\b/,
|
|
324
|
+
/\bmargin\b/,
|
|
325
|
+
/\bcost\b/,
|
|
326
|
+
/\brisk\b/,
|
|
327
|
+
/\blatency\b/,
|
|
328
|
+
/\baccuracy\b/,
|
|
329
|
+
/\bscalable\b/,
|
|
330
|
+
/\barchitecture\b/,
|
|
331
|
+
/市场规模/,
|
|
332
|
+
/增长/,
|
|
333
|
+
/领先/,
|
|
334
|
+
/超过/,
|
|
335
|
+
/竞品/,
|
|
336
|
+
/建议/,
|
|
337
|
+
/必须/,
|
|
338
|
+
/投资/,
|
|
339
|
+
/收入/,
|
|
340
|
+
/利润/,
|
|
341
|
+
/成本/,
|
|
342
|
+
/风险/,
|
|
343
|
+
/性能/,
|
|
344
|
+
/架构/,
|
|
345
|
+
/可扩展/,
|
|
346
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
InspectionAppendixCandidate,
|
|
3
|
+
InspectionClaimCandidate,
|
|
4
|
+
InspectionContext,
|
|
5
|
+
InspectionEvidenceTrace,
|
|
6
|
+
InspectionGap,
|
|
7
|
+
InspectionSlideContext,
|
|
8
|
+
} from "./compile"
|
|
9
|
+
|
|
10
|
+
export type InspectionMatchConfidence = "none" | "low" | "medium" | "high"
|
|
11
|
+
|
|
12
|
+
export interface InspectionElementSnapshot {
|
|
13
|
+
scope?: "element" | "selection" | "slide"
|
|
14
|
+
slideIndex?: number
|
|
15
|
+
text?: string
|
|
16
|
+
selectedText?: string
|
|
17
|
+
tagName?: string
|
|
18
|
+
slideTitle?: string
|
|
19
|
+
selector?: string
|
|
20
|
+
domPath?: string
|
|
21
|
+
id?: string
|
|
22
|
+
classList?: string[]
|
|
23
|
+
role?: string
|
|
24
|
+
outerHTMLExcerpt?: string
|
|
25
|
+
nearbyText?: string
|
|
26
|
+
elements?: Array<{
|
|
27
|
+
text?: string
|
|
28
|
+
tagName?: string
|
|
29
|
+
slideIndex?: number
|
|
30
|
+
slideTitle?: string
|
|
31
|
+
selector?: string
|
|
32
|
+
domPath?: string
|
|
33
|
+
id?: string
|
|
34
|
+
classList?: string[]
|
|
35
|
+
role?: string
|
|
36
|
+
outerHTMLExcerpt?: string
|
|
37
|
+
nearbyText?: string
|
|
38
|
+
boundingBox?: Record<string, unknown>
|
|
39
|
+
viewport?: Record<string, unknown>
|
|
40
|
+
}>
|
|
41
|
+
boundingBox?: Record<string, unknown>
|
|
42
|
+
viewport?: Record<string, unknown>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface InspectionElementMatch {
|
|
46
|
+
slide?: InspectionSlideContext
|
|
47
|
+
claim?: InspectionClaimCandidate
|
|
48
|
+
evidence: InspectionEvidenceTrace[]
|
|
49
|
+
gaps: InspectionGap[]
|
|
50
|
+
caveats: string[]
|
|
51
|
+
appendixCandidates: InspectionAppendixCandidate[]
|
|
52
|
+
confidence: InspectionMatchConfidence
|
|
53
|
+
reason: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function matchInspectionElement(context: InspectionContext, snapshot: InspectionElementSnapshot): InspectionElementMatch {
|
|
57
|
+
const selectedText = normalizeText(snapshot.text)
|
|
58
|
+
const candidateSlides = candidateSlidesForSnapshot(context, snapshot)
|
|
59
|
+
|
|
60
|
+
if (selectedText) {
|
|
61
|
+
const exactClaim = findClaimMatch(candidateSlides, selectedText, "exact")
|
|
62
|
+
if (exactClaim) return claimMatch(context, exactClaim.slide, exactClaim.claim, "high", "Exact normalized text match.")
|
|
63
|
+
|
|
64
|
+
const containsClaim = findClaimMatch(candidateSlides, selectedText, "contains")
|
|
65
|
+
if (containsClaim) return claimMatch(context, containsClaim.slide, containsClaim.claim, "medium", "Conservative normalized contains match.")
|
|
66
|
+
|
|
67
|
+
if (typeof snapshot.slideIndex === "number") {
|
|
68
|
+
const exactFallback = findClaimMatch(context.slides, selectedText, "exact")
|
|
69
|
+
if (exactFallback) {
|
|
70
|
+
return claimMatch(context, exactFallback.slide, exactFallback.claim, "high", "Exact normalized text match after slideIndex fallback.")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const containsFallback = findClaimMatch(context.slides, selectedText, "contains")
|
|
74
|
+
if (containsFallback) {
|
|
75
|
+
return claimMatch(context, containsFallback.slide, containsFallback.claim, "medium", "Conservative normalized contains match after slideIndex fallback.")
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const slide = candidateSlides[0]
|
|
81
|
+
if (slide) return slideMatch(context, slide, snapshot.slideIndex ? "medium" : "low", snapshot.slideIndex ? "Matched by slideIndex only." : "No claim text matched; returning first candidate slide.")
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
evidence: [],
|
|
85
|
+
gaps: [],
|
|
86
|
+
caveats: [],
|
|
87
|
+
appendixCandidates: [],
|
|
88
|
+
confidence: "none",
|
|
89
|
+
reason: "No slide or claim matched the selection snapshot.",
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function candidateSlidesForSnapshot(context: InspectionContext, snapshot: InspectionElementSnapshot): InspectionSlideContext[] {
|
|
94
|
+
if (typeof snapshot.slideIndex === "number") {
|
|
95
|
+
const slide = context.slides.find((item) => item.index === snapshot.slideIndex)
|
|
96
|
+
return slide ? [slide] : []
|
|
97
|
+
}
|
|
98
|
+
return context.slides
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findClaimMatch(
|
|
102
|
+
slides: InspectionSlideContext[],
|
|
103
|
+
selectedText: string,
|
|
104
|
+
mode: "exact" | "contains",
|
|
105
|
+
): { slide: InspectionSlideContext; claim: InspectionClaimCandidate } | undefined {
|
|
106
|
+
for (const slide of slides) {
|
|
107
|
+
for (const claim of slide.claims) {
|
|
108
|
+
const claimText = normalizeText(claim.text)
|
|
109
|
+
if (!claimText) continue
|
|
110
|
+
if (mode === "exact" && claimText === selectedText) return { slide, claim }
|
|
111
|
+
if (mode === "contains" && conservativeContains(claimText, selectedText)) return { slide, claim }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return undefined
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function conservativeContains(claimText: string, selectedText: string): boolean {
|
|
118
|
+
if (selectedText.length < 12 && claimText.length < 12) return false
|
|
119
|
+
return claimText.includes(selectedText) || selectedText.includes(claimText)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function claimMatch(
|
|
123
|
+
context: InspectionContext,
|
|
124
|
+
slide: InspectionSlideContext,
|
|
125
|
+
claim: InspectionClaimCandidate,
|
|
126
|
+
confidence: InspectionMatchConfidence,
|
|
127
|
+
reason: string,
|
|
128
|
+
): InspectionElementMatch {
|
|
129
|
+
return {
|
|
130
|
+
slide,
|
|
131
|
+
claim,
|
|
132
|
+
evidence: claim.evidence,
|
|
133
|
+
gaps: claim.gaps,
|
|
134
|
+
caveats: slide.caveats,
|
|
135
|
+
appendixCandidates: appendixCandidatesForSlide(context, slide.index),
|
|
136
|
+
confidence,
|
|
137
|
+
reason,
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function slideMatch(
|
|
142
|
+
context: InspectionContext,
|
|
143
|
+
slide: InspectionSlideContext,
|
|
144
|
+
confidence: InspectionMatchConfidence,
|
|
145
|
+
reason: string,
|
|
146
|
+
): InspectionElementMatch {
|
|
147
|
+
return {
|
|
148
|
+
slide,
|
|
149
|
+
evidence: slide.evidence,
|
|
150
|
+
gaps: slide.claims.flatMap((claim) => claim.gaps),
|
|
151
|
+
caveats: slide.caveats,
|
|
152
|
+
appendixCandidates: appendixCandidatesForSlide(context, slide.index),
|
|
153
|
+
confidence,
|
|
154
|
+
reason,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function appendixCandidatesForSlide(context: InspectionContext, slideIndex: number): InspectionAppendixCandidate[] {
|
|
159
|
+
return context.appendixCandidates.filter((candidate) => candidate.slideIndex === slideIndex)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeText(text: string | undefined): string {
|
|
163
|
+
return String(text ?? "")
|
|
164
|
+
.replace(/\s+/g, " ")
|
|
165
|
+
.replace(/[“”]/g, '"')
|
|
166
|
+
.replace(/[‘’]/g, "'")
|
|
167
|
+
.trim()
|
|
168
|
+
.toLowerCase()
|
|
169
|
+
}
|