@cyber-dash-tech/revela 0.17.1 → 0.17.3

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/tools/decks.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import {
3
+ createEmptyDecksState,
3
4
  createDeckSpec,
4
5
  confirmDeckPlan,
5
6
  DECKS_STATE_FILE,
7
+ hasDecksState,
6
8
  normalizeWorkspaceDeckState,
7
- readOrCreateDecksState,
9
+ readDecksState,
8
10
  reviewDeckState,
9
11
  upsertDeck,
10
12
  upsertSlides,
@@ -30,10 +32,11 @@ import {
30
32
  reviewNarrativeState,
31
33
  } from "../lib/narrative-state/readiness"
32
34
  import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
35
+ import { DECK_PLAN_ARTIFACT_PATH, readDeckPlanArtifact } from "../lib/narrative-state/deck-plan-artifact"
33
36
  import { backfillSlideClaimRefsFromCoverage } from "../lib/narrative-state/coverage"
34
37
  import { closeResearchGapInState, deriveResearchGapsFromReadiness, deriveResearchTargets, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
35
38
  import { evaluateResearchFindingsBinding } from "../lib/narrative-state/research-binding-eval"
36
- import { stableEvidenceId } from "../lib/narrative-state/hash"
39
+ import { computeNarrativeHash, stableEvidenceId } from "../lib/narrative-state/hash"
37
40
  import { normalizeNarrativeState } from "../lib/narrative-state/normalize"
38
41
  import { buildNarrativeVaultInventory, compileNarrativeVault, exportNarrativeStateToVault, formatVaultDiagnosticReport, getNarrativeVaultMigrationHint, hasNarrativeVault, initNarrativeVault, narrativeVaultAuthoringContract, narrativeVaultTimestampMs, removeVaultRelation, runNarrativeMarkdownQa, updateVaultCoreNodes, updateVaultResearchGapNode, upsertVaultClaimNode, upsertVaultEvidenceNode, upsertVaultObjectionNode, upsertVaultRelation, upsertVaultRiskNode, VAULT_MIGRATION_PRESERVED_IN_DECKS_JSON } from "../lib/narrative-vault"
39
42
  import { compileCacheMirrorNarrativeVault } from "../lib/narrative-vault/compile-mirror"
@@ -114,10 +117,10 @@ export default tool({
114
117
  description:
115
118
  `Read and update ${DECKS_STATE_FILE}, Revela's workspace deck state file. ` +
116
119
  "Use this tool instead of writing or patching the state file directly. " +
117
- "It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
120
+ "It stores compatibility/render state, narrative projections, active output paths, cached deck projections, approvals, reviews, artifact coverage, and readiness.",
118
121
  args: {
119
122
  action: tool.schema
120
- .enum(["read", "init", "initNarrativeVault", "narrativeInventory", "vaultInventory", "markdownQa", "upsertDeck", "upsertSlides", "upsertNarrative", "compileNarrativeVault", "exportNarrativeVault", "upsertVaultEvidence", "bindResearchFindings", "updateVaultResearchGap", "upsertVaultResearchGap", "upsertVaultClaim", "upsertVaultObjection", "upsertVaultRisk", "upsertVaultRelation", "removeVaultRelation", "updateVaultCoreNarrative", "compileDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "deriveResearchTargets", "evaluateResearchFindings", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
123
+ .enum(["read", "init", "initNarrativeVault", "narrativeInventory", "vaultInventory", "markdownQa", "upsertDeck", "upsertSlides", "upsertNarrative", "compileNarrativeVault", "exportNarrativeVault", "upsertVaultEvidence", "bindResearchFindings", "updateVaultResearchGap", "upsertVaultResearchGap", "upsertVaultClaim", "upsertVaultObjection", "upsertVaultRisk", "upsertVaultRelation", "removeVaultRelation", "updateVaultCoreNarrative", "compileDeckPlan", "readDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "deriveResearchTargets", "evaluateResearchFindings", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
121
124
  .describe("Action to perform on DECKS.json."),
122
125
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
123
126
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -348,7 +351,14 @@ export default tool({
348
351
  async execute(args, context) {
349
352
  try {
350
353
  const workspaceRoot = context.directory ?? process.cwd()
351
- let state = normalizeWorkspaceDeckState(readOrCreateDecksState(workspaceRoot), workspaceRoot)
354
+ const loadState = () => hasDecksState(workspaceRoot) ? readDecksState(workspaceRoot) : createEmptyDecksState()
355
+ let state = normalizeWorkspaceDeckState(loadState(), workspaceRoot)
356
+ const persistState = (next: DecksState) => {
357
+ state = normalizeWorkspaceDeckState(next, workspaceRoot)
358
+ if (hasDecksState(workspaceRoot)) writeDecksState(workspaceRoot, state)
359
+ return hasDecksState(workspaceRoot)
360
+ }
361
+ const mirrorOptions = (extra: Record<string, unknown> = {}) => hasDecksState(workspaceRoot) ? { state, ...extra } : extra
352
362
  const defaultSlug = workspaceDeckSlug(workspaceRoot)
353
363
 
354
364
  if (args.action === "init") {
@@ -368,8 +378,8 @@ export default tool({
368
378
  nodeIds: discovered.map((material) => `source:${material.path}`),
369
379
  })
370
380
  }
371
- writeDecksState(workspaceRoot, state)
372
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, ingest, state }, null, 2)
381
+ const persisted = persistState(state)
382
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, ingest, state }, null, 2)
373
383
  }
374
384
 
375
385
  if (args.action === "read") {
@@ -404,7 +414,7 @@ export default tool({
404
414
  }
405
415
 
406
416
  if (args.action === "compileNarrativeVault") {
407
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
417
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
408
418
  const { result, diagnosticReport } = compiled
409
419
  const markdownQa = hasNarrativeVault(workspaceRoot) ? runNarrativeMarkdownQa(workspaceRoot) : undefined
410
420
  const narrativeInventory = hasNarrativeVault(workspaceRoot) ? buildNarrativeVaultInventory(workspaceRoot) : undefined
@@ -415,7 +425,7 @@ export default tool({
415
425
  if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "revela-narrative/ already exists. Edit Markdown nodes directly or move the existing vault before exporting." })
416
426
  const narrative = state.narrative ?? normalizeNarrativeState(state)
417
427
  const result = exportNarrativeStateToVault(workspaceRoot, narrative)
418
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state, fallbackApprovals: narrative.approvals })
428
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions({ fallbackApprovals: narrative.approvals }))
419
429
  return JSON.stringify({
420
430
  ok: compiled.result.ok,
421
431
  path: "revela-narrative",
@@ -435,7 +445,7 @@ export default tool({
435
445
 
436
446
  if (args.action === "initNarrativeVault") {
437
447
  if (hasNarrativeVault(workspaceRoot)) {
438
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
448
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
439
449
  return JSON.stringify({ ok: true, path: "revela-narrative", created: false, files: [], diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract(), nextActions: ["Inspect narrativeInventory before authoring; reuse existing ids and relation targets.", "Maintain Markdown nodes directly when useful, then run compileNarrativeVault. Use structured vault helpers only when they reduce schema risk."], narrative: compiled.result.narrative }, null, 2)
440
450
  }
441
451
  const result = initNarrativeVault(workspaceRoot, {
@@ -445,7 +455,7 @@ export default tool({
445
455
  decision: args.narrative?.decision as any,
446
456
  thesis: args.narrative?.thesis as any,
447
457
  })
448
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
458
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
449
459
  return JSON.stringify({ ok: compiled.result.ok, path: result.path, created: result.created, files: result.files, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract(), nextActions: ["Inspect narrativeInventory before adding claims, evidence, relations, or research gaps; reuse existing ids where possible.", "Maintain Markdown nodes directly when useful. Use structured vault helpers for evidence binding or when they reduce schema risk.", "Treat workspace source material records as candidates until explicit evidence trace is written."], narrative: compiled.result.narrative }, null, 2)
450
460
  }
451
461
 
@@ -454,7 +464,7 @@ export default tool({
454
464
  if (!args.evidence) return JSON.stringify({ ok: false, error: "evidence is required for upsertVaultEvidence" })
455
465
  const mutation = upsertVaultEvidenceNode(workspaceRoot, args.evidence as any)
456
466
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
457
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
467
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
458
468
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
459
469
  }
460
470
 
@@ -495,7 +505,7 @@ export default tool({
495
505
  notes: gap.notes,
496
506
  })
497
507
  : undefined
498
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
508
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
499
509
  return JSON.stringify({
500
510
  ok: compiled.result.ok,
501
511
  path: mutation.file,
@@ -521,7 +531,7 @@ export default tool({
521
531
  notes: args.gapNotes,
522
532
  })
523
533
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation, recovery: researchGapHelperRecovery("updateVaultResearchGap", mutation.missingFields), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
524
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
534
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
525
535
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
526
536
  }
527
537
 
@@ -544,7 +554,7 @@ export default tool({
544
554
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation, recovery: researchGapHelperRecovery("upsertVaultResearchGap", mutation.missingFields), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
545
555
  mutations.push(mutation)
546
556
  }
547
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
557
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
548
558
  return JSON.stringify({ ok: compiled.result.ok, path: mutations[0]?.file, mutation: mutations[0], mutations, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, authoringContract: narrativeVaultAuthoringContract(), narrative: compiled.result.narrative }, null, 2)
549
559
  }
550
560
 
@@ -560,7 +570,7 @@ export default tool({
560
570
  rationale: relation.rationale,
561
571
  })
562
572
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation, recovery: relationHelperRecovery("upsertVaultRelation", mutation.missingFields), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
563
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
573
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
564
574
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract(), narrative: compiled.result.narrative }, null, 2)
565
575
  }
566
576
 
@@ -570,7 +580,7 @@ export default tool({
570
580
  if (!relationId) return JSON.stringify({ ok: false, error: "relationId or relation.id is required for removeVaultRelation", recovery: relationHelperRecovery("removeVaultRelation"), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
571
581
  const mutation = removeVaultRelation(workspaceRoot, relationId)
572
582
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation, recovery: relationHelperRecovery("removeVaultRelation", mutation.missingFields), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
573
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
583
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
574
584
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract(), narrative: compiled.result.narrative }, null, 2)
575
585
  }
576
586
 
@@ -583,7 +593,7 @@ export default tool({
583
593
  : undefined
584
594
  const mutation = upsertVaultClaimNode(workspaceRoot, claim)
585
595
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
586
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
596
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
587
597
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, relationHint, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
588
598
  }
589
599
 
@@ -593,7 +603,7 @@ export default tool({
593
603
  if (!objection?.id) return JSON.stringify({ ok: false, error: "narrative.objections[0].id is required for upsertVaultObjection" })
594
604
  const mutation = upsertVaultObjectionNode(workspaceRoot, objection)
595
605
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
596
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
606
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
597
607
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
598
608
  }
599
609
 
@@ -603,7 +613,7 @@ export default tool({
603
613
  if (!risk?.id) return JSON.stringify({ ok: false, error: "narrative.risks[0].id is required for upsertVaultRisk" })
604
614
  const mutation = upsertVaultRiskNode(workspaceRoot, risk)
605
615
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
606
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
616
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
607
617
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
608
618
  }
609
619
 
@@ -611,7 +621,7 @@ export default tool({
611
621
  if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "updateVaultCoreNarrative requires revela-narrative/ to exist. Use initNarrativeVault first." })
612
622
  const mutation = updateVaultCoreNodes(workspaceRoot, args.narrative as any ?? {})
613
623
  if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
614
- const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
624
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, mirrorOptions())
615
625
  return JSON.stringify({ ok: compiled.result.ok, path: mutation.file, mutation, diagnostics: compiled.result.diagnostics, diagnosticReport: compiled.diagnosticReport, narrative: compiled.result.narrative }, null, 2)
616
626
  }
617
627
 
@@ -637,16 +647,16 @@ export default tool({
637
647
  researchPlan: (args.researchPlan as ResearchAxis[] | undefined) ?? existing?.researchPlan,
638
648
  }
639
649
  const next = upsertDeck(state, deckInput)
640
- writeDecksState(workspaceRoot, next)
641
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
650
+ const persisted = persistState(next)
651
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
642
652
  }
643
653
 
644
654
  if (args.action === "upsertSlides") {
645
655
  const deckKey = state.activeDeck || defaultSlug
646
656
  if (!args.slides) return JSON.stringify({ ok: false, error: "slides are required for upsertSlides" })
647
657
  const next = upsertSlides(state, deckKey, args.slides as SlideSpec[])
648
- writeDecksState(workspaceRoot, next)
649
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
658
+ const persisted = persistState(next)
659
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
650
660
  }
651
661
 
652
662
  if (args.action === "upsertNarrative") {
@@ -679,14 +689,15 @@ export default tool({
679
689
  summary: `Reviewed deck readiness: ${reviewed.result.ready ? "ready" : "blocked"}.`,
680
690
  nodeIds: [`artifact:${reviewed.state.decks[reviewed.result.slug]?.outputPath ?? reviewed.result.slug}`, ...(snapshot ? [snapshot.id] : [])],
681
691
  })
682
- writeDecksState(workspaceRoot, reviewed.state)
683
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
692
+ const persisted = persistState(reviewed.state)
693
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: reviewed.result }, null, 2)
684
694
  }
685
695
 
686
696
  if (args.action === "compileDeckPlan") {
687
697
  const qaGate = strictVaultMarkdownQaGate(workspaceRoot, "render", "compileDeckPlan")
688
698
  if (qaGate) return JSON.stringify(qaGate, null, 2)
689
699
  const compiled = compileDeckPlanFromNarrative(state)
700
+ const planArtifact = compiled.result.compiled ? { path: DECK_PLAN_ARTIFACT_PATH } : undefined
690
701
  if (compiled.result.compiled) {
691
702
  recordWorkspaceAction(compiled.state, {
692
703
  type: "deck.plan_compiled",
@@ -694,23 +705,44 @@ export default tool({
694
705
  inputs: { narrativeId: compiled.state.narrative?.id, activeDeck: compiled.state.activeDeck },
695
706
  outputs: {
696
707
  narrativeHash: compiled.result.narrativeHash,
708
+ planArtifactPath: planArtifact?.path,
709
+ planningPacket: Boolean(compiled.result.planningPacket),
710
+ deckPlanRequirements: Boolean(compiled.result.deckPlanRequirements),
697
711
  slideCount: compiled.result.slideCount,
698
712
  outputPath: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck]?.outputPath : undefined,
699
713
  },
700
714
  status: "success",
701
- summary: `Compiled deck plan from canonical narrative with ${compiled.result.slideCount} slide${compiled.result.slideCount === 1 ? "" : "s"}.`,
715
+ summary: "Prepared deck planning packet and deck-plan authoring requirements from canonical narrative.",
702
716
  nodeIds: [compiled.state.narrative?.id, compiled.state.activeDeck ? `artifact:${compiled.state.decks[compiled.state.activeDeck]?.outputPath ?? compiled.state.activeDeck}` : undefined].filter((item): item is string => Boolean(item)),
703
717
  })
704
718
  }
705
- writeDecksState(workspaceRoot, compiled.state)
706
- 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)
719
+ const persisted = persistState(compiled.state)
720
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, planArtifact, result: compiled.result, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined, narrative: state.narrative }, null, 2)
721
+ }
722
+
723
+ if (args.action === "readDeckPlan") {
724
+ const narrative = normalizeNarrativeState(state)
725
+ const knownNodeIds = new Set([
726
+ ...narrative.claims.map((claim) => claim.id),
727
+ ...narrative.evidenceBindings.map((binding) => binding.id),
728
+ ...narrative.risks.map((risk) => risk.id),
729
+ ...narrative.objections.map((objection) => objection.id),
730
+ ...(narrative.researchGaps ?? []).map((gap) => gap.id),
731
+ ])
732
+ const read = readDeckPlanArtifact(workspaceRoot, { narrativeHash: computeNarrativeHash(narrative), knownNodeIds })
733
+ return JSON.stringify({ ok: read.ok, planArtifact: read }, null, 2)
707
734
  }
708
735
 
709
736
  if (args.action === "confirmDeckPlan") {
710
- if (args.approvalBy && args.approvalBy !== "user") return JSON.stringify({ ok: false, error: "confirmDeckPlan requires approvalBy=user" })
737
+ const deck = state.activeDeck ? state.decks[state.activeDeck] : undefined
738
+ if (!deck) return JSON.stringify({ ok: false, result: { confirmed: false, skipped: true, reason: "Cannot confirm because no current deck exists." } }, null, 2)
739
+ const narrative = normalizeNarrativeState(state)
740
+ const narrativeHash = computeNarrativeHash(narrative)
741
+ const read = readDeckPlanArtifact(workspaceRoot, { narrativeHash })
711
742
  const confirmed = confirmDeckPlan(state, {
712
743
  approvedBy: "user",
713
- note: args.approvalNote,
744
+ note: args.approvalNote ?? "Deprecated confirmDeckPlan compatibility confirmation; deck-plan/ diagnostics are advisory.",
745
+ planHash: read.planHash,
714
746
  })
715
747
  if (confirmed.result.confirmed) {
716
748
  recordWorkspaceAction(confirmed.state, {
@@ -723,18 +755,18 @@ export default tool({
723
755
  planHash: confirmed.result.planHash,
724
756
  },
725
757
  status: "success",
726
- summary: args.approvalNote?.trim() || "User confirmed the compiled deck plan.",
758
+ summary: args.approvalNote?.trim() || "Recorded deck-plan compatibility confirmation; deck-plan/ remains user-directed projection state.",
727
759
  nodeIds: [confirmed.state.narrative?.id, confirmed.result.slug ? `deck:${confirmed.result.slug}` : undefined].filter((item): item is string => Boolean(item)),
728
760
  })
729
761
  }
730
- writeDecksState(workspaceRoot, confirmed.state)
731
- return JSON.stringify({ ok: confirmed.result.confirmed, path: DECKS_STATE_FILE, result: confirmed.result, deck: confirmed.state.activeDeck ? confirmed.state.decks[confirmed.state.activeDeck] : undefined }, null, 2)
762
+ const persisted = persistState(confirmed.state)
763
+ return JSON.stringify({ ok: confirmed.result.confirmed, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: confirmed.result, planArtifact: read, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
732
764
  }
733
765
 
734
766
  if (args.action === "backfillClaimRefs") {
735
767
  const backfilled = backfillSlideClaimRefsFromCoverage(state)
736
- writeDecksState(workspaceRoot, backfilled.state)
737
- 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)
768
+ const persisted = persistState(backfilled.state)
769
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: backfilled.result, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined, narrative: state.narrative }, null, 2)
738
770
  }
739
771
 
740
772
  if (args.action === "reviewNarrative") {
@@ -742,8 +774,8 @@ export default tool({
742
774
  if (qaGate) return JSON.stringify(qaGate, null, 2)
743
775
  const reviewed = reviewNarrativeState(state)
744
776
  recordNarrativeReviewAction(reviewed.state, reviewed.result)
745
- writeDecksState(workspaceRoot, reviewed.state)
746
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result, narrative: reviewed.state.narrative }, null, 2)
777
+ const persisted = persistState(reviewed.state)
778
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: reviewed.result, narrative: state.narrative }, null, 2)
747
779
  }
748
780
 
749
781
  if (args.action === "approveNarrative") {
@@ -755,15 +787,15 @@ export default tool({
755
787
  note: args.approvalNote,
756
788
  })
757
789
  recordNarrativeApprovalAction(approved.state, approved.result)
758
- writeDecksState(workspaceRoot, approved.state)
759
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: approved.result, narrative: approved.state.narrative }, null, 2)
790
+ const persisted = persistState(approved.state)
791
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: approved.result, narrative: state.narrative }, null, 2)
760
792
  }
761
793
 
762
794
  if (args.action === "deriveResearchGaps") {
763
795
  if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("deriveResearchGaps", "deriveResearchTargets for read-only target selection, then upsertVaultResearchGap for canonical gap nodes"), authoringContract: narrativeVaultAuthoringContract() })
764
796
  const derived = deriveResearchGapsFromReadiness(state)
765
- writeDecksState(workspaceRoot, derived.state)
766
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: derived.result, narrative: derived.state.narrative }, null, 2)
797
+ const persisted = persistState(derived.state)
798
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: derived.result, narrative: state.narrative }, null, 2)
767
799
  }
