@cyber-dash-tech/revela 0.16.1 → 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/research.ts +25 -18
- package/lib/narrative-state/research-gaps.ts +332 -0
- package/package.json +1 -1
- package/tools/decks.ts +7 -2
package/lib/commands/research.ts
CHANGED
|
@@ -25,18 +25,19 @@ Current state:
|
|
|
25
25
|
${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
|
|
26
26
|
|
|
27
27
|
Closed-loop workflow:
|
|
28
|
-
1. Call \`revela-decks\` action \`read\`, then \`reviewNarrative\`.
|
|
29
|
-
2. If current research gaps are missing or stale, call \`deriveResearchGaps\` when useful. Do not invent gaps that are not tied to a claim, objection, risk, decision, or narrative issue.
|
|
28
|
+
1. Call \`revela-decks\` action \`read\`, then \`reviewNarrative\`, then \`deriveResearchTargets\`. Treat the returned \`selected\` target as the deterministic first target unless it is clearly blocked by user-only information.
|
|
29
|
+
2. If current research gaps are missing or stale, call \`deriveResearchGaps\` when useful, then call \`deriveResearchTargets\` again. Do not invent gaps that are not tied to a claim, objection, risk, decision, or narrative issue.
|
|
30
30
|
3. Run up to 3 research loops unless the stop conditions below are met earlier.
|
|
31
|
-
4. At the start of each loop,
|
|
32
|
-
5. If a target
|
|
33
|
-
6.
|
|
34
|
-
7.
|
|
35
|
-
8.
|
|
36
|
-
9.
|
|
37
|
-
10.
|
|
38
|
-
11.
|
|
39
|
-
12.
|
|
31
|
+
4. At the start of each loop, use \`deriveResearchTargets\` as the target order. Work the \`selected\` target first, then the next 1-2 highest-priority targets only when they are related. Do not repeat searches for claims already strongly supported.
|
|
32
|
+
5. If a target has \`findingsFile\` or \`kind: "unattached_findings"\`, inspect \`bindingDiagnostic\` before doing external search. Prefer existing findings before external research.
|
|
33
|
+
6. When \`bindingDiagnostic.bindable\` is false, do not bind or package the findings as strong evidence. Report the exact \`failureReasons\` such as \`missing_quote\`, \`unclear_source\`, \`unsupported_scope\`, \`caveat_conflict\`, \`weak_source\`, \`source_mismatch\`, or \`context_only_finding\`, then either narrow the claim safely or run targeted research for the missing fields.
|
|
34
|
+
7. For targets needing external evidence, mark matching gaps \`in_progress\` with \`revela-decks updateResearchGap\`, then delegate search to the \`revela-research\` subagent. Ask it for source URLs, quotes/snippets, dates or locations when available, caveats, remaining gaps, and a \`## Recommended evidence bindings\` section with claimId, quote, source, supportScope, unsupportedScope, caveat, and strength. Save findings with \`revela-research-save\` under \`researches/{topic}/{axis}.md\` using \`## Data\`, \`## Cases\`, \`## Images\`, and \`## Gaps\` sections as applicable.
|
|
35
|
+
8. After findings are saved or existing findings are selected, read or inspect the findings file. Attach it with \`revela-decks attachResearchFindings\` when it maps to an existing research axis. Re-run \`deriveResearchTargets\` so the next loop sees updated \`bindingDiagnostic\` and target order.
|
|
36
|
+
9. Automatically bind evidence only when all binding criteria are met and the diagnostic is \`bindable: true\` or the same fields are explicit in the findings. Use \`revela-decks applyEvidenceCandidates\` for concrete candidate ids when available, or \`upsertNarrative\` to preserve canonical evidence bindings with exact source, URL/path, quote/snippet, support scope, unsupported scope, caveat, and strength.
|
|
37
|
+
10. Binding criteria: claimId exists; quote/snippet is traceable to the source and is not invented; source URL or workspace source path is present; supportScope and unsupportedScope are explicit; strength is strong or useful partial; caveat is preserved; binding does not expand the claim beyond the evidence.
|
|
38
|
+
11. If a claim or relation is broader than the evidence, narrow the claim text, supportedScope, unsupportedScope, or relation rationale through \`upsertNarrative\` only when the narrower wording preserves the user's strategic meaning and source boundaries. Do not silently change the decision ask, central recommendation, or approval meaning.
|
|
39
|
+
12. Update matching gaps after binding: use \`evidence_bound\` when canonical evidence was added, \`closed\` when the gap is resolved or non-researchable, \`findings_saved\` only when findings exist but binding criteria are not met, and \`open\` with notes when more external research is still warranted.
|
|
40
|
+
13. Re-run \`reviewNarrative\` and \`deriveResearchTargets\` after each loop. Compare against the previous loop: fewer open gaps, fewer unattached findings, stronger evidence, narrower unsupported scope, or clearer internal-data caveats should count as progress.
|
|
40
41
|
|
|
41
42
|
Stop conditions:
|
|
42
43
|
- No open externally researchable gaps remain.
|
|
@@ -47,20 +48,26 @@ Stop conditions:
|
|
|
47
48
|
|
|
48
49
|
Report format:
|
|
49
50
|
- Start with \`Research loop completed after <n> round(s).\`
|
|
50
|
-
-
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
51
|
+
- Then use these exact sections in order:
|
|
52
|
+
- \`Selected target\`: report \`kind\`, \`priority\`, \`reason\`, \`question\`, \`targetId\`, \`claimId\`, and any \`findingsFile\`.
|
|
53
|
+
- \`Existing findings inspected\`: for each file, report \`findingsFile\`, \`bindingDiagnostic.bindable\`, \`failureReasons\`, and which explicit fields were present: \`source\`, \`quoteOrSnippet\`, \`supportScope\`, \`unsupportedScope\`, \`caveat\`, \`strength\`. If none were inspected, write \`none\`.
|
|
54
|
+
- \`Attachments\`: list findings attached with axis/status, or \`none\`.
|
|
55
|
+
- \`Evidence bound\`: list evidence bindings by claim id, source, quote/snippet, supportScope, unsupportedScope, caveat, and strength, or \`none\`.
|
|
56
|
+
- \`Unbound findings\`: list every inspected but unbound findings file with structured failure reasons such as \`missing_quote\`, \`unclear_source\`, \`unsupported_scope\`, \`caveat_conflict\`, \`weak_source\`, \`source_mismatch\`, or \`context_only_finding\`. If none, write \`none\`.
|
|
57
|
+
- \`Gap updates\`: list gaps moved to \`in_progress\`, \`findings_saved\`, \`attached\`, \`evidence_bound\`, \`closed\`, or still \`open\` with notes.
|
|
58
|
+
- \`Narrative changes\`: list claims or relations narrowed, with remaining unsupported scope. If none, write \`none\`.
|
|
59
|
+
- \`Remaining caveats\`: use only \`internal_data_needed\`, \`not_publicly_researchable\`, \`source_quality_limit\`, or \`still_open\`.
|
|
60
|
+
- \`Next smallest story action\`: end with one concrete next command or action, not a generic request for confirmation.
|
|
61
|
+
- If no binding happened, the \`Unbound findings\` or \`Remaining caveats\` section must say why binding criteria failed and what exact source type is needed next.
|
|
56
62
|
|
|
57
63
|
Rules:
|
|
58
64
|
- Do not use primary-agent broad websearch. Use the \`revela-research\` subagent for external search.
|
|
59
65
|
- Do not invent quotes, source paths, URLs, page references, locations, or caveats.
|
|
60
66
|
- Do not treat \`researches/**/*.md\` as canonical evidence until attached or evidence-bound, but do not stop at findings_saved when binding criteria are met.
|
|
67
|
+
- Do not bypass \`deriveResearchTargets\`; target selection, \`selected\`, and \`bindingDiagnostic\` are deterministic inputs, not LLM judgement.
|
|
61
68
|
- Do not mutate canonical claims merely to fit a source; narrow only to preserve evidence boundaries and avoid overstated claims.
|
|
62
69
|
- Do not ask the user to approve each evidence binding. Ask only when binding would change strategic meaning, downgrade a central claim, rely on suspicious sources, or require narrative approval.
|
|
63
70
|
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
64
71
|
|
|
65
|
-
Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, reviewing current readiness, and running the first research/binding loop.`
|
|
72
|
+
Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, reviewing current readiness, deriving research targets, and running the first research/binding loop from the selected target.`
|
|
66
73
|
}
|
|
@@ -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[])
|