@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.15.4",
3
+ "version": "0.16.2",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
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[])
@@ -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
- displayLabel: tool.schema.string().optional().describe("Display-only localization of an existing canonical relation label. Omit for inferred relations."),
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."),