768
800
 
769
801
  if (args.action === "deriveResearchTargets") {
@@ -785,8 +817,8 @@ export default tool({
785
817
  if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("upsertResearchGaps", "upsertVaultResearchGap for new or updated research gap nodes"), authoringContract: narrativeVaultAuthoringContract() })
786
818
  if (!args.researchGaps?.length) return JSON.stringify({ ok: false, error: "researchGaps are required for upsertResearchGaps" })
787
819
  const upserted = upsertResearchGapsInState(state, args.researchGaps as any[])
788
- writeDecksState(workspaceRoot, upserted.state)
789
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: upserted.result, narrative: upserted.state.narrative }, null, 2)
820
+ const persisted = persistState(upserted.state)
821
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: upserted.result, narrative: state.narrative }, null, 2)
790
822
  }
791
823
 
792
824
  if (args.action === "updateResearchGap") {
@@ -799,16 +831,16 @@ export default tool({
799
831
  evidenceBindingIds: args.evidenceBindingIds,
800
832
  notes: args.gapNotes,
801
833
  })
802
- writeDecksState(workspaceRoot, updated.state)
803
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: updated.result, narrative: updated.state.narrative }, null, 2)
834
+ const persisted = persistState(updated.state)
835
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: updated.result, narrative: state.narrative }, null, 2)
804
836
  }
