@cyber-dash-tech/revela 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/README.zh-CN.md +16 -16
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/edit.ts +7 -5
- package/lib/commands/help.ts +5 -3
- package/lib/commands/inspect.ts +7 -5
- package/lib/commands/narrative.ts +160 -0
- package/lib/decks-state.ts +33 -0
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspect/prompt.ts +15 -2
- package/lib/inspect/requests.ts +21 -2
- package/lib/inspection-context/compile.ts +230 -10
- package/lib/inspection-context/match.ts +71 -1
- package/lib/inspection-context/project.ts +131 -8
- package/lib/inspection-context/result.ts +183 -0
- package/lib/narrative-state/coverage.ts +100 -0
- package/lib/narrative-state/display.ts +219 -0
- package/lib/narrative-state/executive-brief.ts +246 -0
- package/lib/narrative-state/hash.ts +9 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +54 -0
- package/lib/narrative-state/queries.ts +434 -0
- package/lib/narrative-state/readiness.ts +71 -1
- package/lib/narrative-state/render-plan.ts +44 -1
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +33 -0
- package/lib/refine/server.ts +91 -13
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +56 -2
- package/lib/workspace-state/types.ts +10 -1
- package/package.json +1 -1
- package/plugin.ts +33 -2
- package/tools/decks.ts +86 -1
- package/tools/edit.ts +10 -8
- package/tools/inspection-result.ts +37 -0
- package/tools/narrative-view.ts +84 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec } from "../decks-state"
|
|
2
|
+
import { ensureActiveHtmlDeckRenderTarget } from "../workspace-state/render-targets"
|
|
3
|
+
import { getClaimSlideRefs } from "./queries"
|
|
2
4
|
import { computeNarrativeHash } from "./hash"
|
|
3
5
|
import { normalizeNarrativeState } from "./normalize"
|
|
4
6
|
import { narrativeToBrief } from "./project-compat"
|
|
@@ -64,6 +66,23 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
64
66
|
})
|
|
65
67
|
next = upsertSlides(next, slug, slides)
|
|
66
68
|
next.narrative = { ...narrative, updatedAt: options.now ?? narrative.updatedAt }
|
|
69
|
+
const htmlTarget = ensureActiveHtmlDeckRenderTarget(next)
|
|
70
|
+
if (htmlTarget) {
|
|
71
|
+
htmlTarget.data = {
|
|
72
|
+
...(htmlTarget.data ?? {}),
|
|
73
|
+
narrativeId: narrative.id,
|
|
74
|
+
narrativeHash,
|
|
75
|
+
claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
|
|
76
|
+
claimId: ref.claimId,
|
|
77
|
+
claimText: ref.claimText,
|
|
78
|
+
slideIndex: ref.slideIndex,
|
|
79
|
+
slideTitle: ref.slideTitle,
|
|
80
|
+
match: ref.match,
|
|
81
|
+
role: ref.role,
|
|
82
|
+
location: ref.location,
|
|
83
|
+
})),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
67
86
|
|
|
68
87
|
return {
|
|
69
88
|
state: next,
|
|
@@ -110,6 +129,7 @@ function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
|
110
129
|
|
|
111
130
|
for (const claim of centralClaims) slides.push(claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? []))
|
|
112
131
|
if (supportingClaims.length > 0) {
|
|
132
|
+
const supportingBindings = supportingClaims.flatMap((claim) => evidenceByClaim.get(claim.id) ?? [])
|
|
113
133
|
slides.push({
|
|
114
134
|
index: slides.length + 1,
|
|
115
135
|
title: "Supporting Logic",
|
|
@@ -118,16 +138,24 @@ function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
|
118
138
|
layout: "card-grid",
|
|
119
139
|
qa: true,
|
|
120
140
|
components: ["card"],
|
|
141
|
+
claimIds: supportingClaims.map((claim) => claim.id),
|
|
142
|
+
claimRefs: supportingClaims.map((claim) => ({ claimId: claim.id, role: "supporting" as const })),
|
|
143
|
+
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
121
144
|
content: {
|
|
122
145
|
headline: "Supporting claims and boundaries",
|
|
123
146
|
bullets: supportingClaims.slice(0, 5).map((claim) => claim.text),
|
|
124
147
|
},
|
|
125
|
-
evidence:
|
|
148
|
+
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
126
149
|
status: "planned",
|
|
127
150
|
})
|
|
128
151
|
}
|
|
129
152
|
|
|
130
153
|
if (narrative.risks.length > 0 || narrative.objections.length > 0) {
|
|
154
|
+
const challengedClaimRefs = [
|
|
155
|
+
...narrative.risks.map((risk) => risk.claimId ? { claimId: risk.claimId, role: "risk" as const } : undefined).filter((ref): ref is { claimId: string; role: "risk" } => Boolean(ref)),
|
|
156
|
+
...narrative.objections.map((objection) => objection.claimId ? { claimId: objection.claimId, role: "objection" as const } : undefined).filter((ref): ref is { claimId: string; role: "objection" } => Boolean(ref)),
|
|
157
|
+
]
|
|
158
|
+
const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
|
|
131
159
|
slides.push({
|
|
132
160
|
index: slides.length + 1,
|
|
133
161
|
title: "Risks And Objections",
|
|
@@ -136,6 +164,8 @@ function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
|
136
164
|
layout: "two-col",
|
|
137
165
|
qa: true,
|
|
138
166
|
components: ["card"],
|
|
167
|
+
claimIds: challengedClaimIds,
|
|
168
|
+
claimRefs: dedupeClaimRefs(challengedClaimRefs),
|
|
139
169
|
content: {
|
|
140
170
|
headline: "What could break the recommendation",
|
|
141
171
|
bullets: [
|
|
@@ -176,6 +206,9 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
|
|
|
176
206
|
layout: "two-col",
|
|
177
207
|
qa: true,
|
|
178
208
|
components: ["card"],
|
|
209
|
+
claimIds: [claim.id],
|
|
210
|
+
claimRefs: [{ claimId: claim.id, role: "primary" }],
|
|
211
|
+
evidenceBindingIds: bindings.map((binding) => binding.id),
|
|
179
212
|
content: {
|
|
180
213
|
headline: claim.text,
|
|
181
214
|
bullets: [claim.supportedScope, claim.unsupportedScope ? `Unsupported scope: ${claim.unsupportedScope}` : undefined, ...(claim.caveats ?? [])].filter((item): item is string => Boolean(item)),
|
|
@@ -185,6 +218,16 @@ function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvi
|
|
|
185
218
|
}
|
|
186
219
|
}
|
|
187
220
|
|
|
221
|
+
function dedupeClaimRefs<T extends { claimId: string; role: "risk" | "objection" }>(refs: T[]): T[] {
|
|
222
|
+
const seen = new Set<string>()
|
|
223
|
+
return refs.filter((ref) => {
|
|
224
|
+
const key = `${ref.claimId}:${ref.role}`
|
|
225
|
+
if (seen.has(key)) return false
|
|
226
|
+
seen.add(key)
|
|
227
|
+
return true
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
188
231
|
function evidenceRefFromBinding(binding: NarrativeEvidenceBinding): EvidenceRef {
|
|
189
232
|
return {
|
|
190
233
|
source: binding.source,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
3
|
+
import { stableResearchGapId } from "./hash"
|
|
4
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
5
|
+
import { reviewNarrativeState } from "./readiness"
|
|
6
|
+
import type {
|
|
7
|
+
NarrativeReadinessIssue,
|
|
8
|
+
NarrativeResearchGap,
|
|
9
|
+
NarrativeResearchGapStatus,
|
|
10
|
+
NarrativeResearchGapTargetType,
|
|
11
|
+
NarrativeStateV1,
|
|
12
|
+
} from "./types"
|
|
13
|
+
|
|
14
|
+
export interface UpsertResearchGapInput {
|
|
15
|
+
id?: string
|
|
16
|
+
targetType?: NarrativeResearchGapTargetType
|
|
17
|
+
targetId?: string
|
|
18
|
+
question: string
|
|
19
|
+
status?: NarrativeResearchGapStatus
|
|
20
|
+
priority?: "high" | "medium" | "low"
|
|
21
|
+
findingsFile?: string
|
|
22
|
+
evidenceBindingIds?: string[]
|
|
23
|
+
createdFromIssueType?: NarrativeReadinessIssue["type"]
|
|
24
|
+
notes?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UpdateResearchGapInput {
|
|
28
|
+
id: string
|
|
29
|
+
status?: NarrativeResearchGapStatus
|
|
30
|
+
findingsFile?: string
|
|
31
|
+
evidenceBindingIds?: string[]
|
|
32
|
+
notes?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResearchGapMutationResult {
|
|
36
|
+
created: NarrativeResearchGap[]
|
|
37
|
+
updated: NarrativeResearchGap[]
|
|
38
|
+
skipped: Array<{ id?: string; question?: string; reason: string }>
|
|
39
|
+
gaps: NarrativeResearchGap[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CloseResearchGapResult {
|
|
43
|
+
closed: boolean
|
|
44
|
+
skipped: boolean
|
|
45
|
+
reason?: string
|
|
46
|
+
gap?: NarrativeResearchGap
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function deriveResearchGapsFromReadiness(state: DecksState, options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
|
|
50
|
+
const reviewed = reviewNarrativeState(state, { now: options.now })
|
|
51
|
+
return upsertResearchGapsInState(reviewed.state, gapsFromIssues(reviewed.state.narrative!, reviewed.result.issues), options)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function upsertResearchGapsInState(state: DecksState, inputs: UpsertResearchGapInput[], options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
|
|
55
|
+
const now = options.now ?? new Date().toISOString()
|
|
56
|
+
const narrative = ensureNarrative(state)
|
|
57
|
+
const existing = new Map((narrative.researchGaps ?? []).map((gap) => [gap.id, gap]))
|
|
58
|
+
const created: NarrativeResearchGap[] = []
|
|
59
|
+
const updated: NarrativeResearchGap[] = []
|
|
60
|
+
const skipped: ResearchGapMutationResult["skipped"] = []
|
|
61
|
+
|
|
62
|
+
for (const input of inputs) {
|
|
63
|
+
const question = clean(input.question)
|
|
64
|
+
if (!question) {
|
|
65
|
+
skipped.push({ reason: "question is required" })
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
const targetType = input.targetType ?? "narrative"
|
|
69
|
+
const targetId = clean(input.targetId)
|
|
70
|
+
const id = input.id?.trim() || stableResearchGapId([targetType, targetId, question].filter(Boolean).join("|"))
|
|
71
|
+
const prior = existing.get(id)
|
|
72
|
+
if (prior?.status === "closed") {
|
|
73
|
+
skipped.push({ id, question, reason: "matching research gap is already closed" })
|
|
74
|
+
continue
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const next: NarrativeResearchGap = {
|
|
78
|
+
id,
|
|
79
|
+
targetType,
|
|
80
|
+
targetId,
|
|
81
|
+
question,
|
|
82
|
+
status: input.status ?? prior?.status ?? "open",
|
|
83
|
+
priority: input.priority ?? prior?.priority ?? "medium",
|
|
84
|
+
findingsFile: clean(input.findingsFile) || prior?.findingsFile,
|
|
85
|
+
evidenceBindingIds: mergeIds(prior?.evidenceBindingIds, input.evidenceBindingIds),
|
|
86
|
+
createdFromIssueType: input.createdFromIssueType ?? prior?.createdFromIssueType,
|
|
87
|
+
notes: clean(input.notes) || prior?.notes,
|
|
88
|
+
createdAt: prior?.createdAt ?? now,
|
|
89
|
+
updatedAt: now,
|
|
90
|
+
closedAt: input.status === "closed" ? now : prior?.closedAt,
|
|
91
|
+
}
|
|
92
|
+
existing.set(id, next)
|
|
93
|
+
if (prior) updated.push(next)
|
|
94
|
+
else created.push(next)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const gaps = [...existing.values()].sort((a, b) => gapSortValue(a) - gapSortValue(b) || a.question.localeCompare(b.question))
|
|
98
|
+
state.narrative = { ...narrative, researchGaps: gaps, updatedAt: now }
|
|
99
|
+
if (created.length > 0) recordResearchGapAction(state, "research.gap_created", created, now)
|
|
100
|
+
if (updated.length > 0) recordResearchGapAction(state, "research.gap_updated", updated, now)
|
|
101
|
+
return { state, result: { created, updated, skipped, gaps } }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function updateResearchGapInState(state: DecksState, input: UpdateResearchGapInput, options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
|
|
105
|
+
const narrative = ensureNarrative(state)
|
|
106
|
+
const gap = (narrative.researchGaps ?? []).find((item) => item.id === input.id)
|
|
107
|
+
if (!gap) return { state, result: { created: [], updated: [], skipped: [{ id: input.id, reason: "research gap not found" }], gaps: narrative.researchGaps ?? [] } }
|
|
108
|
+
return upsertResearchGapsInState(state, [{ ...gap, ...input, question: gap.question, targetType: gap.targetType, targetId: gap.targetId }], options)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function closeResearchGapInState(state: DecksState, id: string, reason?: string, options: { now?: string } = {}): { state: DecksState; result: CloseResearchGapResult } {
|
|
112
|
+
const now = options.now ?? new Date().toISOString()
|
|
113
|
+
const narrative = ensureNarrative(state)
|
|
114
|
+
const gaps = narrative.researchGaps ?? []
|
|
115
|
+
const gap = gaps.find((item) => item.id === id)
|
|
116
|
+
if (!gap) return { state, result: { closed: false, skipped: true, reason: "research gap not found" } }
|
|
117
|
+
const closed: NarrativeResearchGap = { ...gap, status: "closed", notes: clean(reason) || gap.notes, updatedAt: now, closedAt: now }
|
|
118
|
+
state.narrative = { ...narrative, researchGaps: gaps.map((item) => item.id === id ? closed : item), updatedAt: now }
|
|
119
|
+
recordResearchGapAction(state, "research.gap_closed", [closed], now)
|
|
120
|
+
return { state, result: { closed: true, skipped: false, gap: closed } }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function gapsFromIssues(narrative: NarrativeStateV1, issues: NarrativeReadinessIssue[]): UpsertResearchGapInput[] {
|
|
124
|
+
return issues.flatMap((issue) => {
|
|
125
|
+
if (!researchableIssue(issue)) return []
|
|
126
|
+
const target = targetForIssue(narrative, issue)
|
|
127
|
+
return [{
|
|
128
|
+
targetType: target.type,
|
|
129
|
+
targetId: target.id,
|
|
130
|
+
question: questionForIssue(issue),
|
|
131
|
+
priority: issue.severity === "blocker" ? "high" : "medium",
|
|
132
|
+
status: "open",
|
|
133
|
+
createdFromIssueType: issue.type,
|
|
134
|
+
notes: issue.suggestedAction,
|
|
135
|
+
findingsFile: issue.source?.startsWith("researches/") ? issue.source : undefined,
|
|
136
|
+
}]
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function researchableIssue(issue: NarrativeReadinessIssue): boolean {
|
|
141
|
+
return issue.type === "missing_evidence" || issue.type === "weak_evidence" || issue.type === "unsupported_scope" || issue.type === "unhandled_objection" || issue.type === "missing_risk"
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function targetForIssue(narrative: NarrativeStateV1, issue: NarrativeReadinessIssue): { type: NarrativeResearchGapTargetType; id?: string } {
|
|
145
|
+
if (issue.claimId) return { type: "claim", id: issue.claimId }
|
|
146
|
+
const objection = narrative.objections.find((item) => item.text === issue.claimText)
|
|
147
|
+
if (objection) return { type: "objection", id: objection.id }
|
|
148
|
+
if (issue.type === "missing_risk") return { type: "decision", id: narrative.decision.action ? stableResearchGapId(`decision:${narrative.decision.action}`) : undefined }
|
|
149
|
+
return { type: "narrative", id: narrative.id }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function questionForIssue(issue: NarrativeReadinessIssue): string {
|
|
153
|
+
if (issue.claimText && issue.type === "missing_evidence") return `Find evidence for claim: ${issue.claimText}`
|
|
154
|
+
if (issue.claimText && issue.type === "weak_evidence") return `Strengthen evidence for claim: ${issue.claimText}`
|
|
155
|
+
if (issue.claimText && issue.type === "unsupported_scope") return `Resolve unsupported scope for claim: ${issue.claimText}`
|
|
156
|
+
if (issue.type === "unhandled_objection") return `Find response or evidence for objection: ${issue.claimText ?? issue.message}`
|
|
157
|
+
if (issue.type === "missing_risk") return "Identify risk, assumption, caveat, or tradeoff handling for the recommendation."
|
|
158
|
+
return issue.message
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function ensureNarrative(state: DecksState): NarrativeStateV1 {
|
|
162
|
+
state.narrative = normalizeNarrativeState(state)
|
|
163
|
+
return state.narrative
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function recordResearchGapAction(state: DecksState, type: "research.gap_created" | "research.gap_updated" | "research.gap_closed", gaps: NarrativeResearchGap[], timestamp: string): void {
|
|
167
|
+
recordWorkspaceAction(state, {
|
|
168
|
+
type,
|
|
169
|
+
actor: "revela-decks",
|
|
170
|
+
timestamp,
|
|
171
|
+
inputs: { narrativeId: state.narrative?.id },
|
|
172
|
+
outputs: { gaps: gaps.map((gap) => ({ id: gap.id, status: gap.status, targetType: gap.targetType, targetId: gap.targetId, findingsFile: gap.findingsFile, evidenceBindingIds: gap.evidenceBindingIds })) },
|
|
173
|
+
status: "success",
|
|
174
|
+
summary: `${type === "research.gap_created" ? "Created" : type === "research.gap_closed" ? "Closed" : "Updated"} ${gaps.length} research gap${gaps.length === 1 ? "" : "s"}.`,
|
|
175
|
+
nodeIds: gaps.map((gap) => gap.id),
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function gapSortValue(gap: NarrativeResearchGap): number {
|
|
180
|
+
const statusValue: Record<NarrativeResearchGapStatus, number> = { open: 0, in_progress: 1, findings_saved: 2, attached: 3, evidence_bound: 4, closed: 5 }
|
|
181
|
+
const priorityValue = gap.priority === "high" ? 0 : gap.priority === "medium" ? 1 : 2
|
|
182
|
+
return statusValue[gap.status] * 10 + priorityValue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function mergeIds(existing: string[] | undefined, next: string[] | undefined): string[] {
|
|
186
|
+
return [...new Set([...(existing ?? []), ...(next ?? [])].map(clean).filter(Boolean))].sort()
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function clean(value: string | undefined): string {
|
|
190
|
+
return value?.trim() ?? ""
|
|
191
|
+
}
|
|
@@ -4,6 +4,12 @@ export type NarrativeClaimKind = "context" | "problem" | "opportunity" | "eviden
|
|
|
4
4
|
|
|
5
5
|
export type NarrativeEvidenceStatus = "supported" | "partial" | "weak" | "missing" | "not_required"
|
|
6
6
|
|
|
7
|
+
export type NarrativeResearchGapStatus = "open" | "in_progress" | "findings_saved" | "attached" | "evidence_bound" | "closed"
|
|
8
|
+
|
|
9
|
+
export type NarrativeResearchGapTargetType = "claim" | "objection" | "risk" | "decision" | "narrative"
|
|
10
|
+
|
|
11
|
+
export type NarrativeClaimRelationType = "leads_to" | "supports" | "depends_on" | "contrasts_with" | "constrains" | "answers"
|
|
12
|
+
|
|
7
13
|
export interface NarrativeStateV1 {
|
|
8
14
|
version: 1
|
|
9
15
|
id: string
|
|
@@ -12,9 +18,11 @@ export interface NarrativeStateV1 {
|
|
|
12
18
|
decision: DecisionIntent
|
|
13
19
|
thesis?: NarrativeThesis
|
|
14
20
|
claims: NarrativeClaim[]
|
|
21
|
+
claimRelations?: NarrativeClaimRelation[]
|
|
15
22
|
evidenceBindings: NarrativeEvidenceBinding[]
|
|
16
23
|
objections: NarrativeObjection[]
|
|
17
24
|
risks: NarrativeRisk[]
|
|
25
|
+
researchGaps?: NarrativeResearchGap[]
|
|
18
26
|
approvals: NarrativeApproval[]
|
|
19
27
|
updatedAt: string
|
|
20
28
|
}
|
|
@@ -55,6 +63,14 @@ export interface NarrativeClaim {
|
|
|
55
63
|
caveats?: string[]
|
|
56
64
|
}
|
|
57
65
|
|
|
66
|
+
export interface NarrativeClaimRelation {
|
|
67
|
+
id: string
|
|
68
|
+
fromClaimId: string
|
|
69
|
+
toClaimId: string
|
|
70
|
+
relation: NarrativeClaimRelationType
|
|
71
|
+
rationale?: string
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
export interface NarrativeEvidenceBinding {
|
|
59
75
|
id: string
|
|
60
76
|
claimId: string
|
|
@@ -86,6 +102,22 @@ export interface NarrativeRisk {
|
|
|
86
102
|
mitigation?: string
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
export interface NarrativeResearchGap {
|
|
106
|
+
id: string
|
|
107
|
+
targetType: NarrativeResearchGapTargetType
|
|
108
|
+
targetId?: string
|
|
109
|
+
question: string
|
|
110
|
+
status: NarrativeResearchGapStatus
|
|
111
|
+
priority: "high" | "medium" | "low"
|
|
112
|
+
findingsFile?: string
|
|
113
|
+
evidenceBindingIds?: string[]
|
|
114
|
+
createdFromIssueType?: NarrativeReadinessIssueType
|
|
115
|
+
notes?: string
|
|
116
|
+
createdAt: string
|
|
117
|
+
updatedAt: string
|
|
118
|
+
closedAt?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
89
121
|
export interface NarrativeApproval {
|
|
90
122
|
id: string
|
|
91
123
|
narrativeHash: string
|
|
@@ -112,6 +144,7 @@ export type NarrativeReadinessIssueType =
|
|
|
112
144
|
| "approval_stale"
|
|
113
145
|
| "artifact_stale"
|
|
114
146
|
| "research_findings_unattached"
|
|
147
|
+
| "research_gap_open"
|
|
115
148
|
|
|
116
149
|
export interface NarrativeReadinessIssue {
|
|
117
150
|
type: NarrativeReadinessIssueType
|
package/lib/refine/server.ts
CHANGED
|
@@ -459,6 +459,7 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
const snapshot = normalizeSnapshot(body?.snapshot ?? body)
|
|
462
|
+
const language = normalizeInspectLanguage(body?.language)
|
|
462
463
|
const requestId = typeof body?.requestId === "string" && body.requestId.trim() ? body.requestId.trim() : randomBytes(10).toString("base64url")
|
|
463
464
|
const version = readDeckVersion(session).version
|
|
464
465
|
const staleReason = typeof body?.deckVersion === "string" && body.deckVersion !== version
|
|
@@ -480,6 +481,7 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
480
481
|
text: buildInspectionPrompt({
|
|
481
482
|
requestId,
|
|
482
483
|
file: session.file,
|
|
484
|
+
language,
|
|
483
485
|
projection: staleReason
|
|
484
486
|
? { ...projection, stale: { stale: true, reason: staleReason } } as any
|
|
485
487
|
: projection,
|
|
@@ -491,7 +493,7 @@ async function handleInspect(req: Request, session: EditSession): Promise<Respon
|
|
|
491
493
|
failInspectRequest(requestId, message)
|
|
492
494
|
})
|
|
493
495
|
|
|
494
|
-
return jsonResponse({ ok: true, requestId, deckVersion: version, status: "pending", preprocess })
|
|
496
|
+
return jsonResponse({ ok: true, requestId, deckVersion: version, status: "pending", language, preprocess })
|
|
495
497
|
} catch (error) {
|
|
496
498
|
const message = error instanceof Error ? error.message : String(error)
|
|
497
499
|
failInspectRequest(requestId, message)
|
|
@@ -510,6 +512,11 @@ function handleInspectResult(requestId: string | null, session: EditSession): Re
|
|
|
510
512
|
return jsonResponse({ ok: true, requestId, status: request.status, deckVersion: request.deckVersion })
|
|
511
513
|
}
|
|
512
514
|
|
|
515
|
+
function normalizeInspectLanguage(input: unknown): string {
|
|
516
|
+
const value = typeof input === "string" ? input.trim() : ""
|
|
517
|
+
return value || "Auto"
|
|
518
|
+
}
|
|
519
|
+
|
|
513
520
|
function normalizeSnapshot(input: any): InspectionElementSnapshot {
|
|
514
521
|
return {
|
|
515
522
|
scope: input?.scope === "selection" || input?.scope === "slide" || input?.scope === "element" ? input.scope : undefined,
|
|
@@ -657,6 +664,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
657
664
|
.comment-bubble.stale .comment-bubble-state { color: #a16207; }
|
|
658
665
|
.comment-bubble.failed .comment-bubble-state { color: #b91c1c; }
|
|
659
666
|
.inspect-actions { display: flex; flex-direction: column; gap: 8px; }
|
|
667
|
+
.inspect-options { display: flex; flex-direction: column; gap: 5px; }
|
|
668
|
+
.inspect-options label { color: #64748b; font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: .05em; }
|
|
669
|
+
.inspect-select { width: 100%; padding: 10px 11px; border: 1px solid #d7e0ea; border-radius: 12px; background: #fff; color: #0f172a; font-weight: 700; }
|
|
660
670
|
.inspect-cards { display: flex; flex-direction: column; gap: 12px; }
|
|
661
671
|
.inspect-card { border: 1px solid #d7e0ea; border-radius: 16px; background: #fff; padding: 13px; box-shadow: 0 10px 24px rgba(15,23,42,.05); }
|
|
662
672
|
.inspect-card-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 8px; }
|
|
@@ -681,7 +691,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
681
691
|
<aside>
|
|
682
692
|
<div>
|
|
683
693
|
<h1><span class="wordmark">REVELA</span> Refine</h1>
|
|
684
|
-
<p class="hint">Cmd/Ctrl-click slide elements once, then use Edit for fast changes or Inspect for Source
|
|
694
|
+
<p class="hint">Cmd/Ctrl-click slide elements once, then use Edit for fast changes or Inspect for Narrative Reading, Exploratory Reading, Source, and Purpose review.</p>
|
|
685
695
|
</div>
|
|
686
696
|
<div id="selectionSummary" class="selection-summary"><strong>Selection</strong><span>No references selected.</span><div id="selectionChips" class="selection-chips"></div></div>
|
|
687
697
|
<div class="tabs" role="tablist" aria-label="Refine mode">
|
|
@@ -698,10 +708,11 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
698
708
|
</div>
|
|
699
709
|
<div id="inspectPanel" class="tab-panel">
|
|
700
710
|
<div class="inspect-actions">
|
|
711
|
+
<div class="inspect-options"><label for="inspectLanguage">Display Language</label><select id="inspectLanguage" class="inspect-select"><option>Auto</option><option>English</option><option>简体中文</option><option>繁體中文</option><option>日本語</option><option>Deutsch</option><option>Français</option><option>Español</option><option>Português</option><option>Arabic</option></select></div>
|
|
701
712
|
<button id="inspectButton" disabled>Inspect Selection</button>
|
|
702
713
|
<div id="inspectStale"></div>
|
|
703
714
|
</div>
|
|
704
|
-
<div id="inspectCards" class="inspect-cards"><div class="inspect-empty">Select one or more deck elements, then inspect them for Source and Purpose. This does not edit the deck.</div></div>
|
|
715
|
+
<div id="inspectCards" class="inspect-cards"><div class="inspect-empty">Select one or more deck elements, then inspect them for Narrative Reading, Exploratory Reading, Source, and Purpose. This does not edit the deck.</div></div>
|
|
705
716
|
</div>
|
|
706
717
|
<div id="status" class="status"></div>
|
|
707
718
|
</aside>
|
|
@@ -744,6 +755,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
744
755
|
mode: defaultMode === 'inspect' ? 'inspect' : 'edit',
|
|
745
756
|
inspecting: false,
|
|
746
757
|
activeInspectRequestId: '',
|
|
758
|
+
inspectLanguage: 'Auto',
|
|
759
|
+
inspectFallback: null,
|
|
747
760
|
};
|
|
748
761
|
const els = {
|
|
749
762
|
frame: null,
|
|
@@ -759,6 +772,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
759
772
|
commentThread: null,
|
|
760
773
|
send: null,
|
|
761
774
|
inspectButton: null,
|
|
775
|
+
inspectLanguage: null,
|
|
762
776
|
inspectCards: null,
|
|
763
777
|
inspectStale: null,
|
|
764
778
|
status: null,
|
|
@@ -792,7 +806,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
792
806
|
els.inspectStale = document.getElementById('inspectStale');
|
|
793
807
|
els.status = document.getElementById('status');
|
|
794
808
|
|
|
795
|
-
|
|
809
|
+
els.inspectLanguage = document.getElementById('inspectLanguage');
|
|
810
|
+
|
|
811
|
+
if (!els.frame || !els.hitbox || !els.resizeHandle || !els.selectionSummary || !els.selectionChips || !els.editTab || !els.inspectTab || !els.editPanel || !els.inspectPanel || !els.comment || !els.commentThread || !els.send || !els.inspectButton || !els.inspectLanguage || !els.inspectCards || !els.inspectStale || !els.status) {
|
|
796
812
|
throw new Error('Editor boot failed: required DOM nodes are missing.');
|
|
797
813
|
}
|
|
798
814
|
|
|
@@ -840,6 +856,9 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
840
856
|
els.resizeHandle.addEventListener('dblclick', resetEditorWidth);
|
|
841
857
|
els.send.addEventListener('click', sendComment);
|
|
842
858
|
els.inspectButton.addEventListener('click', inspectCurrentSelection);
|
|
859
|
+
els.inspectLanguage.addEventListener('change', () => {
|
|
860
|
+
state.inspectLanguage = els.inspectLanguage.value || 'Auto';
|
|
861
|
+
});
|
|
843
862
|
els.editTab.addEventListener('click', () => setMode('edit'));
|
|
844
863
|
els.inspectTab.addEventListener('click', () => setMode('inspect'));
|
|
845
864
|
}
|
|
@@ -1075,7 +1094,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1075
1094
|
renderReferenceOutlines();
|
|
1076
1095
|
updateSendState();
|
|
1077
1096
|
renderSelectionSummary();
|
|
1078
|
-
resetInspectCards('References ready. Open Inspect and click Inspect Selection when you want Source
|
|
1097
|
+
resetInspectCards('References ready. Open Inspect and click Inspect Selection when you want Narrative Reading, Exploratory Reading, Source, and Purpose review.');
|
|
1079
1098
|
setStatus('Inserted @' + label + '. ' + state.references.length + ' reference' + (state.references.length === 1 ? '' : 's') + ' will be sent.');
|
|
1080
1099
|
}
|
|
1081
1100
|
|
|
@@ -1286,22 +1305,28 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1286
1305
|
updateSendState();
|
|
1287
1306
|
setMode('inspect');
|
|
1288
1307
|
els.inspectStale.innerHTML = '';
|
|
1289
|
-
|
|
1308
|
+
state.inspectFallback = null;
|
|
1309
|
+
els.inspectCards.innerHTML = '<div class="inspect-loading"><b>Reading selection...</b><br>Sending grounded selection context to OpenCode. Deterministic context is kept as fallback if generation fails.</div>';
|
|
1290
1310
|
try {
|
|
1291
1311
|
const res = await fetch('/api/inspect?token=' + encodeURIComponent(token), {
|
|
1292
1312
|
method: 'POST',
|
|
1293
1313
|
headers: { 'content-type': 'application/json' },
|
|
1294
|
-
body: JSON.stringify({ snapshot, deckVersion: state.deckVersion }),
|
|
1314
|
+
body: JSON.stringify({ snapshot, deckVersion: state.deckVersion, language: state.inspectLanguage }),
|
|
1295
1315
|
});
|
|
1296
1316
|
const body = await res.json().catch(() => ({}));
|
|
1297
1317
|
if (!res.ok || !body.ok) throw new Error(body.error || 'Inspection failed');
|
|
1298
1318
|
state.deckVersion = body.deckVersion || state.deckVersion;
|
|
1299
1319
|
state.activeInspectRequestId = body.requestId;
|
|
1300
|
-
|
|
1301
|
-
els.inspectCards.
|
|
1320
|
+
state.inspectFallback = body.preprocess || null;
|
|
1321
|
+
els.inspectCards.innerHTML = '<div class="inspect-loading"><b>Reading selection...</b><br>Waiting for localized structured reading cards.</div>';
|
|
1302
1322
|
await pollInspectResult(body.requestId);
|
|
1303
1323
|
} catch (error) {
|
|
1304
|
-
|
|
1324
|
+
if (state.inspectFallback) {
|
|
1325
|
+
renderInspectResult(state.inspectFallback, 'Deterministic fallback');
|
|
1326
|
+
els.inspectCards.insertAdjacentHTML('afterbegin', '<div class="inspect-warning">Generated inspection failed or timed out. Showing deterministic fallback context only.</div>');
|
|
1327
|
+
} else {
|
|
1328
|
+
resetInspectCards(error && error.message ? error.message : String(error));
|
|
1329
|
+
}
|
|
1305
1330
|
} finally {
|
|
1306
1331
|
state.inspecting = false;
|
|
1307
1332
|
updateSendState();
|
|
@@ -1309,7 +1334,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1309
1334
|
}
|
|
1310
1335
|
|
|
1311
1336
|
async function pollInspectResult(requestId) {
|
|
1312
|
-
for (
|
|
1337
|
+
for (let attempt = 0; attempt < 80; attempt++) {
|
|
1313
1338
|
await delay(900);
|
|
1314
1339
|
const res = await fetch('/api/inspect-result?token=' + encodeURIComponent(token) + '&requestId=' + encodeURIComponent(requestId));
|
|
1315
1340
|
const body = await res.json().catch(() => ({}));
|
|
@@ -1321,6 +1346,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1321
1346
|
}
|
|
1322
1347
|
if (body.status === 'failed' || body.status === 'expired') throw new Error(body.error || 'Inspection failed');
|
|
1323
1348
|
}
|
|
1349
|
+
throw new Error('Inspection timed out while waiting for OpenCode result');
|
|
1324
1350
|
}
|
|
1325
1351
|
|
|
1326
1352
|
function collectReferenceSnapshot() {
|
|
@@ -1351,6 +1377,8 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1351
1377
|
else els.inspectStale.innerHTML = '';
|
|
1352
1378
|
els.inspectCards.innerHTML = [
|
|
1353
1379
|
'<div class="status">' + escapeHtml(phase || 'Inspection') + '</div>',
|
|
1380
|
+
result.cards.reading ? renderInspectCard('Narrative Reading', result.cards.reading.status, result.cards.reading.rationale, renderReading(result.cards.reading)) : '',
|
|
1381
|
+
result.cards.exploratory ? renderInspectCard('Exploratory Reading', result.cards.exploratory.status, result.cards.exploratory.rationale, renderExploratory(result.cards.exploratory)) : '',
|
|
1354
1382
|
renderInspectCard('Purpose', result.cards.purpose.status, result.cards.purpose.rationale, renderPurpose(result.cards.purpose)),
|
|
1355
1383
|
renderInspectCard('Source', result.cards.source.status, result.cards.source.rationale, renderSource(result.cards.source)),
|
|
1356
1384
|
].join('');
|
|
@@ -1364,6 +1392,49 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1364
1392
|
return '<div class="inspect-item">' + field('Role', card.role) + field('Why it matters', card.whyItMatters) + '</div>';
|
|
1365
1393
|
}
|
|
1366
1394
|
|
|
1395
|
+
function renderReading(card) {
|
|
1396
|
+
return '<div class="inspect-item">'
|
|
1397
|
+
+ field('Claim ID', card.claimId)
|
|
1398
|
+
+ field('Canonical claim ID', card.canonicalClaimId)
|
|
1399
|
+
+ field('Claim', card.claimText)
|
|
1400
|
+
+ field('Evidence status', card.evidenceStatus)
|
|
1401
|
+
+ field('Evidence bindings', card.evidenceBindingIds && card.evidenceBindingIds.length ? card.evidenceBindingIds.join(', ') : '')
|
|
1402
|
+
+ field('Supported scope', card.supportedScope)
|
|
1403
|
+
+ field('Unsupported scope', card.unsupportedScope)
|
|
1404
|
+
+ '</div>'
|
|
1405
|
+
+ renderSectionList('Caveats', card.caveats)
|
|
1406
|
+
+ renderSectionList('Objections', card.relatedObjections)
|
|
1407
|
+
+ renderSectionList('Risks', card.relatedRisks)
|
|
1408
|
+
+ renderArtifactCoverage(card.artifactCoverage);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function renderArtifactCoverage(items) {
|
|
1412
|
+
if (!items || !items.length) return '';
|
|
1413
|
+
return '<div class="label">Artifact Coverage</div>' + items.map((item) => {
|
|
1414
|
+
const title = (item.type || 'artifact') + (item.outputPath ? ' · ' + item.outputPath : '');
|
|
1415
|
+
const status = (item.coverageStatus || 'unknown') + (item.containsClaim ? ' · contains claim' : ' · claim not rendered');
|
|
1416
|
+
return '<div class="inspect-item"><b>' + escapeHtml(title) + '</b>'
|
|
1417
|
+
+ field('Coverage', status)
|
|
1418
|
+
+ field('Stale', item.stale ? (item.staleReason || 'stale') : '')
|
|
1419
|
+
+ field('Note', item.note)
|
|
1420
|
+
+ renderSectionList('Locations', item.locations)
|
|
1421
|
+
+ '</div>';
|
|
1422
|
+
}).join('');
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function renderExploratory(card) {
|
|
1426
|
+
return '<div class="inspect-item"><b>Non-official reading aid</b>'
|
|
1427
|
+
+ field('Official artifact content', card.official === false ? 'No' : '')
|
|
1428
|
+
+ field('Audience', card.audience)
|
|
1429
|
+
+ field('Claim focus', card.claimFocus)
|
|
1430
|
+
+ field('Audience reframe boundary', card.audienceReframe)
|
|
1431
|
+
+ '</div>'
|
|
1432
|
+
+ renderSectionList('Objection Prep', card.objectionPrompts)
|
|
1433
|
+
+ renderSectionList('Appendix Leads', card.appendixLeads)
|
|
1434
|
+
+ renderSectionList('Meeting Prep', card.meetingPrep)
|
|
1435
|
+
+ renderSectionList('Boundaries', card.boundaries);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1367
1438
|
function renderSource(card) {
|
|
1368
1439
|
return renderSources(card.sources) + renderWarnings(card.warnings) + renderSectionList('Gaps', card.gaps) + renderSectionList('Caveats', card.caveats);
|
|
1369
1440
|
}
|
|
@@ -1422,7 +1493,7 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1422
1493
|
state.references = [];
|
|
1423
1494
|
if (removeChips) els.comment.querySelectorAll('.ref-chip').forEach((chip) => chip.remove());
|
|
1424
1495
|
renderSelectionSummary();
|
|
1425
|
-
resetInspectCards('Select one or more deck elements, then inspect them for Source and Purpose. This does not edit the deck.');
|
|
1496
|
+
resetInspectCards('Select one or more deck elements, then inspect them for Narrative Reading, Exploratory Reading, Source, and Purpose. This does not edit the deck.');
|
|
1426
1497
|
}
|
|
1427
1498
|
|
|
1428
1499
|
function getCommentText() {
|
|
@@ -1525,10 +1596,17 @@ export function renderRefineShell(token: string, defaultMode: RefineMode = "edit
|
|
|
1525
1596
|
}
|
|
1526
1597
|
|
|
1527
1598
|
function findSlide(node) {
|
|
1528
|
-
|
|
1599
|
+
if (!node || !node.closest) return null;
|
|
1600
|
+
return node.closest('.slide[data-slide-index]')
|
|
1601
|
+
|| node.closest('.slide')
|
|
1602
|
+
|| node.closest('[slide-qa]')
|
|
1603
|
+
|| node.closest('.slide-canvas')
|
|
1604
|
+
|| node.closest('.page');
|
|
1529
1605
|
}
|
|
1530
1606
|
|
|
1531
1607
|
function getSlides(doc) {
|
|
1608
|
+
const canonicalSlides = Array.from(doc.querySelectorAll('.slide[data-slide-index]'));
|
|
1609
|
+
if (canonicalSlides.length) return canonicalSlides;
|
|
1532
1610
|
const slides = Array.from(doc.querySelectorAll('.slide'));
|
|
1533
1611
|
if (slides.length) return slides;
|
|
1534
1612
|
const qaSlides = Array.from(doc.querySelectorAll('[slide-qa]'));
|
|
@@ -40,15 +40,25 @@ export interface EvidenceStatusMatch {
|
|
|
40
40
|
slideIndex?: number
|
|
41
41
|
slideTitle?: string
|
|
42
42
|
claimId?: string
|
|
43
|
+
canonicalClaimId?: string
|
|
43
44
|
claimText?: string
|
|
44
45
|
claimEvidenceSensitive?: boolean
|
|
45
46
|
claimEvidenceSupport?: string
|
|
47
|
+
evidenceBindingIds: string[]
|
|
48
|
+
supportedScope?: string
|
|
49
|
+
unsupportedScope?: string
|
|
50
|
+
caveats: string[]
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
export interface EvidenceStatusEvidence extends EvidenceRef {
|
|
49
54
|
slideIndex: number
|
|
50
55
|
slideTitle: string
|
|
51
56
|
hasDetail: boolean
|
|
57
|
+
evidenceBindingId?: string
|
|
58
|
+
claimId?: string
|
|
59
|
+
supportScope?: string
|
|
60
|
+
unsupportedScope?: string
|
|
61
|
+
strength?: "strong" | "partial" | "weak"
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export interface EvidenceStatusGap {
|
|
@@ -159,9 +169,14 @@ function projectMatch(match: InspectionElementMatch): EvidenceStatusMatch {
|
|
|
159
169
|
slideIndex: match.slide?.index,
|
|
160
170
|
slideTitle: match.slide?.title,
|
|
161
171
|
claimId: match.claim?.id,
|
|
172
|
+
canonicalClaimId: match.claim?.canonicalClaimId,
|
|
162
173
|
claimText: match.claim?.text,
|
|
163
174
|
claimEvidenceSensitive: match.claim?.evidenceSensitive,
|
|
164
175
|
claimEvidenceSupport: match.claim?.evidenceSupport,
|
|
176
|
+
evidenceBindingIds: match.claim?.evidenceBindingIds ?? [],
|
|
177
|
+
supportedScope: match.claim?.supportedScope,
|
|
178
|
+
unsupportedScope: match.claim?.unsupportedScope,
|
|
179
|
+
caveats: match.claim?.caveats ?? [],
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
|
|
@@ -235,7 +250,12 @@ function relevantSearchDiagnostics(issues: ReadinessIssue[], match: InspectionEl
|
|
|
235
250
|
|
|
236
251
|
function actionTraceForMatch(actions: WorkspaceAction[], match: InspectionElementMatch): EvidenceStatusActionTrace[] {
|
|
237
252
|
const slideNodeId = match.slide ? `slide:${match.slide.index}` : undefined
|
|
238
|
-
const evidenceKeys = new Set(
|
|
253
|
+
const evidenceKeys = new Set([
|
|
254
|
+
match.claim?.id,
|
|
255
|
+
match.claim?.canonicalClaimId,
|
|
256
|
+
...(match.claim?.evidenceBindingIds ?? []),
|
|
257
|
+
...match.evidence.flatMap((item) => [item.source, item.sourcePath, item.findingsFile, item.evidenceBindingId, item.claimId]),
|
|
258
|
+
].filter((value): value is string => Boolean(value)))
|
|
239
259
|
return actions
|
|
240
260
|
.filter((action) => actionRelevantToMatch(action, slideNodeId, evidenceKeys))
|
|
241
261
|
.slice(-12)
|