@cyber-dash-tech/revela 0.12.0 → 0.13.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/lib/commands/brief.ts +63 -0
- package/lib/commands/help.ts +2 -0
- package/lib/commands/narrative.ts +160 -0
- package/lib/decks-state.ts +33 -0
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspection-context/compile.ts +159 -5
- package/lib/inspection-context/project.ts +20 -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 +433 -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/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 +31 -0
- package/tools/decks.ts +86 -1
- 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
|
|
@@ -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)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "crypto"
|
|
2
2
|
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, ResearchAxis, SlideSpec, SourceMaterial } from "../decks-state"
|
|
3
|
-
import type { NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
|
|
3
|
+
import type { NarrativeClaimRelation, NarrativeEvidenceBinding, NarrativeResearchGap, NarrativeStateV1 } from "../narrative-state/types"
|
|
4
4
|
import { renderTargetId } from "./render-targets"
|
|
5
5
|
import type { GraphEdge, GraphEdgeType, GraphNode, GraphNodeType, RenderTarget, WorkspaceGraph } from "./types"
|
|
6
6
|
|
|
@@ -178,6 +178,8 @@ function addCanonicalNarrative(builder: GraphBuilder, narrative: NarrativeStateV
|
|
|
178
178
|
}))
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
for (const relation of narrative.claimRelations ?? []) addClaimRelation(builder, relation)
|
|
182
|
+
|
|
181
183
|
for (const objection of narrative.objections) {
|
|
182
184
|
addNode(builder, {
|
|
183
185
|
id: objection.id,
|
|
@@ -200,6 +202,55 @@ function addCanonicalNarrative(builder: GraphBuilder, narrative: NarrativeStateV
|
|
|
200
202
|
addEdge(builder, "constrained_by", risk.claimId || narrative.id, risk.id)
|
|
201
203
|
}
|
|
202
204
|
|
|
205
|
+
for (const gap of narrative.researchGaps ?? []) addResearchGap(builder, narrative, gap)
|
|
206
|
+
|
|
207
|
+
return narrative.id
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function addClaimRelation(builder: GraphBuilder, relation: NarrativeClaimRelation): void {
|
|
211
|
+
addEdge(builder, graphEdgeTypeForClaimRelation(relation.relation), relation.fromClaimId, relation.toClaimId, compactData({
|
|
212
|
+
relationId: relation.id,
|
|
213
|
+
relation: relation.relation,
|
|
214
|
+
rationale: relation.rationale,
|
|
215
|
+
source: "canonicalNarrative",
|
|
216
|
+
}))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function graphEdgeTypeForClaimRelation(relation: NarrativeClaimRelation["relation"]): GraphEdgeType {
|
|
220
|
+
if (relation === "constrains") return "constrained_by"
|
|
221
|
+
if (relation === "supports") return "supports"
|
|
222
|
+
return relation
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function addResearchGap(builder: GraphBuilder, narrative: NarrativeStateV1, gap: NarrativeResearchGap): void {
|
|
226
|
+
addNode(builder, {
|
|
227
|
+
id: gap.id,
|
|
228
|
+
type: "researchGap",
|
|
229
|
+
label: gap.question,
|
|
230
|
+
data: compactData({
|
|
231
|
+
question: gap.question,
|
|
232
|
+
status: gap.status,
|
|
233
|
+
priority: gap.priority,
|
|
234
|
+
targetType: gap.targetType,
|
|
235
|
+
targetId: gap.targetId,
|
|
236
|
+
findingsFile: gap.findingsFile,
|
|
237
|
+
evidenceBindingIds: gap.evidenceBindingIds,
|
|
238
|
+
createdFromIssueType: gap.createdFromIssueType,
|
|
239
|
+
notes: gap.notes,
|
|
240
|
+
}),
|
|
241
|
+
})
|
|
242
|
+
addEdge(builder, "contains", narrative.id, gap.id)
|
|
243
|
+
addEdge(builder, "derived_from", gap.id, gapTargetNodeId(narrative, gap))
|
|
244
|
+
if (gap.findingsFile) addEdge(builder, "derived_from", gap.id, findingNodeId(gap.findingsFile), { status: gap.status })
|
|
245
|
+
for (const evidenceId of gap.evidenceBindingIds ?? []) {
|
|
246
|
+
const binding = narrative.evidenceBindings.find((item) => item.id === evidenceId)
|
|
247
|
+
if (binding) addEdge(builder, "derived_from", gap.id, binding.claimId, { evidenceBindingId: evidenceId })
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function gapTargetNodeId(narrative: NarrativeStateV1, gap: NarrativeResearchGap): string {
|
|
252
|
+
if (gap.targetId) return gap.targetId
|
|
253
|
+
if (gap.targetType === "narrative" || gap.targetType === "decision") return narrative.id
|
|
203
254
|
return narrative.id
|
|
204
255
|
}
|
|
205
256
|
|
|
@@ -352,9 +403,11 @@ function renderTargetsForDeck(state: DecksState, deck: DeckSpec): RenderTarget[]
|
|
|
352
403
|
const deckOutputPath = normalizePath(deck.outputPath)
|
|
353
404
|
const htmlTargetId = renderTargetId("html_deck", deckOutputPath)
|
|
354
405
|
const htmlArtifactId = artifactNodeId(deckOutputPath)
|
|
406
|
+
const narrativeId = state.narrative?.id
|
|
355
407
|
const targets = (state.renderTargets ?? []).filter((target) => {
|
|
356
408
|
if (target.id === htmlTargetId) return true
|
|
357
409
|
if (target.type === "html_deck") return normalizePath(target.outputPath ?? "") === deckOutputPath
|
|
410
|
+
if (narrativeId && target.sourceNodeIds.includes(narrativeId)) return true
|
|
358
411
|
const data = target.data ?? {}
|
|
359
412
|
return data.sourceTargetId === htmlTargetId ||
|
|
360
413
|
data.sourceOutputPath === deckOutputPath ||
|
|
@@ -422,7 +475,8 @@ function hasCanonicalNarrative(narrative: NarrativeStateV1 | undefined): narrati
|
|
|
422
475
|
narrative?.claims.length ||
|
|
423
476
|
narrative?.evidenceBindings.length ||
|
|
424
477
|
narrative?.objections.length ||
|
|
425
|
-
narrative?.risks.length
|
|
478
|
+
narrative?.risks.length ||
|
|
479
|
+
narrative?.researchGaps?.length,
|
|
426
480
|
)
|
|
427
481
|
}
|
|
428
482
|
|
|
@@ -50,6 +50,7 @@ export type GraphNodeType =
|
|
|
50
50
|
| "risk"
|
|
51
51
|
| "slide"
|
|
52
52
|
| "artifact"
|
|
53
|
+
| "researchGap"
|
|
53
54
|
|
|
54
55
|
export interface GraphEdge {
|
|
55
56
|
id: string
|
|
@@ -64,6 +65,10 @@ export type GraphEdgeType =
|
|
|
64
65
|
| "extracted_as"
|
|
65
66
|
| "produced"
|
|
66
67
|
| "supports"
|
|
68
|
+
| "leads_to"
|
|
69
|
+
| "depends_on"
|
|
70
|
+
| "contrasts_with"
|
|
71
|
+
| "answers"
|
|
67
72
|
| "appears_in"
|
|
68
73
|
| "challenges"
|
|
69
74
|
| "constrained_by"
|
|
@@ -88,8 +93,12 @@ export type WorkspaceActionType =
|
|
|
88
93
|
| "source.extracted"
|
|
89
94
|
| "research.findings_saved"
|
|
90
95
|
| "research.findings_attached"
|
|
96
|
+
| "research.gap_created"
|
|
97
|
+
| "research.gap_updated"
|
|
98
|
+
| "research.gap_closed"
|
|
91
99
|
| "narrative.upserted"
|
|
92
100
|
| "deck.plan_compiled"
|
|
101
|
+
| "artifact.coverage_backfilled"
|
|
93
102
|
| "evidence.candidate_generated"
|
|
94
103
|
| "evidence.binding_applied"
|
|
95
104
|
| "narrative.approved"
|
|
@@ -98,7 +107,7 @@ export type WorkspaceActionType =
|
|
|
98
107
|
|
|
99
108
|
export interface RenderTarget {
|
|
100
109
|
id: string
|
|
101
|
-
type: "html_deck" | "pdf" | "pptx" | "brief" | "appendix" | "qa_view" | "interactive_page"
|
|
110
|
+
type: "html_deck" | "pdf" | "pptx" | "brief" | "executive_brief" | "appendix" | "qa_view" | "interactive_page"
|
|
102
111
|
outputPath?: string
|
|
103
112
|
sourceNodeIds: string[]
|
|
104
113
|
artifactVersion?: string
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -59,6 +59,8 @@ import {
|
|
|
59
59
|
buildDesignsEditPrompt,
|
|
60
60
|
} from "./lib/commands/designs-new"
|
|
61
61
|
import { buildInitPrompt } from "./lib/commands/init"
|
|
62
|
+
import { handleBrief, parseBriefArgs } from "./lib/commands/brief"
|
|
63
|
+
import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs } from "./lib/commands/narrative"
|
|
62
64
|
import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
|
|
63
65
|
import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
|
|
64
66
|
import {
|
|
@@ -85,6 +87,7 @@ import researchImagesListTool from "./tools/research-images-list"
|
|
|
85
87
|
import researchSaveTool from "./tools/research-save"
|
|
86
88
|
import inspectionContextTool from "./tools/inspection-context"
|
|
87
89
|
import inspectionResultTool from "./tools/inspection-result"
|
|
90
|
+
import narrativeViewTool from "./tools/narrative-view"
|
|
88
91
|
import workspaceScanTool from "./tools/workspace-scan"
|
|
89
92
|
import extractDocumentMaterialsTool from "./tools/extract-document-materials"
|
|
90
93
|
import qaTool from "./tools/qa"
|
|
@@ -361,6 +364,33 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
361
364
|
} as any)
|
|
362
365
|
return
|
|
363
366
|
}
|
|
367
|
+
if (sub === "narrative") {
|
|
368
|
+
const parsed = parseNarrativeArgs(param)
|
|
369
|
+
if (!parsed.ok) {
|
|
370
|
+
await send(parsed.error)
|
|
371
|
+
throw new Error("__REVELA_NARRATIVE_USAGE_HANDLED__")
|
|
372
|
+
}
|
|
373
|
+
if (parsed.args.raw) {
|
|
374
|
+
await handleNarrative({ workspaceRoot, openBrowser: true, language: parsed.args.language }, send)
|
|
375
|
+
throw new Error("__REVELA_NARRATIVE_HANDLED__")
|
|
376
|
+
}
|
|
377
|
+
buildPrompt({ mode: "narrative" })
|
|
378
|
+
output.parts.length = 0
|
|
379
|
+
output.parts.push({
|
|
380
|
+
type: "text",
|
|
381
|
+
text: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
|
|
382
|
+
} as any)
|
|
383
|
+
return
|
|
384
|
+
}
|
|
385
|
+
if (sub === "brief") {
|
|
386
|
+
const parsed = parseBriefArgs(param)
|
|
387
|
+
if (!parsed.ok) {
|
|
388
|
+
await send(parsed.error)
|
|
389
|
+
throw new Error("__REVELA_BRIEF_USAGE_HANDLED__")
|
|
390
|
+
}
|
|
391
|
+
await handleBrief({ workspaceRoot, outputPath: parsed.args.outputPath }, send)
|
|
392
|
+
throw new Error("__REVELA_BRIEF_HANDLED__")
|
|
393
|
+
}
|
|
364
394
|
if (sub === "deck") {
|
|
365
395
|
if (param && param !== "--review") {
|
|
366
396
|
await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness.")
|
|
@@ -507,6 +537,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
507
537
|
"revela-research-save": researchSaveTool,
|
|
508
538
|
"revela-inspection-context": inspectionContextTool,
|
|
509
539
|
"revela-inspection-result": inspectionResultTool,
|
|
540
|
+
"revela-narrative-view": narrativeViewTool,
|
|
510
541
|
"revela-workspace-scan": workspaceScanTool,
|
|
511
542
|
"revela-extract-document-materials": extractDocumentMaterialsTool,
|
|
512
543
|
"revela-qa": qaTool,
|