@cyber-dash-tech/revela 0.13.0 → 0.15.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.
@@ -18,7 +18,7 @@ Goal:
18
18
  - Treat this as a narrative readiness review, not a deck HTML write-readiness review.
19
19
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
20
20
  - Call \`revela-decks\` action \`reviewNarrative\` as the authoritative deterministic readiness engine.
21
- - Do not call \`revela-decks\` action \`review\` here. That action is the deck/artifact gate and belongs to \`/revela deck --review\`.
21
+ - Do not call \`revela-decks\` action \`review\` here. That action is the deck/artifact gate and belongs to \`/revela make deck --review\`.
22
22
  - Do not treat legacy \`writeReadiness.status\`, old review snapshots, or an existing HTML deck as narrative approval.
23
23
  - Do not write or overwrite \`decks/*.html\` during narrative review.
24
24
  - If the narrative is \`ready_for_approval\`, ask whether the user wants to approve it or revise it. Do not approve automatically.
@@ -50,7 +50,7 @@ Report format:
50
50
  - If warnings exist, list them after blockers as residual risks.
51
51
  - If approval is missing, ask whether the user wants to approve the narrative or revise it.
52
52
  - If approval is stale, say the prior approval no longer matches the current narrative hash.
53
- - Keep deck/artifact readiness separate. If the user wants to review slide-writing readiness, tell them to run \`/revela deck --review\`.
53
+ - Keep deck/artifact readiness separate. If the user wants to review slide-writing readiness, tell them to run \`/revela make deck --review\`.
54
54
 
55
55
  Rules:
56
56
  - Do not write or overwrite \`decks/*.html\` during narrative review.
@@ -128,7 +128,7 @@ export function buildDeckReviewPrompt({
128
128
 
129
129
  Goal:
130
130
  - Use ${DECKS_STATE_FILE} as the source of truth for whether the current workspace deck is ready to be written to \`decks/*.html\`.
131
- - Treat this as an artifact gate for deck rendering, not strategic narrative approval. Narrative readiness is reviewed by \`/revela review\`.
131
+ - Treat this as an artifact gate for deck rendering, not strategic narrative approval. Narrative readiness reports are reviewed by \`/revela review\`.
132
132
  - Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, production status, and the 0.9 narrative compiler brief when available.
133
133
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
134
134
  - Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
@@ -564,7 +564,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
564
564
  type: "missing_slide_spec",
565
565
  severity: "blocker",
566
566
  message,
567
- suggestedAction: "Run /revela review and resolve all readiness blockers before writing deck HTML.",
567
+ suggestedAction: "Run /revela make deck --review and resolve all readiness blockers before writing deck HTML.",
568
568
  })
569
569
  }
570
570
  if (deck.writeReadiness.blockers.length > 0) {
@@ -574,7 +574,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
574
574
  type: "missing_slide_spec",
575
575
  severity: "blocker",
576
576
  message,
577
- suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
577
+ suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela make deck --review.",
578
578
  })
579
579
  }
580
580
  if (normalized.reviews.length > 0) {
@@ -587,7 +587,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
587
587
  type: "missing_slide_spec",
588
588
  severity: "blocker",
589
589
  message,
590
- suggestedAction: "Run /revela review so readiness is recorded against the current active render target.",
590
+ suggestedAction: "Run /revela make deck --review so readiness is recorded against the current active render target.",
591
591
  })
592
592
  } else if (!isReviewSnapshotCurrent(normalized, snapshot, deck.slug)) {
593
593
  const message = "Latest review snapshot is stale for the current deck, sources, evidence, narrative state, or render target"
@@ -596,7 +596,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
596
596
  type: "missing_slide_spec",
597
597
  severity: "blocker",
598
598
  message,
599
- suggestedAction: "Run /revela review again after the latest state changes before writing deck HTML.",
599
+ suggestedAction: "Run /revela make deck --review again after the latest state changes before writing deck HTML.",
600
600
  })
601
601
  } else if (snapshot.status !== "ready") {
602
602
  const message = `Latest review snapshot is ${snapshot.status}, not ready`
@@ -605,7 +605,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
605
605
  type: "missing_slide_spec",
606
606
  severity: "blocker",
607
607
  message,
608
- suggestedAction: "Resolve review blockers and rerun /revela review before writing deck HTML.",
608
+ suggestedAction: "Resolve review blockers and rerun /revela make deck --review before writing deck HTML.",
609
609
  })
610
610
  }
611
611
  }
@@ -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: answer the selected object's purpose and source credibility only. Do not edit files. Do not mutate DECKS.json. Do not invent sources, quotes, URLs, page references, caveats, or evidence not present in the projection.
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
+ }
@@ -1,5 +1,5 @@
1
1
  import type { InspectionPromptProjection } from "../inspection-context/project"
2
- import type { InspectionResult } from "../inspection-context/result"
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 = { ...result, requestId }
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,4 +1,5 @@
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"
2
3
  import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
3
4
 
4
5
  export type InspectionClaimOrigin = "narrative" | "title" | "headline" | "body" | "bullet" | "purpose"
@@ -20,6 +21,7 @@ export interface InspectionContext {
20
21
  appendixCandidates: InspectionAppendixCandidate[]
21
22
  objectionContext: InspectionNarrativeContext[]
22
23
  riskContext: InspectionNarrativeContext[]
24
+ artifactCoverage: InspectionArtifactCoverage[]
23
25
  }
24
26
 
25
27
  export interface InspectionNarrativeStateContext {
@@ -98,11 +100,35 @@ export interface InspectionAppendixCandidate {
98
100
 
99
101
  export interface InspectionNarrativeContext {
100
102
  text: string
101
- source: "narrativeBrief" | "slide"
103
+ source: "narrative" | "narrativeBrief" | "slide"
102
104
  slideIndex?: number
103
105
  slideTitle?: string
104
106
  }
105
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
+
106
132
  export function compileInspectionContext(state: DecksState, slug?: string): InspectionContext {
107
133
  const deck = activeDeck(state, slug)
108
134
  const narrative = state.narrative
@@ -127,11 +153,36 @@ export function compileInspectionContext(state: DecksState, slug?: string): Insp
127
153
  slides,
128
154
  gaps,
129
155
  appendixCandidates: compileAppendixCandidates(slides),
130
- objectionContext: compileNarrativeList(deck, "objections"),
131
- riskContext: compileNarrativeList(deck, "risks"),
156
+ objectionContext: compileNarrativeList(deck, "objections", narrative),
157
+ riskContext: compileNarrativeList(deck, "risks", narrative),
158
+ artifactCoverage: compileArtifactCoverage(state),
132
159
  }
133
160
  }
134
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
+
135
186
  function activeDeck(state: DecksState, slug?: string): DeckSpec {
136
187
  const key = slug || state.activeDeck || (Object.keys(state.decks).length === 1 ? Object.keys(state.decks)[0] : undefined)
137
188
  if (!key || !state.decks[key]) throw new Error("No active deck is available for inspection context compilation.")
@@ -389,7 +440,10 @@ function appendixReason(slide: InspectionSlideContext): string {
389
440
  return "Slide has recorded evidence that may be useful for source excerpts or backup detail."
390
441
  }
391
442
 
392
- 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 }))
393
447
  const fromBrief = (deck.narrativeBrief?.[key] ?? []).map((text) => ({ text, source: "narrativeBrief" as const }))
394
448
  const role = key === "risks" ? "risk" : undefined
395
449
  const fromSlides = deck.slides
@@ -400,7 +454,19 @@ function compileNarrativeList(deck: DeckSpec, key: "objections" | "risks"): Insp
400
454
  slideIndex: slide.index,
401
455
  slideTitle: slide.title,
402
456
  })))
403
- 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
404
470
  }
405
471
 
406
472
  function slideTextList(slide: SlideSpec): string[] {
@@ -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) return slideMatch(context, slide, snapshot.slideIndex ? "medium" : "low", snapshot.slideIndex ? "Matched by slideIndex only." : "No claim text matched; returning first candidate 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
+ }
@@ -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[]
@@ -60,6 +65,20 @@ export interface InspectionProjectionMatch {
60
65
  unsupportedScope?: string
61
66
  caveats: string[]
62
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[]
63
82
  }
64
83
 
65
84
  export interface InspectionSourceProjection {
@@ -100,6 +119,32 @@ export interface InspectionAppendixProjection {
100
119
  relatedObjections: string[]
101
120
  }
102
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
+
103
148
  export interface InspectionEvidenceProjectionTrace {
104
149
  source: string
105
150
  evidenceBindingId?: string
@@ -132,6 +177,7 @@ export function projectInspectionMatch(
132
177
  ): InspectionPromptProjection {
133
178
  const slide = match.slide
134
179
  const claim = match.claim
180
+ const candidateClaims = dedupeClaims(match.candidateClaims ?? [])
135
181
  const traces = match.evidence.map(projectEvidenceTrace)
136
182
  const gaps = match.gaps.map(projectGap)
137
183
  const narrativeBrief = context.narrativeBrief
@@ -149,11 +195,15 @@ export function projectInspectionMatch(
149
195
  slideIndex: snapshot.slideIndex,
150
196
  scope: snapshot.scope,
151
197
  text: truncateOptional(snapshot.selectedText || snapshot.text, 700),
198
+ nearbyText: truncateOptional(snapshot.nearbyText, 900),
199
+ outerHTMLExcerpt: truncateOptional(snapshot.outerHTMLExcerpt, 900),
152
200
  elements: snapshot.elements?.slice(0, 12).map((item) => ({
153
201
  text: truncateOptional(item.text, 320),
154
202
  tagName: truncateOptional(item.tagName, 40),
155
203
  classList: (item.classList ?? []).slice(0, 8).map((className) => truncate(className, 80)),
156
204
  role: truncateOptional(item.role, 80),
205
+ nearbyText: truncateOptional(item.nearbyText, 420),
206
+ outerHTMLExcerpt: truncateOptional(item.outerHTMLExcerpt, 420),
157
207
  })),
158
208
  tagName: truncateOptional(snapshot.tagName, 40),
159
209
  classList: (snapshot.classList ?? []).slice(0, 12).map((item) => truncate(item, 80)),
@@ -171,19 +221,9 @@ export function projectInspectionMatch(
171
221
  }
172
222
  : undefined,
173
223
  claim: claim
174
- ? {
175
- id: claim.id,
176
- canonicalClaimId: claim.canonicalClaimId,
177
- origin: claim.origin,
178
- text: truncate(claim.text, 500),
179
- evidenceSensitive: claim.evidenceSensitive,
180
- evidenceSupport: claim.evidenceSupport,
181
- evidenceBindingIds: claim.evidenceBindingIds,
182
- supportedScope: truncateOptional(claim.supportedScope, 280),
183
- unsupportedScope: truncateOptional(claim.unsupportedScope, 280),
184
- caveats: claim.caveats.map((item) => truncate(item, 280)).slice(0, 8),
185
- }
224
+ ? projectClaimCandidate(claim)
186
225
  : undefined,
226
+ candidateClaims: candidateClaims.map(projectClaimCandidate).slice(0, 8),
187
227
  },
188
228
  cards: {
189
229
  source: {
@@ -219,10 +259,73 @@ export function projectInspectionMatch(
219
259
  relatedRisks: relatedNarrativeText(context.riskContext, slide?.index),
220
260
  relatedObjections: relatedNarrativeText(context.objectionContext, slide?.index),
221
261
  },
262
+ artifacts: projectArtifactCoverage(context, claim?.canonicalClaimId ?? claim?.id),
222
263
  },
223
264
  }
224
265
  }
225
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
+
226
329
  function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenceProjectionTrace {
227
330
  return {
228
331
  source: truncate(trace.source, 180),