@cyber-dash-tech/revela 0.9.0 → 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.
@@ -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
+ }