805
837
 
806
838
  if (args.action === "closeResearchGap") {
807
839
  if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("closeResearchGap", "updateVaultResearchGap with gapStatus=closed"), authoringContract: narrativeVaultAuthoringContract() })
808
840
  if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for closeResearchGap" })
809
841
  const closed = closeResearchGapInState(state, args.gapId, args.gapNotes)
810
- writeDecksState(workspaceRoot, closed.state)
811
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: closed.result, narrative: closed.state.narrative }, null, 2)
842
+ const persisted = persistState(closed.state)
843
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, result: closed.result, narrative: state.narrative }, null, 2)
812
844
  }
813
845
 
814
846
  if (args.action === "applyEvidenceCandidates") {
@@ -835,8 +867,8 @@ export default tool({
835
867
  const preferenceType = args.preferenceType ?? "user"
836
868
  const list = state.workspace.preferences[preferenceType]
837
869
  if (!list.some((entry) => entry.trim().toLowerCase() === memory.toLowerCase())) list.push(memory)
838
- writeDecksState(workspaceRoot, state)
839
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, preferenceType, memory }, null, 2)
870
+ const persisted = persistState(state)
871
+ return JSON.stringify({ ok: true, path: persisted ? DECKS_STATE_FILE : undefined, persisted, preferenceType, memory }, null, 2)
840
872
  }
