@cyber-dash-tech/revela 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +16 -16
  2. package/README.zh-CN.md +16 -16
  3. package/lib/commands/brief.ts +63 -0
  4. package/lib/commands/edit.ts +7 -5
  5. package/lib/commands/help.ts +5 -3
  6. package/lib/commands/inspect.ts +7 -5
  7. package/lib/commands/narrative.ts +160 -0
  8. package/lib/decks-state.ts +33 -0
  9. package/lib/edit/prompt.ts +3 -0
  10. package/lib/inspect/prompt.ts +15 -2
  11. package/lib/inspect/requests.ts +21 -2
  12. package/lib/inspection-context/compile.ts +230 -10
  13. package/lib/inspection-context/match.ts +71 -1
  14. package/lib/inspection-context/project.ts +131 -8
  15. package/lib/inspection-context/result.ts +183 -0
  16. package/lib/narrative-state/coverage.ts +100 -0
  17. package/lib/narrative-state/display.ts +219 -0
  18. package/lib/narrative-state/executive-brief.ts +246 -0
  19. package/lib/narrative-state/hash.ts +9 -0
  20. package/lib/narrative-state/map-html.ts +348 -0
  21. package/lib/narrative-state/map.ts +282 -0
  22. package/lib/narrative-state/normalize.ts +54 -0
  23. package/lib/narrative-state/queries.ts +434 -0
  24. package/lib/narrative-state/readiness.ts +71 -1
  25. package/lib/narrative-state/render-plan.ts +44 -1
  26. package/lib/narrative-state/research-gaps.ts +191 -0
  27. package/lib/narrative-state/types.ts +33 -0
  28. package/lib/refine/server.ts +91 -13
  29. package/lib/workspace-state/evidence-status.ts +21 -1
  30. package/lib/workspace-state/graph.ts +56 -2
  31. package/lib/workspace-state/types.ts +10 -1
  32. package/package.json +1 -1
  33. package/plugin.ts +33 -2
  34. package/tools/decks.ts +86 -1
  35. package/tools/edit.ts +10 -8
  36. package/tools/inspection-result.ts +37 -0
  37. package/tools/narrative-view.ts +84 -0
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
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.")
@@ -393,7 +423,7 @@ const server: Plugin = (async (pluginCtx) => {
393
423
  }
394
424
  if (sub === "edit") {
395
425
  if (param) {
396
- await send("`/revela edit` no longer accepts a target. It opens the only HTML deck in `decks/`.")
426
+ await send("`/revela edit` is deprecated and does not accept a target. Use `/revela refine` for the unified refinement workspace.")
397
427
  throw new Error("__REVELA_EDIT_USAGE_HANDLED__")
398
428
  }
399
429
  await handleEdit({ client, sessionID, workspaceRoot }, send)
@@ -401,7 +431,7 @@ const server: Plugin = (async (pluginCtx) => {
401
431
  }
402
432
  if (sub === "inspect") {
403
433
  if (param) {
404
- await send("`/revela inspect` does not accept a target. It opens the only HTML deck in `decks/`.")
434
+ await send("`/revela inspect` is deprecated and does not accept a target. Use `/revela refine` for the unified reading and refinement workspace.")
405
435
  throw new Error("__REVELA_INSPECT_USAGE_HANDLED__")
406
436
  }
407
437
  await handleInspect({ client, sessionID, workspaceRoot }, send)
@@ -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,
package/tools/decks.ts CHANGED
@@ -28,6 +28,8 @@ import {
28
28
  reviewNarrativeState,
29
29
  } from "../lib/narrative-state/readiness"
30
30
  import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
31
+ import { backfillSlideClaimRefsFromCoverage } from "../lib/narrative-state/coverage"
32
+ import { closeResearchGapInState, deriveResearchGapsFromReadiness, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
31
33
  import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "../lib/narrative-state/normalize"
32
34
  import { narrativeToBrief } from "../lib/narrative-state/project-compat"
33
35
  import type { NarrativeStateV1 } from "../lib/narrative-state/types"
@@ -48,9 +50,11 @@ function mergeNarrativeInput(current: NarrativeStateV1, input: Partial<Narrative
48
50
  },
49
51
  thesis: input.thesis ? { ...current.thesis, ...input.thesis } as NarrativeStateV1["thesis"] : current.thesis,
50
52
  claims: input.claims ?? current.claims,
53
+ claimRelations: input.claimRelations ?? current.claimRelations,
51
54
  evidenceBindings: input.evidenceBindings ?? current.evidenceBindings,
52
55
  objections: input.objections ?? current.objections,
53
56
  risks: input.risks ?? current.risks,
57
+ researchGaps: input.researchGaps ?? current.researchGaps,
54
58
  approvals: current.approvals,
55
59
  updatedAt: new Date().toISOString(),
56
60
  }
@@ -63,7 +67,7 @@ export default tool({
63
67
  "It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
64
68
  args: {
65
69
  action: tool.schema
66
- .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "review", "reviewNarrative", "approveNarrative", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
70
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
67
71
  .describe("Action to perform on DECKS.json."),
68
72
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
69
73
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -113,6 +117,13 @@ export default tool({
113
117
  unsupportedScope: tool.schema.string().optional(),
114
118
  caveats: tool.schema.array(tool.schema.string()).optional(),
115
119
  })).optional(),
120
+ claimRelations: tool.schema.array(tool.schema.object({
121
+ id: tool.schema.string().optional(),
122
+ fromClaimId: tool.schema.string().describe("Canonical claim id the relation starts from."),
123
+ toClaimId: tool.schema.string().describe("Canonical claim id the relation points to."),
124
+ relation: tool.schema.enum(["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"]).optional(),
125
+ rationale: tool.schema.string().optional().describe("Short explanation of why this narrative relation exists."),
126
+ })).optional().describe("Canonical claim-to-claim narrative progression relationships. These affect narrative approval hash."),
116
127
  evidenceBindings: tool.schema.array(tool.schema.object({
117
128
  id: tool.schema.string().optional(),
118
129
  claimId: tool.schema.string().describe("Canonical claim id this evidence supports."),
@@ -141,6 +152,18 @@ export default tool({
141
152
  severity: tool.schema.enum(["high", "medium", "low"]).optional(),
142
153
  mitigation: tool.schema.string().optional(),
143
154
  })).optional(),
155
+ researchGaps: tool.schema.array(tool.schema.object({
156
+ id: tool.schema.string().optional(),
157
+ targetType: tool.schema.enum(["claim", "objection", "risk", "decision", "narrative"]).optional(),
158
+ targetId: tool.schema.string().optional(),
159
+ question: tool.schema.string().describe("Research question or gap to resolve."),
160
+ status: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional(),
161
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
162
+ findingsFile: tool.schema.string().optional(),
163
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional(),
164
+ createdFromIssueType: tool.schema.string().optional(),
165
+ notes: tool.schema.string().optional(),
166
+ })).optional(),
144
167
  }).optional().describe("For upsertNarrative: canonical narrative state fields to merge into DECKS.json. Replaces provided arrays, preserves approvals."),
145
168
  design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
146
169
  domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
@@ -190,6 +213,13 @@ export default tool({
190
213
  layout: tool.schema.string().describe("Design layout name."),
191
214
  qa: tool.schema.boolean().optional().describe("Whether the slide is marked QA-relevant deck metadata."),
192
215
  components: tool.schema.array(tool.schema.string()).describe("Design components used by this slide."),
216
+ claimIds: tool.schema.array(tool.schema.string()).optional().describe("Canonical narrative claim ids directly expressed by this slide."),
217
+ claimRefs: tool.schema.array(tool.schema.object({
218
+ claimId: tool.schema.string().describe("Canonical narrative claim id referenced by this slide."),
219
+ role: tool.schema.enum(["primary", "supporting", "evidence", "risk", "objection"]).describe("How the slide uses this claim."),
220
+ note: tool.schema.string().optional().describe("Optional short rationale for this claim-slide relationship."),
221
+ })).optional().describe("Structured canonical claim references for this slide; preferred over flat claimIds when available."),
222
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional().describe("Canonical narrative evidence binding ids used by this slide."),
193
223
  content: tool.schema.object({
194
224
  headline: tool.schema.string().optional(),
195
225
  body: tool.schema.array(tool.schema.string()).optional(),
@@ -224,6 +254,22 @@ export default tool({
224
254
  approvalNote: tool.schema.string().optional().describe("For approveNarrative: optional note explaining the approval or override."),
225
255
  approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative: use override only for explicit render overrides, not normal strategic approval."),
226
256
  approvalScope: tool.schema.enum(["narrative", "render_override"]).optional().describe("For approveNarrative: narrative approval or explicit render override scope."),
257
+ gapId: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: canonical research gap id."),
258
+ researchGaps: tool.schema.array(tool.schema.object({
259
+ id: tool.schema.string().optional(),
260
+ targetType: tool.schema.enum(["claim", "objection", "risk", "decision", "narrative"]).optional(),
261
+ targetId: tool.schema.string().optional(),
262
+ question: tool.schema.string().describe("Research question or gap to resolve."),
263
+ status: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional(),
264
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
265
+ findingsFile: tool.schema.string().optional(),
266
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional(),
267
+ createdFromIssueType: tool.schema.string().optional(),
268
+ notes: tool.schema.string().optional(),
269
+ })).optional().describe("For upsertResearchGaps: explicit canonical research gaps to create or update."),
270
+ gapStatus: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional().describe("For updateResearchGap: lifecycle status."),
271
+ gapNotes: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: notes or close reason."),
272
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional().describe("For updateResearchGap: canonical narrative evidence binding ids associated with the gap."),
227
273
  },
228
274
  async execute(args, context) {
229
275
  try {
@@ -382,6 +428,12 @@ export default tool({
382
428
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: compiled.result, deck: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck] : undefined, narrative: compiled.state.narrative }, null, 2)
383
429
  }
384
430
 
431
+ if (args.action === "backfillClaimRefs") {
432
+ const backfilled = backfillSlideClaimRefsFromCoverage(state)
433
+ writeDecksState(workspaceRoot, backfilled.state)
434
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: backfilled.result, deck: backfilled.state.activeDeck ? backfilled.state.decks[backfilled.state.activeDeck] : undefined, narrative: backfilled.state.narrative }, null, 2)
435
+ }
436
+
385
437
  if (args.action === "reviewNarrative") {
386
438
  const reviewed = reviewNarrativeState(state)
387
439
  recordNarrativeReviewAction(reviewed.state, reviewed.result)
@@ -400,6 +452,39 @@ export default tool({
400
452
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: approved.result, narrative: approved.state.narrative }, null, 2)
401
453
  }
402
454
 
455
+ if (args.action === "deriveResearchGaps") {
456
+ const derived = deriveResearchGapsFromReadiness(state)
457
+ writeDecksState(workspaceRoot, derived.state)
458
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: derived.result, narrative: derived.state.narrative }, null, 2)
459
+ }
460
+
461
+ if (args.action === "upsertResearchGaps") {
462
+ if (!args.researchGaps?.length) return JSON.stringify({ ok: false, error: "researchGaps are required for upsertResearchGaps" })
463
+ const upserted = upsertResearchGapsInState(state, args.researchGaps as any[])
464
+ writeDecksState(workspaceRoot, upserted.state)
465
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: upserted.result, narrative: upserted.state.narrative }, null, 2)
466
+ }
467
+
468
+ if (args.action === "updateResearchGap") {
469
+ if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for updateResearchGap" })
470
+ const updated = updateResearchGapInState(state, {
471
+ id: args.gapId,
472
+ status: args.gapStatus as any,
473
+ findingsFile: args.findingsFile,
474
+ evidenceBindingIds: args.evidenceBindingIds,
475
+ notes: args.gapNotes,
476
+ })
477
+ writeDecksState(workspaceRoot, updated.state)
478
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: updated.result, narrative: updated.state.narrative }, null, 2)
479
+ }
480
+
481
+ if (args.action === "closeResearchGap") {
482
+ if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for closeResearchGap" })
483
+ const closed = closeResearchGapInState(state, args.gapId, args.gapNotes)
484
+ writeDecksState(workspaceRoot, closed.state)
485
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: closed.result, narrative: closed.state.narrative }, null, 2)
486
+ }
487
+
403
488
  if (args.action === "applyEvidenceCandidates") {
404
489
  const candidateIds = args.candidateIds ?? []
405
490
  if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
package/tools/edit.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * tools/edit.ts
3
3
  *
4
- * revela-edit — Open Revela's visual comment editor for an existing deck.
4
+ * revela-edit — Compatibility tool that opens Revela Refine in Edit mode.
5
5
  */
6
6
 
7
7
  import { tool } from "@opencode-ai/plugin"
8
- import { openEditableDeck } from "../lib/edit/open"
8
+ import { openRefineDeck } from "../lib/refine/open"
9
9
 
10
10
  export function createEditTool(options: { client: any; workspaceRoot: string; openBrowser?: boolean }) {
11
11
  return tool({
12
12
  description:
13
- "Open Revela's visual comment editor for an existing slide deck. " +
13
+ "Open Revela Refine in Edit mode for an existing slide deck. " +
14
14
  "Use this when the user asks to edit, revise, annotate, or visually comment on a deck, " +
15
15
  "including when they reference the current deck. " +
16
- "Revela 0.8 opens the only HTML deck in decks/. " +
17
- "This opens a local browser editor where the user can Ctrl/Cmd-click deck elements, write comments, " +
16
+ "This is a compatibility tool for the older edit-only workflow; the user-facing entry is /revela refine. " +
17
+ "It opens a local browser workspace where the user can Ctrl/Cmd-click deck elements, use the Edit tab, " +
18
18
  "and send precise edit requests back to the current OpenCode session.",
19
19
  args: {},
20
20
  async execute(_args, context: any) {
@@ -27,10 +27,11 @@ export function createEditTool(options: { client: any; workspaceRoot: string; op
27
27
  }
28
28
 
29
29
  try {
30
- const result = openEditableDeck("", {
30
+ const result = openRefineDeck("", {
31
31
  client: options.client,
32
32
  sessionID,
33
33
  workspaceRoot: options.workspaceRoot,
34
+ mode: "edit",
34
35
  openBrowser: options.openBrowser,
35
36
  })
36
37
 
@@ -40,9 +41,10 @@ export function createEditTool(options: { client: any; workspaceRoot: string; op
40
41
  file: result.deck.file,
41
42
  source: result.source,
42
43
  url: result.url,
44
+ mode: result.mode,
43
45
  message:
44
- `${result.stateNote} Opened visual editor. ` +
45
- "Ask the user to use Ctrl/Cmd + click in the browser to reference elements, write a comment, then send comments.",
46
+ `${result.stateNote} Opened Revela Refine in Edit mode. ` +
47
+ "Ask the user to use Ctrl/Cmd-click in the browser to reference elements, then use the Edit tab to send comments.",
46
48
  }, null, 2)
47
49
  } catch (error) {
48
50
  return JSON.stringify({
@@ -29,6 +29,43 @@ export default tool({
29
29
  }).optional(),
30
30
  matchConfidence: tool.schema.enum(["none", "low", "medium", "high"]),
31
31
  cards: tool.schema.object({
32
+ reading: tool.schema.object({
33
+ status: tool.schema.enum(["matched", "no_match"]),
34
+ claimId: tool.schema.string().optional(),
35
+ canonicalClaimId: tool.schema.string().optional(),
36
+ claimText: tool.schema.string().optional(),
37
+ evidenceStatus: tool.schema.string().optional(),
38
+ evidenceBindingIds: tool.schema.array(tool.schema.string()),
39
+ supportedScope: tool.schema.string().optional(),
40
+ unsupportedScope: tool.schema.string().optional(),
41
+ caveats: tool.schema.array(tool.schema.string()),
42
+ relatedObjections: tool.schema.array(tool.schema.string()),
43
+ relatedRisks: tool.schema.array(tool.schema.string()),
44
+ artifactCoverage: tool.schema.array(tool.schema.object({
45
+ artifactId: tool.schema.string(),
46
+ type: tool.schema.string(),
47
+ outputPath: tool.schema.string().optional(),
48
+ coverageStatus: tool.schema.enum(["current", "stale", "partial", "missing"]),
49
+ containsClaim: tool.schema.boolean(),
50
+ stale: tool.schema.boolean(),
51
+ staleReason: tool.schema.string().optional(),
52
+ locations: tool.schema.array(tool.schema.string()),
53
+ note: tool.schema.string().optional(),
54
+ })).optional(),
55
+ rationale: tool.schema.string(),
56
+ }).optional(),
57
+ exploratory: tool.schema.object({
58
+ status: tool.schema.enum(["available", "limited", "unavailable"]),
59
+ official: tool.schema.boolean().describe("Must be false; exploratory reading is not official artifact content."),
60
+ audience: tool.schema.string().optional(),
61
+ claimFocus: tool.schema.string().optional(),
62
+ objectionPrompts: tool.schema.array(tool.schema.string()),
63
+ audienceReframe: tool.schema.string(),
64
+ appendixLeads: tool.schema.array(tool.schema.string()),
65
+ meetingPrep: tool.schema.array(tool.schema.string()),
66
+ boundaries: tool.schema.array(tool.schema.string()),
67
+ rationale: tool.schema.string(),
68
+ }).optional(),
32
69
  purpose: tool.schema.object({
33
70
  status: tool.schema.enum(["clear", "weak", "misplaced", "unknown"]),
34
71
  role: tool.schema.string().optional(),
@@ -0,0 +1,84 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { hasDecksState, readDecksState } from "../lib/decks-state"
3
+ import { buildNarrativeMap } from "../lib/narrative-state/map"
4
+ import { validateNarrativeDisplayModel, type NarrativeDisplayModel, type NarrativeViewLanguage } from "../lib/narrative-state/display"
5
+ import { writeNarrativeMapHtml } from "../lib/commands/narrative"
6
+ import { openUrl } from "../lib/edit/open"
7
+
8
+ export default tool({
9
+ description:
10
+ "Render Revela's read-only narrative claim-flow UI from the current deterministic narrative map plus an optional localized display model. " +
11
+ "This tool validates display IDs against DECKS.json, opens a local HTML view, and never mutates workspace state.",
12
+ args: {
13
+ language: tool.schema.string().describe("UI language request from /revela narrative. May be any language tag or language name, such as en, zh-CN, fr, de, Korean, Arabic, or Portuguese-BR."),
14
+ narrativeHash: tool.schema.string().optional().describe("Narrative hash from the prompt projection. Used to detect stale display prompts."),
15
+ displayModel: tool.schema.object({
16
+ version: tool.schema.number().describe("Must be 1."),
17
+ language: tool.schema.string().describe("Must exactly match the top-level language request."),
18
+ pageTitle: tool.schema.string().optional(),
19
+ summaryLine: tool.schema.string().optional(),
20
+ labels: tool.schema.object({
21
+ eyebrow: tool.schema.string().optional(),
22
+ claimFlow: tool.schema.string().optional(),
23
+ flowNote: tool.schema.string().optional(),
24
+ selectedClaim: tool.schema.string().optional(),
25
+ claim: tool.schema.string().optional(),
26
+ claimId: tool.schema.string().optional(),
27
+ status: tool.schema.string().optional(),
28
+ supportedScope: tool.schema.string().optional(),
29
+ unsupportedScope: tool.schema.string().optional(),
30
+ incomingRelations: tool.schema.string().optional(),
31
+ outgoingRelations: tool.schema.string().optional(),
32
+ evidence: tool.schema.string().optional(),
33
+ objections: tool.schema.string().optional(),
34
+ risks: tool.schema.string().optional(),
35
+ researchGaps: tool.schema.string().optional(),
36
+ coveredSlides: tool.schema.string().optional(),
37
+ noClaims: tool.schema.string().optional(),
38
+ none: tool.schema.string().optional(),
39
+ }).optional(),
40
+ claimCards: tool.schema.array(tool.schema.object({
41
+ claimId: tool.schema.string().describe("Existing canonical claim id. Must match the deterministic map."),
42
+ displayTitle: tool.schema.string().optional().describe("Display-only localized claim title in the requested language. For Chinese manufacturing/industrial AI context, translate autonomy as 自主化/自主能力, not 自治; convert slug-like claim text into a readable title."),
43
+ roleLabel: tool.schema.string().optional(),
44
+ narrativeJob: tool.schema.string().optional(),
45
+ evidenceSummary: tool.schema.string().optional(),
46
+ riskOrGapSummary: tool.schema.string().optional(),
47
+ })).optional(),
48
+ relations: tool.schema.array(tool.schema.object({
49
+ fromClaimId: tool.schema.string(),
50
+ toClaimId: tool.schema.string(),
51
+ 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."),
53
+ 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
+ })).optional(),
55
+ }).optional().describe("Localized and organized display-only projection. It must not add facts or alter IDs."),
56
+ },
57
+ async execute(args, context) {
58
+ const workspaceRoot = context.directory ?? process.cwd()
59
+ try {
60
+ if (!hasDecksState(workspaceRoot)) {
61
+ return JSON.stringify({ ok: false, error: "No DECKS.json found. Run /revela init first." })
62
+ }
63
+ const language = args.language as NarrativeViewLanguage
64
+ const map = buildNarrativeMap(readDecksState(workspaceRoot))
65
+ const stalePrompt = Boolean(args.narrativeHash && args.narrativeHash !== map.snapshot.narrativeHash)
66
+ const display = validateNarrativeDisplayModel(map, args.displayModel as NarrativeDisplayModel | undefined, language)
67
+ const htmlPath = writeNarrativeMapHtml(map, display)
68
+ const url = `file://${htmlPath}`
69
+ openUrl(url)
70
+ return JSON.stringify({ ok: true, url, path: htmlPath, narrativeHash: map.snapshot.narrativeHash, stalePrompt, fallback: false }, null, 2)
71
+ } catch (e: any) {
72
+ try {
73
+ const language = (args.language ?? "en") as NarrativeViewLanguage
74
+ const map = buildNarrativeMap(readDecksState(workspaceRoot))
75
+ const htmlPath = writeNarrativeMapHtml(map)
76
+ const url = `file://${htmlPath}`
77
+ openUrl(url)
78
+ return JSON.stringify({ ok: false, fallback: true, url, path: htmlPath, error: e.message || String(e) }, null, 2)
79
+ } catch (fallbackError: any) {
80
+ return JSON.stringify({ ok: false, fallback: false, error: e.message || String(e), fallbackError: fallbackError.message || String(fallbackError) })
81
+ }
82
+ }
83
+ },
84
+ })