@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.
- package/README.md +75 -55
- package/README.zh-CN.md +75 -55
- package/lib/command-intent.ts +59 -0
- package/lib/commands/brief.ts +1 -1
- package/lib/commands/designs.ts +1 -1
- package/lib/commands/domains.ts +1 -1
- package/lib/commands/edit.ts +7 -5
- package/lib/commands/enable.ts +6 -6
- package/lib/commands/help.ts +19 -10
- package/lib/commands/init.ts +1 -1
- package/lib/commands/inspect.ts +7 -5
- package/lib/commands/research.ts +66 -0
- package/lib/commands/review.ts +3 -3
- package/lib/decks-state.ts +5 -5
- package/lib/inspect/prompt.ts +15 -2
- package/lib/inspect/requests.ts +21 -2
- package/lib/inspection-context/compile.ts +71 -5
- package/lib/inspection-context/match.ts +71 -1
- package/lib/inspection-context/project.ts +116 -13
- package/lib/inspection-context/result.ts +183 -0
- package/lib/narrative-state/queries.ts +1 -0
- package/lib/refine/server.ts +91 -13
- package/package.json +1 -1
- package/plugin.ts +252 -53
- package/skill/NARRATIVE_SKILL.md +103 -25
- package/skill/SKILL.md +1 -1
- package/tools/edit.ts +10 -8
- package/tools/inspection-result.ts +37 -0
package/lib/commands/review.ts
CHANGED
|
@@ -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
|
|
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.
|
package/lib/decks-state.ts
CHANGED
|
@@ -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
|
}
|
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,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)
|
|
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),
|