841
873
 
842
874
  return JSON.stringify({ ok: false, error: `Unsupported action: ${args.action}` })
@@ -1,14 +1,12 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
- import { hasDecksState, readDecksState } from "../lib/decks-state"
3
- import { buildNarrativeMap } from "../lib/narrative-state/map"
4
2
  import { validateNarrativeDisplayModel, type NarrativeDisplayModel, type NarrativeViewLanguage } from "../lib/narrative-state/display"
5
- import { writeNarrativeMapHtml } from "../lib/commands/narrative"
3
+ import { loadStoryMap, writeNarrativeMapHtml } from "../lib/commands/narrative"
6
4
  import { openUrl } from "../lib/edit/open"
7
5
 
8
6
  export default tool({
9
7
  description:
10
8
  "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.",
9
+ "This tool validates display IDs against the file-native narrative vault, opens a local HTML view, and never mutates workspace state.",
12
10
  args: {
13
11
  language: tool.schema.string().describe("UI language request from /revela story or /revela narrative. May be any language tag or language name, such as en, zh-CN, fr, de, Korean, Arabic, or Portuguese-BR."),
14
12
  narrativeHash: tool.schema.string().optional().describe("Narrative hash from the prompt projection. Used to detect stale display prompts."),
@@ -79,11 +77,10 @@ export default tool({
79
77
  async execute(args, context) {
80
78
  const workspaceRoot = context.directory ?? process.cwd()
81
79
  try {
82
- if (!hasDecksState(workspaceRoot)) {
83
- return JSON.stringify({ ok: false, error: "No DECKS.json found. Run /revela init first." })
84
- }
85
80
  const language = args.language as NarrativeViewLanguage
86
- const map = buildNarrativeMap(readDecksState(workspaceRoot))
81
+ const loaded = loadStoryMap(workspaceRoot)
82
+ if (!loaded.ok) return JSON.stringify({ ok: false, error: loaded.error, diagnostics: loaded.diagnosticsReport, diagnosticsMarkdown: loaded.diagnosticsMarkdown })
83
+ const map = loaded.map
87
84
  const stalePrompt = Boolean(args.narrativeHash && args.narrativeHash !== map.snapshot.narrativeHash)
88
85
  const display = validateNarrativeDisplayModel(map, args.displayModel as NarrativeDisplayModel | undefined, language)
89
86
  const htmlPath = writeNarrativeMapHtml(map, display)
@@ -92,8 +89,9 @@ export default tool({
92
89
  return JSON.stringify({ ok: true, url, path: htmlPath, narrativeHash: map.snapshot.narrativeHash, stalePrompt, fallback: false }, null, 2)
93
90
  } catch (e: any) {
94
91
  try {
95
- const language = (args.language ?? "en") as NarrativeViewLanguage
96
- const map = buildNarrativeMap(readDecksState(workspaceRoot))
92
+ const loaded = loadStoryMap(workspaceRoot)
93
+ if (!loaded.ok) return JSON.stringify({ ok: false, fallback: false, error: e.message || String(e), fallbackError: loaded.error, diagnostics: loaded.diagnosticsReport, diagnosticsMarkdown: loaded.diagnosticsMarkdown })
94
+ const map = loaded.map
97
95
  const htmlPath = writeNarrativeMapHtml(map)
98
96
  const url = `file://${htmlPath}`
99
97
  openUrl(url)