@cyber-dash-tech/revela 0.15.4 → 0.16.2
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/narrative.ts +9 -3
- package/lib/commands/research.ts +25 -18
- package/lib/commands/review.ts +2 -1
- package/lib/decks-state.ts +142 -4
- package/lib/narrative-state/display.ts +40 -0
- package/lib/narrative-state/map-html.ts +133 -6
- package/lib/narrative-state/map.ts +333 -16
- package/lib/narrative-state/render-plan.ts +254 -84
- package/lib/narrative-state/research-gaps.ts +332 -0
- package/package.json +1 -1
- package/tools/decks.ts +7 -2
- package/tools/narrative-view.ts +11 -1
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs"
|
|
2
|
+
import { resolve, sep } from "path"
|
|
1
3
|
import type { DecksState } from "../decks-state"
|
|
2
4
|
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
3
5
|
import { stableResearchGapId } from "./hash"
|
|
4
6
|
import { normalizeNarrativeState } from "./normalize"
|
|
5
7
|
import { reviewNarrativeState } from "./readiness"
|
|
6
8
|
import type {
|
|
9
|
+
NarrativeClaim,
|
|
7
10
|
NarrativeReadinessIssue,
|
|
8
11
|
NarrativeResearchGap,
|
|
9
12
|
NarrativeResearchGapStatus,
|
|
@@ -11,6 +14,62 @@ import type {
|
|
|
11
14
|
NarrativeStateV1,
|
|
12
15
|
} from "./types"
|
|
13
16
|
|
|
17
|
+
export type ResearchTargetKind =
|
|
18
|
+
| "research_gap"
|
|
19
|
+
| "missing_evidence"
|
|
20
|
+
| "weak_evidence"
|
|
21
|
+
| "unsupported_scope"
|
|
22
|
+
| "unhandled_objection"
|
|
23
|
+
| "high_severity_risk"
|
|
24
|
+
| "unattached_findings"
|
|
25
|
+
| "claim_chain_gap"
|
|
26
|
+
|
|
27
|
+
export type EvidenceBindingFailureReason =
|
|
28
|
+
| "missing_quote"
|
|
29
|
+
| "unclear_source"
|
|
30
|
+
| "over_broad_claim"
|
|
31
|
+
| "weak_source"
|
|
32
|
+
| "unsupported_scope"
|
|
33
|
+
| "caveat_conflict"
|
|
34
|
+
| "source_mismatch"
|
|
35
|
+
| "context_only_finding"
|
|
36
|
+
|
|
37
|
+
export interface ResearchTarget {
|
|
38
|
+
id: string
|
|
39
|
+
kind: ResearchTargetKind
|
|
40
|
+
targetType: NarrativeResearchGapTargetType | "findings" | "relation"
|
|
41
|
+
targetId?: string
|
|
42
|
+
priority: "high" | "medium" | "low"
|
|
43
|
+
reason: string
|
|
44
|
+
question: string
|
|
45
|
+
status?: NarrativeResearchGapStatus | "unattached"
|
|
46
|
+
findingsFile?: string
|
|
47
|
+
claimId?: string
|
|
48
|
+
claimText?: string
|
|
49
|
+
requiredEvidence: string[]
|
|
50
|
+
bindingFailureReasons?: EvidenceBindingFailureReason[]
|
|
51
|
+
bindingDiagnostic?: EvidenceBindingDiagnostic
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResearchTargetsResult {
|
|
55
|
+
targets: ResearchTarget[]
|
|
56
|
+
selected?: ResearchTarget
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EvidenceBindingDiagnostic {
|
|
60
|
+
findingsFile: string
|
|
61
|
+
bindable: boolean
|
|
62
|
+
failureReasons: EvidenceBindingFailureReason[]
|
|
63
|
+
explicit: {
|
|
64
|
+
source: boolean
|
|
65
|
+
quoteOrSnippet: boolean
|
|
66
|
+
supportScope: boolean
|
|
67
|
+
unsupportedScope: boolean
|
|
68
|
+
caveat: boolean
|
|
69
|
+
strength: boolean
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
14
73
|
export interface UpsertResearchGapInput {
|
|
15
74
|
id?: string
|
|
16
75
|
targetType?: NarrativeResearchGapTargetType
|
|
@@ -46,6 +105,20 @@ export interface CloseResearchGapResult {
|
|
|
46
105
|
gap?: NarrativeResearchGap
|
|
47
106
|
}
|
|
48
107
|
|
|
108
|
+
export function deriveResearchTargets(state: DecksState, options: { now?: string; workspaceRoot?: string } = {}): ResearchTargetsResult {
|
|
109
|
+
const reviewed = reviewNarrativeState(state, { now: options.now })
|
|
110
|
+
const narrative = reviewed.state.narrative!
|
|
111
|
+
const targets = dedupeTargets([
|
|
112
|
+
...targetsFromResearchGaps(narrative, options.workspaceRoot),
|
|
113
|
+
...targetsFromClaims(narrative),
|
|
114
|
+
...targetsFromObjections(narrative),
|
|
115
|
+
...targetsFromRisks(narrative),
|
|
116
|
+
...targetsFromReadinessIssues(reviewed.result.issues),
|
|
117
|
+
...targetsFromUnattachedFindings(reviewed.state, narrative, options.workspaceRoot),
|
|
118
|
+
]).sort(compareResearchTargets)
|
|
119
|
+
return { targets, selected: targets[0] }
|
|
120
|
+
}
|
|
121
|
+
|
|
49
122
|
export function deriveResearchGapsFromReadiness(state: DecksState, options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
|
|
50
123
|
const reviewed = reviewNarrativeState(state, { now: options.now })
|
|
51
124
|
return upsertResearchGapsInState(reviewed.state, gapsFromIssues(reviewed.state.narrative!, reviewed.result.issues), options)
|
|
@@ -137,6 +210,265 @@ function gapsFromIssues(narrative: NarrativeStateV1, issues: NarrativeReadinessI
|
|
|
137
210
|
})
|
|
138
211
|
}
|
|
139
212
|
|
|
213
|
+
function targetsFromResearchGaps(narrative: NarrativeStateV1, workspaceRoot: string | undefined): ResearchTarget[] {
|
|
214
|
+
return (narrative.researchGaps ?? [])
|
|
215
|
+
.filter((gap) => gap.status !== "closed" && gap.status !== "evidence_bound")
|
|
216
|
+
.map((gap) => {
|
|
217
|
+
const claim = gap.targetType === "claim" ? narrative.claims.find((item) => item.id === gap.targetId) : undefined
|
|
218
|
+
const bindingDiagnostic = gap.findingsFile ? evidenceBindingDiagnostic(workspaceRoot, gap.findingsFile) : undefined
|
|
219
|
+
return {
|
|
220
|
+
id: `gap:${gap.id}`,
|
|
221
|
+
kind: "research_gap",
|
|
222
|
+
targetType: gap.targetType,
|
|
223
|
+
targetId: gap.targetId,
|
|
224
|
+
priority: gap.priority,
|
|
225
|
+
reason: gap.status === "findings_saved" || gap.status === "attached"
|
|
226
|
+
? "Saved findings exist; inspect and bind explicit evidence before launching new research."
|
|
227
|
+
: "Open canonical research gap needs findings or binding progress.",
|
|
228
|
+
question: gap.question,
|
|
229
|
+
status: gap.status,
|
|
230
|
+
findingsFile: gap.findingsFile,
|
|
231
|
+
claimId: claim?.id,
|
|
232
|
+
claimText: claim?.text,
|
|
233
|
+
requiredEvidence: requiredEvidenceForClaim(claim),
|
|
234
|
+
bindingFailureReasons: bindingDiagnostic?.failureReasons ?? (gap.findingsFile ? ["missing_quote", "unclear_source", "unsupported_scope"] : undefined),
|
|
235
|
+
bindingDiagnostic,
|
|
236
|
+
} satisfies ResearchTarget
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function targetsFromClaims(narrative: NarrativeStateV1): ResearchTarget[] {
|
|
241
|
+
return narrative.claims.flatMap((claim) => {
|
|
242
|
+
const targets: ResearchTarget[] = []
|
|
243
|
+
if (claim.evidenceRequired && claim.evidenceStatus === "missing") {
|
|
244
|
+
targets.push(claimTarget("missing_evidence", claim, claim.importance === "central" ? "high" : "medium", "Evidence-required claim has no bound support."))
|
|
245
|
+
}
|
|
246
|
+
if (claim.evidenceRequired && (claim.evidenceStatus === "weak" || claim.evidenceStatus === "partial")) {
|
|
247
|
+
targets.push(claimTarget("weak_evidence", claim, claim.importance === "central" ? "high" : "medium", `Claim evidence is ${claim.evidenceStatus}; strengthen source trace or narrow scope.`))
|
|
248
|
+
}
|
|
249
|
+
if (claim.unsupportedScope) {
|
|
250
|
+
targets.push(claimTarget("unsupported_scope", claim, claim.importance === "central" ? "high" : "medium", "Claim records unsupported scope that needs evidence, narrowing, or explicit caveat."))
|
|
251
|
+
}
|
|
252
|
+
return targets
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function targetsFromObjections(narrative: NarrativeStateV1): ResearchTarget[] {
|
|
257
|
+
return narrative.objections
|
|
258
|
+
.filter((objection) => objection.priority === "high" && !objection.response)
|
|
259
|
+
.map((objection) => ({
|
|
260
|
+
id: `objection:${objection.id}`,
|
|
261
|
+
kind: "unhandled_objection",
|
|
262
|
+
targetType: "objection",
|
|
263
|
+
targetId: objection.id,
|
|
264
|
+
priority: "high",
|
|
265
|
+
reason: "High-priority objection has no recorded response or evidence boundary.",
|
|
266
|
+
question: `Find response or evidence for objection: ${objection.text}`,
|
|
267
|
+
claimId: objection.claimId,
|
|
268
|
+
claimText: narrative.claims.find((claim) => claim.id === objection.claimId)?.text,
|
|
269
|
+
requiredEvidence: ["response evidence or boundary", "source", "quote/snippet", "caveat"],
|
|
270
|
+
} satisfies ResearchTarget))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function targetsFromRisks(narrative: NarrativeStateV1): ResearchTarget[] {
|
|
274
|
+
return narrative.risks
|
|
275
|
+
.filter((risk) => risk.severity === "high" && !risk.mitigation)
|
|
276
|
+
.map((risk) => ({
|
|
277
|
+
id: `risk:${risk.id}`,
|
|
278
|
+
kind: "high_severity_risk",
|
|
279
|
+
targetType: "risk",
|
|
280
|
+
targetId: risk.id,
|
|
281
|
+
priority: "high",
|
|
282
|
+
reason: "High-severity risk has no mitigation or evidence boundary.",
|
|
283
|
+
question: `Find mitigation, evidence boundary, or caveat for risk: ${risk.text}`,
|
|
284
|
+
claimId: risk.claimId,
|
|
285
|
+
claimText: narrative.claims.find((claim) => claim.id === risk.claimId)?.text,
|
|
286
|
+
requiredEvidence: ["mitigation evidence or boundary", "source", "quote/snippet", "caveat"],
|
|
287
|
+
} satisfies ResearchTarget))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function targetsFromReadinessIssues(issues: NarrativeReadinessIssue[]): ResearchTarget[] {
|
|
291
|
+
return issues.flatMap((issue) => {
|
|
292
|
+
if (issue.type !== "claim_chain_gap") return []
|
|
293
|
+
return [{
|
|
294
|
+
id: `issue:${issue.type}:${stableResearchGapId(issue.message)}`,
|
|
295
|
+
kind: "claim_chain_gap",
|
|
296
|
+
targetType: "relation",
|
|
297
|
+
priority: issue.severity === "blocker" ? "high" : "medium",
|
|
298
|
+
reason: issue.message,
|
|
299
|
+
question: issue.suggestedAction,
|
|
300
|
+
requiredEvidence: ["claim relation rationale", "supporting source or explicit user rationale", "caveat if relation is assumption-based"],
|
|
301
|
+
} satisfies ResearchTarget]
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function targetsFromUnattachedFindings(state: DecksState, narrative: NarrativeStateV1, workspaceRoot: string | undefined): ResearchTarget[] {
|
|
306
|
+
return (state.actions ?? []).flatMap((action) => {
|
|
307
|
+
if (action.type !== "research.findings_saved") return []
|
|
308
|
+
const path = typeof action.outputs?.path === "string" ? action.outputs.path : undefined
|
|
309
|
+
if (!path || isFindingsAttachedOrBound(state, narrative, path)) return []
|
|
310
|
+
const bindingDiagnostic = evidenceBindingDiagnostic(workspaceRoot, path)
|
|
311
|
+
return [{
|
|
312
|
+
id: `findings:${path}`,
|
|
313
|
+
kind: "unattached_findings",
|
|
314
|
+
targetType: "findings",
|
|
315
|
+
targetId: path,
|
|
316
|
+
priority: "medium",
|
|
317
|
+
reason: "Saved findings are not attached to a research axis or bound as canonical evidence.",
|
|
318
|
+
question: `Inspect and attach or bind saved findings: ${path}`,
|
|
319
|
+
status: "unattached",
|
|
320
|
+
findingsFile: path,
|
|
321
|
+
requiredEvidence: ["source", "quote/snippet", "support scope", "unsupported scope", "caveat", "strength"],
|
|
322
|
+
bindingFailureReasons: bindingDiagnostic?.failureReasons ?? ["missing_quote", "unclear_source", "context_only_finding", "unsupported_scope"],
|
|
323
|
+
bindingDiagnostic,
|
|
324
|
+
} satisfies ResearchTarget]
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function evidenceBindingDiagnostic(workspaceRoot: string | undefined, findingsFile: string): EvidenceBindingDiagnostic | undefined {
|
|
329
|
+
const text = readWorkspaceText(workspaceRoot, findingsFile)
|
|
330
|
+
if (text === undefined) return undefined
|
|
331
|
+
|
|
332
|
+
const explicit = {
|
|
333
|
+
source: hasSourceTrace(text),
|
|
334
|
+
quoteOrSnippet: hasQuoteOrSnippet(text),
|
|
335
|
+
supportScope: hasField(text, ["support scope", "supported scope", "supports", "support"]),
|
|
336
|
+
unsupportedScope: hasField(text, ["unsupported scope", "unsupported", "not supported", "gaps"]),
|
|
337
|
+
caveat: hasField(text, ["caveat", "limitation", "limits", "boundary"]),
|
|
338
|
+
strength: hasField(text, ["strength", "support strength", "evidence strength"]),
|
|
339
|
+
}
|
|
340
|
+
const failureReasons: EvidenceBindingFailureReason[] = []
|
|
341
|
+
if (!explicit.quoteOrSnippet) failureReasons.push("missing_quote")
|
|
342
|
+
if (!explicit.source) failureReasons.push("unclear_source")
|
|
343
|
+
if (!explicit.supportScope || !explicit.unsupportedScope) failureReasons.push("unsupported_scope")
|
|
344
|
+
if (!explicit.caveat) failureReasons.push("caveat_conflict")
|
|
345
|
+
if (!explicit.strength) failureReasons.push("weak_source")
|
|
346
|
+
if (looksContextOnly(text, explicit)) failureReasons.push("context_only_finding")
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
findingsFile,
|
|
350
|
+
bindable: failureReasons.length === 0,
|
|
351
|
+
failureReasons,
|
|
352
|
+
explicit,
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function readWorkspaceText(workspaceRoot: string | undefined, relativePath: string): string | undefined {
|
|
357
|
+
if (!workspaceRoot) return undefined
|
|
358
|
+
const root = resolve(workspaceRoot)
|
|
359
|
+
const target = resolve(root, relativePath)
|
|
360
|
+
if (target !== root && !target.startsWith(root + sep)) return undefined
|
|
361
|
+
if (!existsSync(target)) return undefined
|
|
362
|
+
return readFileSync(target, "utf-8")
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hasSourceTrace(text: string): boolean {
|
|
366
|
+
return /^sources:\s*$/im.test(text)
|
|
367
|
+
|| /\[source:\s*[^\]]+\]/i.test(text)
|
|
368
|
+
|| /^\s*-?\s*source:\s*\S+/im.test(text)
|
|
369
|
+
|| /^\s*source\s+(path|url):\s*\S+/im.test(text)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function hasQuoteOrSnippet(text: string): boolean {
|
|
373
|
+
return hasField(text, ["quote", "snippet"])
|
|
374
|
+
|| /["“][^"”]{20,}["”]/.test(text)
|
|
375
|
+
|| /^>\s*\S.{20,}/m.test(text)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function hasField(text: string, labels: string[]): boolean {
|
|
379
|
+
return labels.some((label) => new RegExp(`(^|\\n)\\s*(?:[-*]\\s*)?${escapeRegex(label)}\\s*[::]\\s*\\S`, "i").test(text)
|
|
380
|
+
|| new RegExp(`^##+\\s+.*${escapeRegex(label)}`, "im").test(text))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function looksContextOnly(text: string, explicit: EvidenceBindingDiagnostic["explicit"]): boolean {
|
|
384
|
+
return /^##+\s+data\b/im.test(text) && !explicit.quoteOrSnippet && (!explicit.supportScope || !explicit.caveat)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function escapeRegex(value: string): string {
|
|
388
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function claimTarget(kind: Extract<ResearchTargetKind, "missing_evidence" | "weak_evidence" | "unsupported_scope">, claim: NarrativeClaim, priority: "high" | "medium", reason: string): ResearchTarget {
|
|
392
|
+
return {
|
|
393
|
+
id: `claim:${kind}:${claim.id}`,
|
|
394
|
+
kind,
|
|
395
|
+
targetType: "claim",
|
|
396
|
+
targetId: claim.id,
|
|
397
|
+
priority,
|
|
398
|
+
reason,
|
|
399
|
+
question: questionForClaimTarget(kind, claim),
|
|
400
|
+
claimId: claim.id,
|
|
401
|
+
claimText: claim.text,
|
|
402
|
+
requiredEvidence: requiredEvidenceForClaim(claim),
|
|
403
|
+
bindingFailureReasons: bindingFailuresForClaim(claim),
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function questionForClaimTarget(kind: ResearchTargetKind, claim: NarrativeClaim): string {
|
|
408
|
+
if (kind === "missing_evidence") return `Find evidence for claim: ${claim.text}`
|
|
409
|
+
if (kind === "weak_evidence") return `Strengthen evidence for claim: ${claim.text}`
|
|
410
|
+
if (kind === "unsupported_scope") return `Resolve unsupported scope for claim: ${claim.text}`
|
|
411
|
+
return claim.text
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function requiredEvidenceForClaim(claim: NarrativeClaim | undefined): string[] {
|
|
415
|
+
const base = ["source", "quote/snippet", "support scope", "unsupported scope", "caveat", "strength"]
|
|
416
|
+
if (!claim) return base
|
|
417
|
+
if (claim.unsupportedScope) return [...base, `address unsupported scope: ${claim.unsupportedScope}`]
|
|
418
|
+
return base
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function bindingFailuresForClaim(claim: NarrativeClaim): EvidenceBindingFailureReason[] {
|
|
422
|
+
const reasons: EvidenceBindingFailureReason[] = ["missing_quote", "unclear_source"]
|
|
423
|
+
if (claim.evidenceStatus === "weak") reasons.push("weak_source")
|
|
424
|
+
if (claim.unsupportedScope) reasons.push("unsupported_scope", "over_broad_claim")
|
|
425
|
+
return reasons
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function isFindingsAttachedOrBound(state: DecksState, narrative: NarrativeStateV1, path: string): boolean {
|
|
429
|
+
const attached = Object.values(state.decks ?? {}).some((deck) => deck.researchPlan.some((axis) => axis.findingsFile === path))
|
|
430
|
+
const bound = narrative.evidenceBindings.some((binding) => binding.findingsFile === path)
|
|
431
|
+
return attached || bound
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function dedupeTargets(targets: ResearchTarget[]): ResearchTarget[] {
|
|
435
|
+
const seen = new Set<string>()
|
|
436
|
+
const result: ResearchTarget[] = []
|
|
437
|
+
for (const target of targets) {
|
|
438
|
+
const key = `${target.kind}:${target.targetType}:${target.targetId ?? target.claimId ?? target.question}`
|
|
439
|
+
if (seen.has(key)) continue
|
|
440
|
+
seen.add(key)
|
|
441
|
+
result.push(target)
|
|
442
|
+
}
|
|
443
|
+
return result
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function compareResearchTargets(a: ResearchTarget, b: ResearchTarget): number {
|
|
447
|
+
return priorityValue(a.priority) - priorityValue(b.priority)
|
|
448
|
+
|| kindValue(a.kind) - kindValue(b.kind)
|
|
449
|
+
|| a.question.localeCompare(b.question)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function priorityValue(priority: ResearchTarget["priority"]): number {
|
|
453
|
+
if (priority === "high") return 0
|
|
454
|
+
if (priority === "medium") return 1
|
|
455
|
+
return 2
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function kindValue(kind: ResearchTargetKind): number {
|
|
459
|
+
const values: Record<ResearchTargetKind, number> = {
|
|
460
|
+
research_gap: 0,
|
|
461
|
+
unattached_findings: 1,
|
|
462
|
+
missing_evidence: 2,
|
|
463
|
+
weak_evidence: 3,
|
|
464
|
+
unsupported_scope: 4,
|
|
465
|
+
unhandled_objection: 5,
|
|
466
|
+
high_severity_risk: 6,
|
|
467
|
+
claim_chain_gap: 7,
|
|
468
|
+
}
|
|
469
|
+
return values[kind]
|
|
470
|
+
}
|
|
471
|
+
|
|
140
472
|
function researchableIssue(issue: NarrativeReadinessIssue): boolean {
|
|
141
473
|
return issue.type === "missing_evidence" || issue.type === "weak_evidence" || issue.type === "unsupported_scope" || issue.type === "unhandled_objection" || issue.type === "missing_risk"
|
|
142
474
|
}
|
package/package.json
CHANGED
package/tools/decks.ts
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "../lib/narrative-state/readiness"
|
|
31
31
|
import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
|
|
32
32
|
import { backfillSlideClaimRefsFromCoverage } from "../lib/narrative-state/coverage"
|
|
33
|
-
import { closeResearchGapInState, deriveResearchGapsFromReadiness, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
|
|
33
|
+
import { closeResearchGapInState, deriveResearchGapsFromReadiness, deriveResearchTargets, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
|
|
34
34
|
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "../lib/narrative-state/normalize"
|
|
35
35
|
import { narrativeToBrief } from "../lib/narrative-state/project-compat"
|
|
36
36
|
import type { NarrativeStateV1 } from "../lib/narrative-state/types"
|
|
@@ -68,7 +68,7 @@ export default tool({
|
|
|
68
68
|
"It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
|
|
69
69
|
args: {
|
|
70
70
|
action: tool.schema
|
|
71
|
-
.enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
71
|
+
.enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "deriveResearchTargets", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
72
72
|
.describe("Action to perform on DECKS.json."),
|
|
73
73
|
summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
|
|
74
74
|
goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
|
|
@@ -484,6 +484,11 @@ export default tool({
|
|
|
484
484
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: derived.result, narrative: derived.state.narrative }, null, 2)
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
if (args.action === "deriveResearchTargets") {
|
|
488
|
+
const result = deriveResearchTargets(state, { workspaceRoot })
|
|
489
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
490
|
+
}
|
|
491
|
+
|
|
487
492
|
if (args.action === "upsertResearchGaps") {
|
|
488
493
|
if (!args.researchGaps?.length) return JSON.stringify({ ok: false, error: "researchGaps are required for upsertResearchGaps" })
|
|
489
494
|
const upserted = upsertResearchGapsInState(state, args.researchGaps as any[])
|
package/tools/narrative-view.ts
CHANGED
|
@@ -34,6 +34,16 @@ export default tool({
|
|
|
34
34
|
risks: tool.schema.string().optional(),
|
|
35
35
|
researchGaps: tool.schema.string().optional(),
|
|
36
36
|
coveredSlides: tool.schema.string().optional(),
|
|
37
|
+
storyWorkbench: tool.schema.string().optional(),
|
|
38
|
+
workbenchNote: tool.schema.string().optional(),
|
|
39
|
+
artifactCoverage: tool.schema.string().optional(),
|
|
40
|
+
noRenderTargets: tool.schema.string().optional(),
|
|
41
|
+
nextActions: tool.schema.string().optional(),
|
|
42
|
+
missingClaims: tool.schema.string().optional(),
|
|
43
|
+
affectedClaims: tool.schema.string().optional(),
|
|
44
|
+
affectedSlides: tool.schema.string().optional(),
|
|
45
|
+
notes: tool.schema.string().optional(),
|
|
46
|
+
recommendedNextCommand: tool.schema.string().optional(),
|
|
37
47
|
noClaims: tool.schema.string().optional(),
|
|
38
48
|
none: tool.schema.string().optional(),
|
|
39
49
|
}).optional(),
|
|
@@ -49,7 +59,7 @@ export default tool({
|
|
|
49
59
|
fromClaimId: tool.schema.string(),
|
|
50
60
|
toClaimId: tool.schema.string(),
|
|
51
61
|
relation: tool.schema.enum(["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"]),
|
|
52
|
-
|
|
62
|
+
displayLabel: tool.schema.string().optional().describe("Display-only localization of an existing canonical relation label. Omit for inferred relations."),
|
|
53
63
|
displayRationale: tool.schema.string().optional().describe("Display-only localization of an existing canonical rationale. Omit when canonical rationale is missing or the relation is inferred."),
|
|
54
64
|
})).optional(),
|
|
55
65
|
}).optional().describe("Localized and organized display-only projection. It must not add facts or alter IDs."),
|