@cyber-dash-tech/revela 0.16.4 → 0.17.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 (47) hide show
  1. package/README.md +7 -5
  2. package/README.zh-CN.md +7 -5
  3. package/lib/commands/brief.ts +9 -0
  4. package/lib/commands/help.ts +5 -2
  5. package/lib/commands/init.ts +42 -27
  6. package/lib/commands/narrative.ts +26 -2
  7. package/lib/commands/research.ts +36 -20
  8. package/lib/commands/review.ts +21 -18
  9. package/lib/ctx.ts +1 -1
  10. package/lib/decks-state.ts +38 -4
  11. package/lib/edit/prompt.ts +1 -1
  12. package/lib/hook-notifications.ts +53 -0
  13. package/lib/narrative-state/render-plan.ts +114 -27
  14. package/lib/narrative-state/research-binding-eval.ts +260 -0
  15. package/lib/narrative-state/research-gaps.ts +2 -88
  16. package/lib/narrative-vault/authoring-contract.ts +127 -0
  17. package/lib/narrative-vault/authoring-guard.ts +122 -0
  18. package/lib/narrative-vault/auto-compile.ts +134 -0
  19. package/lib/narrative-vault/bootstrap.ts +63 -0
  20. package/lib/narrative-vault/cache.ts +14 -0
  21. package/lib/narrative-vault/compile-mirror.ts +45 -0
  22. package/lib/narrative-vault/compile.ts +350 -0
  23. package/lib/narrative-vault/constants.ts +6 -0
  24. package/lib/narrative-vault/diagnostic-report.ts +117 -0
  25. package/lib/narrative-vault/export.ts +71 -0
  26. package/lib/narrative-vault/frontmatter.ts +41 -0
  27. package/lib/narrative-vault/hook-targets.ts +40 -0
  28. package/lib/narrative-vault/index.ts +18 -0
  29. package/lib/narrative-vault/inventory.ts +392 -0
  30. package/lib/narrative-vault/markdown-qa.ts +237 -0
  31. package/lib/narrative-vault/markdown.ts +34 -0
  32. package/lib/narrative-vault/migration.ts +52 -0
  33. package/lib/narrative-vault/mutate.ts +361 -0
  34. package/lib/narrative-vault/paths.ts +19 -0
  35. package/lib/narrative-vault/read.ts +52 -0
  36. package/lib/narrative-vault/relations.ts +32 -0
  37. package/lib/narrative-vault/source-loader.ts +19 -0
  38. package/lib/narrative-vault/timestamp.ts +32 -0
  39. package/lib/narrative-vault/types.ts +44 -0
  40. package/lib/source-materials.ts +98 -0
  41. package/lib/tool-result.ts +34 -0
  42. package/package.json +2 -2
  43. package/plugin.ts +60 -22
  44. package/skill/NARRATIVE_SKILL.md +25 -10
  45. package/tools/decks.ts +363 -67
  46. package/tools/research-save.ts +3 -0
  47. package/tools/workspace-scan.ts +1 -0
@@ -46,6 +46,103 @@ export function sourceMaterialMetadata(filePath: string, workspaceRoot: string):
46
46
  type: sourceMaterialType(resolvedFile),
47
47
  size: stat.size,
48
48
  fingerprint: computeSourceFingerprint(resolvedFile),
49
+ lastModified: new Date(stat.mtimeMs).toISOString(),
50
+ }
51
+ }
52
+
53
+ export function sourceMaterialModifiedMs(material: SourceMaterial, workspaceRoot: string): number {
54
+ if (material.lastModified) {
55
+ const parsed = Date.parse(material.lastModified)
56
+ if (Number.isFinite(parsed)) return parsed
57
+ }
58
+
59
+ try {
60
+ return statSync(ensureWorkspaceFile(material.path, workspaceRoot)).mtimeMs
61
+ } catch {
62
+ return 0
63
+ }
64
+ }
65
+
66
+ export interface SourceMaterialIngestPlan {
67
+ vaultTimestamp: string | null
68
+ vaultTimestampMs: number
69
+ addedSourceMaterials: SourceMaterial[]
70
+ changedSourceMaterials: SourceMaterial[]
71
+ newerThanVaultSourceMaterials: SourceMaterial[]
72
+ unchangedSourceMaterials: SourceMaterial[]
73
+ ingestCandidates: SourceMaterial[]
74
+ suggestedTasks: SourceMaterialIngestTask[]
75
+ }
76
+
77
+ export interface SourceMaterialIngestTask {
78
+ path: string
79
+ reason: Array<"added" | "changed" | "newer_than_vault">
80
+ materialType: string
81
+ needsExtraction: boolean
82
+ suggestedAction: "read_directly" | "extract_then_read"
83
+ note: string
84
+ }
85
+
86
+ export function classifySourceMaterialIngest(
87
+ state: DecksState,
88
+ materials: SourceMaterial[],
89
+ workspaceRoot: string,
90
+ vaultTimestampMs: number,
91
+ ): SourceMaterialIngestPlan {
92
+ const existingByPath = new Map((state.workspace.sourceMaterials ?? []).map((item) => [item.path.replace(/\\/g, "/"), item]))
93
+ const addedSourceMaterials: SourceMaterial[] = []
94
+ const changedSourceMaterials: SourceMaterial[] = []
95
+ const newerThanVaultSourceMaterials: SourceMaterial[] = []
96
+ const unchangedSourceMaterials: SourceMaterial[] = []
97
+ const ingestByPath = new Map<string, SourceMaterial>()
98
+ const reasonsByPath = new Map<string, SourceMaterialIngestTask["reason"]>()
99
+
100
+ for (const material of materials) {
101
+ const path = material.path.replace(/\\/g, "/")
102
+ const existing = existingByPath.get(path)
103
+ const added = !existing
104
+ const changed = Boolean(existing?.fingerprint && material.fingerprint && existing.fingerprint !== material.fingerprint)
105
+ const newerThanVault = sourceMaterialModifiedMs(material, workspaceRoot) > vaultTimestampMs
106
+ const reasons: SourceMaterialIngestTask["reason"] = []
107
+
108
+ if (added) addedSourceMaterials.push(material)
109
+ if (changed) changedSourceMaterials.push(material)
110
+ if (newerThanVault) newerThanVaultSourceMaterials.push(material)
111
+ if (added) reasons.push("added")
112
+ if (changed) reasons.push("changed")
113
+ if (newerThanVault) reasons.push("newer_than_vault")
114
+ if (added || changed || newerThanVault) {
115
+ ingestByPath.set(path, material)
116
+ reasonsByPath.set(path, reasons)
117
+ }
118
+ else unchangedSourceMaterials.push(material)
119
+ }
120
+ const ingestCandidates = [...ingestByPath.values()].sort((a, b) => a.path.localeCompare(b.path))
121
+
122
+ return {
123
+ vaultTimestamp: vaultTimestampMs > 0 ? new Date(vaultTimestampMs).toISOString() : null,
124
+ vaultTimestampMs,
125
+ addedSourceMaterials,
126
+ changedSourceMaterials,
127
+ newerThanVaultSourceMaterials,
128
+ unchangedSourceMaterials,
129
+ ingestCandidates,
130
+ suggestedTasks: ingestCandidates.map((material) => sourceMaterialIngestTask(material, reasonsByPath.get(material.path.replace(/\\/g, "/")) ?? [])),
131
+ }
132
+ }
133
+
134
+ function sourceMaterialIngestTask(material: SourceMaterial, reason: SourceMaterialIngestTask["reason"]): SourceMaterialIngestTask {
135
+ const materialType = (material.type || sourceMaterialType(material.path)).toLowerCase()
136
+ const needsExtraction = ["pdf", "ppt", "pptx", "doc", "docx", "xls", "xlsx"].includes(materialType)
137
+ return {
138
+ path: material.path,
139
+ reason,
140
+ materialType,
141
+ needsExtraction,
142
+ suggestedAction: needsExtraction ? "extract_then_read" : "read_directly",
143
+ note: needsExtraction
144
+ ? "Use revela-extract-document-materials before synthesizing stable narrative findings when this file is relevant."
145
+ : "Read directly when relevant and distill stable narrative findings into the Markdown vault.",
49
146
  }
50
147
  }
51
148
 
@@ -77,6 +174,7 @@ export function upsertSourceMaterial(
77
174
  path,
78
175
  type: material.type ?? existing?.type,
79
176
  status: nextStatus,
177
+ lastModified: material.lastModified ?? existing?.lastModified,
80
178
  firstSeen: existing?.firstSeen ?? material.firstSeen ?? now,
81
179
  lastChecked: now,
82
180
  }
@@ -0,0 +1,34 @@
1
+ export function appendToolResult(output: any, text: string): void {
2
+ if (!output || typeof output !== "object") return
3
+
4
+ if (typeof output.output === "string") {
5
+ output.output = appendText(output.output, text)
6
+ return
7
+ }
8
+
9
+ if (typeof output.result === "string") {
10
+ output.result = appendText(output.result, text)
11
+ return
12
+ }
13
+
14
+ if (typeof output.text === "string") {
15
+ output.text = appendText(output.text, text)
16
+ return
17
+ }
18
+
19
+ if (typeof output.message === "string") {
20
+ output.message = appendText(output.message, text)
21
+ return
22
+ }
23
+
24
+ if (Array.isArray(output.content)) {
25
+ output.content.push({ type: "text", text })
26
+ return
27
+ }
28
+
29
+ output.output = text
30
+ }
31
+
32
+ function appendText(existing: string, text: string): string {
33
+ return (existing ? `${existing}\n\n` : "") + text
34
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.16.4",
4
- "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
3
+ "version": "0.17.0",
4
+ "description": "OpenCode plugin for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
7
7
  "exports": {
package/plugin.ts CHANGED
@@ -72,6 +72,11 @@ import {
72
72
  hasDecksState,
73
73
  isDecksStatePath,
74
74
  } from "./lib/decks-state"
75
+ import { autoCompileNarrativeVault } from "./lib/narrative-vault/auto-compile"
76
+ import {
77
+ extractNarrativeVaultMarkdownTargetsFromPatch,
78
+ normalizeNarrativeVaultMarkdownPath,
79
+ } from "./lib/narrative-vault/hook-targets"
75
80
  import decksTool from "./tools/decks"
76
81
  import designsAuthorTool from "./tools/designs-author"
77
82
  import designsTool from "./tools/designs"
@@ -93,6 +98,8 @@ import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research
93
98
  import { NARRATIVE_REVIEWER_PROMPT, NARRATIVE_REVIEWER_SIGNATURE } from "./lib/agents/narrative-reviewer-prompt"
94
99
  import { extractDesignClasses } from "./lib/design/designs"
95
100
  import { log, childLog } from "./lib/log"
101
+ import { appendToolResult } from "./lib/tool-result"
102
+ import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice, formatStateGateUserNotice } from "./lib/hook-notifications"
96
103
 
97
104
  // OpenCode internal agent signatures — used to skip system prompt injection
98
105
  // for built-in system agents (title, summary, compaction).
@@ -102,16 +109,6 @@ const INTERNAL_AGENT_SIGNATURES = [
102
109
  "Summarize what was done in this conversation",
103
110
  ]
104
111
 
105
- function appendToolResult(output: any, text: string): void {
106
- if (typeof output.output === "string") {
107
- output.output = (output.output ? output.output + "\n\n" : "") + text
108
- return
109
- }
110
-
111
- const existing = output.result ?? ""
112
- output.result = (existing ? existing + "\n\n" : "") + text
113
- }
114
-
115
112
  function extractEditFilePath(args: any): string {
116
113
  return args?.filePath ?? args?.file_path ?? args?.path ?? args?.file ?? ""
117
114
  }
@@ -148,7 +145,7 @@ const server: Plugin = (async (pluginCtx) => {
148
145
  const blockedDeckWrites = new Map<string, string>()
149
146
  const blockedPatches = new Map<string, string>()
150
147
 
151
- async function runPostWriteArtifactQA(filePath: string, output: any): Promise<boolean> {
148
+ async function runPostWriteArtifactQA(filePath: string, output: any, sessionID = ""): Promise<boolean> {
152
149
  if (!isDeckHtmlPath(filePath)) return true
153
150
 
154
151
  try {
@@ -161,6 +158,8 @@ const server: Plugin = (async (pluginCtx) => {
161
158
 
162
159
  const report = await runArtifactQA({ workspaceRoot, filePath, vocabulary })
163
160
  appendToolResult(output, "---\n\n" + formatArtifactQAReport(report))
161
+ const notice = formatArtifactQaUserNotice(report)
162
+ if (notice && sessionID) await sendIgnoredMessage(client, sessionID, notice)
164
163
  return report.passed
165
164
  } catch (e) {
166
165
  childLog("artifact-qa").warn("post-write artifact QA failed", {
@@ -168,10 +167,27 @@ const server: Plugin = (async (pluginCtx) => {
168
167
  error: e instanceof Error ? e.message : String(e),
169
168
  })
170
169
  appendToolResult(output, "---\n\n## Artifact QA: FAILED\n\nError running artifact QA: " + (e instanceof Error ? e.message : String(e)))
170
+ if (sessionID) await sendIgnoredMessage(client, sessionID, `**Artifact QA failed**\nFile: \`${filePath}\`\nError: ${e instanceof Error ? e.message : String(e)}`)
171
171
  return false
172
172
  }
173
173
  }
174
174
 
175
+ async function runPostWriteNarrativeVaultCompile(touched: string[], output: any, sessionID = ""): Promise<void> {
176
+ if (touched.length === 0) return
177
+
178
+ const result = autoCompileNarrativeVault(workspaceRoot, touched)
179
+ appendToolResult(output, "---\n\n" + result.markdown)
180
+ const notice = formatMarkdownQaUserNotice(result)
181
+ if (notice && sessionID) await sendIgnoredMessage(client, sessionID, notice)
182
+ if (!result.ok) {
183
+ childLog("narrative-vault").warn("auto-compile reported blockers", {
184
+ touched,
185
+ mirror: result.mirrored,
186
+ error: result.error,
187
+ })
188
+ }
189
+ }
190
+
175
191
  function extractSessionID(input: any): string {
176
192
  return input?.sessionID ?? input?.session?.id ?? input?.context?.sessionID ?? ""
177
193
  }
@@ -314,6 +330,17 @@ const server: Plugin = (async (pluginCtx) => {
314
330
  await handleHelp(send)
315
331
  throw new Error("__REVELA_STATUS_HANDLED__")
316
332
  }
333
+ if (sub === "enable") {
334
+ ctx.enabled = true
335
+ buildPrompt()
336
+ await send("Revela enabled. Workflow commands and Revela context are active.")
337
+ throw new Error("__REVELA_ENABLE_HANDLED__")
338
+ }
339
+ if (sub === "disable") {
340
+ ctx.enabled = false
341
+ await send("Revela disabled. Run `/revela enable` or any workflow command to reactivate.")
342
+ throw new Error("__REVELA_DISABLE_HANDLED__")
343
+ }
317
344
  if (sub === "make") {
318
345
  const target = args[1]?.toLowerCase() ?? ""
319
346
  const makeParam = args.slice(2).join(" ")
@@ -500,7 +527,7 @@ const server: Plugin = (async (pluginCtx) => {
500
527
  throw new Error("__REVELA_DOMAIN_USAGE_HANDLED__")
501
528
  }
502
529
  const legacyCommands = new Set([
503
- "enable", "disable", "review", "narrative", "deck", "brief", "edit", "inspect", "remember",
530
+ "review", "narrative", "deck", "brief", "edit", "inspect", "remember",
504
531
  "designs", "designs-new", "designs-edit", "designs-preview", "designs-add", "designs-rm",
505
532
  "domains", "domains-add", "domains-rm", "pdf", "pptx",
506
533
  ])
@@ -689,8 +716,6 @@ const server: Plugin = (async (pluginCtx) => {
689
716
  // - write/apply_patch: protect DECKS.json, but do not block deck HTML edits.
690
717
  "tool.execute.before": async (input, output) => {
691
718
  log.info("[hook] tool.execute.before fired", { tool: input.tool, enabled: ctx.enabled, isResearch: ctx.isResearchAgent })
692
- if (!ctx.enabled) return
693
-
694
719
  if (input.tool === "write") {
695
720
  const filePath: string = (output.args as any)?.filePath ?? ""
696
721
  if (isDecksStatePath(filePath)) {
@@ -741,6 +766,8 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
741
766
  }
742
767
  }
743
768
 
769
+ if (!ctx.enabled) return
770
+
744
771
  if (input.tool === "read") {
745
772
  try {
746
773
  await preRead(output.args)
@@ -760,10 +787,9 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
760
787
  // Also reports writes/patches blocked by the DECKS.json prewrite gate and
761
788
  // runs artifact QA before opening Refine after successful deck changes.
762
789
  "tool.execute.after": async (input, output) => {
763
- if (!ctx.enabled) return
764
-
765
790
  // ── Post-read processing ───────────────────────────────────────────
766
791
  if (input.tool === "read") {
792
+ if (!ctx.enabled) return
767
793
  try {
768
794
  await postRead(input.args, output)
769
795
  } catch (e) {
@@ -787,39 +813,51 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
787
813
  `${blockedReason}\n\n` +
788
814
  "Use the `revela-decks` tool or complete the DECKS.json review workflow instead."
789
815
  )
816
+ await sendIgnoredMessage(client, extractSessionID(input), formatStateGateUserNotice("write", blockedReason))
790
817
  return
791
818
  }
792
- const qaPassed = await runPostWriteArtifactQA(filePath, output)
819
+ const vaultTarget = normalizeNarrativeVaultMarkdownPath(filePath, workspaceRoot)
820
+ const sessionID = extractSessionID(input)
821
+ if (vaultTarget) await runPostWriteNarrativeVaultCompile([vaultTarget], output, sessionID)
822
+ const qaPassed = await runPostWriteArtifactQA(filePath, output, sessionID)
793
823
  if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
794
824
  return
795
825
  }
796
826
 
797
827
  if (input.tool === "apply_patch" && blockedPatches.size > 0) {
798
828
  const [blockedPath, blockedReason] = blockedPatches.entries().next().value ?? []
829
+ if (!blockedPath || !blockedReason) return
799
830
  if (blockedPath) blockedPatches.delete(blockedPath)
800
831
  appendToolResult(
801
832
  output,
802
833
  "---\n\n**[revela prewrite gate]** Patch was blocked.\n\n" +
803
834
  `${blockedReason}\n\n` +
804
- "Use the `revela-decks` tool for controlled workspace state changes."
835
+ "Use the `revela-decks` tool for controlled workspace state changes."
805
836
  )
837
+ await sendIgnoredMessage(client, extractSessionID(input), formatStateGateUserNotice("patch", blockedReason))
806
838
  return
807
839
  }
808
840
 
809
841
  if (input.tool === "apply_patch") {
810
842
  const patchText = extractPatchTextArg(input.args as Record<string, unknown>)
843
+ const vaultTargets = patchText ? extractNarrativeVaultMarkdownTargetsFromPatch(patchText, workspaceRoot) : []
844
+ const sessionID = extractSessionID(input)
845
+ await runPostWriteNarrativeVaultCompile(vaultTargets, output, sessionID)
811
846
  const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
812
847
  for (const target of targets) {
813
- const qaPassed = await runPostWriteArtifactQA(target, output)
814
- if (qaPassed) ensureRefineOpenAfterDeckChange(target, extractSessionID(input))
848
+ const qaPassed = await runPostWriteArtifactQA(target, output, sessionID)
849
+ if (qaPassed) ensureRefineOpenAfterDeckChange(target, sessionID)
815
850
  }
816
851
  return
817
852
  }
818
853
 
819
854
  if (input.tool === "edit") {
820
855
  const filePath = extractEditFilePath(input.args)
821
- const qaPassed = await runPostWriteArtifactQA(filePath, output)
822
- if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
856
+ const vaultTarget = normalizeNarrativeVaultMarkdownPath(filePath, workspaceRoot)
857
+ const sessionID = extractSessionID(input)
858
+ if (vaultTarget) await runPostWriteNarrativeVaultCompile([vaultTarget], output, sessionID)
859
+ const qaPassed = await runPostWriteArtifactQA(filePath, output, sessionID)
860
+ if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, sessionID)
823
861
  return
824
862
  }
825
863
  },
@@ -8,7 +8,7 @@ compatibility: opencode
8
8
 
9
9
  You help the user turn source materials, research, data, and intent into trusted, traceable, presentation-ready decision artifacts.
10
10
 
11
- Decks are important, but they are render targets. The durable source of truth is the canonical narrative state: audience, decision, thesis, claims, evidence boundaries, objections, risks, research gaps, approval provenance, and artifact coverage.
11
+ Decks are important, but they are render targets. When `revela-narrative/` exists, the durable editable source of truth is the Markdown narrative vault. Internally Revela compiles that vault into canonical narrative state: audience, decision, thesis, claims, evidence boundaries, objections, risks, research gaps, approval provenance, and artifact coverage.
12
12
 
13
13
  Default mode is narrative-first. Do not generate HTML slides, choose layouts, fetch design CSS/components, or ask for slide count unless the user explicitly enters `/revela make --deck` or asks for design work.
14
14
 
@@ -16,7 +16,7 @@ Default mode is narrative-first. Do not generate HTML slides, choose layouts, fe
16
16
 
17
17
  Use the same phase semantics whether the user invokes a slash command or asks in normal chat:
18
18
 
19
- - `Init` discovers local workspace materials, captures intent, initializes or refreshes `DECKS.json`, and creates conservative narrative state only from explicit user statements or source traces.
19
+ - `Init` discovers local workspace materials, captures intent, initializes or refreshes workspace state, and creates conservative narrative state only from explicit user statements or source traces.
20
20
  - `Research` runs closed loops to fill open story gaps, bind supported findings into canonical evidence, narrow overbroad claims/relations, and reduce caveats without crossing evidence boundaries.
21
21
  - `Story` opens the read-only story workspace UI for inspecting claim flow, evidence strength, unsupported scope, caveats, objections, risks, research gaps, approval state, and affected artifacts.
22
22
  - `Make` renders an artifact from approved or explicitly overridden narrative state. Supported 0.15 targets are deck and executive brief.
@@ -39,32 +39,43 @@ Deprecated compatibility aliases such as `/revela review`, `/revela narrative`,
39
39
 
40
40
  ## Workspace State
41
41
 
42
- Use `DECKS.json` as Revela's current compatibility workspace-state file. Do not write or patch it directly.
42
+ Use `DECKS.json` as Revela's compatibility workspace-state and render-state file. Do not write or patch it directly. Use `revela-narrative/**/*.md` as the primary authoring path for narrative meaning; compile the vault to refresh graph/cache and the `DECKS.json.narrative` mirror.
43
43
 
44
44
  Use `revela-decks` for state operations:
45
45
 
46
46
  - `read` to inspect current workspace state
47
+ - `read` with `summary: true` to inspect compact deck state plus `vaultDiagnostics` when a Markdown vault exists and `migration` guidance when JSON narrative state can be exported to a vault
47
48
  - `init` to register discovered source material candidates during workspace initialization
48
- - `upsertNarrative` to preserve canonical audience, decision, thesis, claims, evidence bindings, objections, risks, and research gaps
49
+ - `initNarrativeVault` to create `revela-narrative/` and draft core Markdown files before writing canonical narrative meaning in a new workspace
50
+ - `exportNarrativeVault` to export existing `DECKS.json.narrative` into `revela-narrative/` when no vault exists; its result states which Markdown files were written and which provenance/render fields remain in `DECKS.json`
51
+ - `compileNarrativeVault` to compile Markdown vault nodes, refresh cache, and mirror compiled narrative into `DECKS.json`
52
+ - `updateVaultCoreNarrative`, `upsertVaultClaim`, `upsertVaultEvidence`, `upsertVaultObjection`, `upsertVaultRisk`, and `updateVaultResearchGap` are deterministic helper actions for common Markdown vault edits; prefer direct Markdown node edits when you can preserve surrounding content safely
53
+ - `upsertNarrative` is deprecated and should not be used; create or update canonical narrative meaning through Markdown vault files, then compile
49
54
  - `reviewNarrative` to run deterministic story readiness
50
- - `deriveResearchGaps`, `upsertResearchGaps`, `updateResearchGap`, and `closeResearchGap` to manage research gap lifecycle
55
+ - `deriveResearchGaps`, `upsertResearchGaps`, `updateResearchGap`, and `closeResearchGap` are compatibility helpers; prefer `updateVaultResearchGap` for canonical gap lifecycle updates
51
56
  - `attachResearchFindings` to attach saved findings to research state
52
- - `applyEvidenceCandidates` only when selected candidates should become canonical support
57
+ - `applyEvidenceCandidates` is compatibility-only; prefer `upsertVaultEvidence` with explicit source trace for canonical support
53
58
  - `approveNarrative` only when the user explicitly approves or requests an override
54
59
  - `compileDeckPlan`, `upsertDeck`, `upsertSlides`, and `review` only inside make-deck or artifact-readiness workflows
55
60
 
56
61
  Never treat `writeReadiness.status`, old review snapshots, existing `decks/*.html`, workspace scans, extraction cache paths, or saved research actions as narrative approval or proof by themselves.
57
62
 
63
+ When a tool returns `vaultDiagnostics` or `diagnosticReport`, report blockers before narrative readiness or artifact work. Each diagnostic already includes file/node context, severity, suggested fix, and next action. Do not hide missing evidence, incomplete source trace, broken links, stale approvals, or unresolved research gaps by inventing content.
64
+
58
65
  ## Init Rules
59
66
 
60
67
  During init:
61
68
 
62
- - scan local workspace materials before asking broad questions
69
+ - if no `revela-narrative/` exists, call `initNarrativeVault` before writing canonical narrative meaning; use `exportNarrativeVault` only for developer workspaces that already have a JSON narrative mirror and no vault
70
+ - scan local workspace materials before asking broad questions, and treat init as repeatable ingest for both first discovery and user-added or user-modified files
71
+ - after `revela-decks init`, inspect `ingest.ingestCandidates`; these include added files, files whose fingerprint changed, and files newer than the vault timestamp, and they must be considered for extraction/reading in this init pass
63
72
  - reuse `workspace.sourceMaterials` and extraction cache when fingerprints match
64
73
  - extract or read only relevant local materials; do not exhaustively process large workspaces
65
- - derive claims, evidence bindings, caveats, unsupported scope, source paths, quotes/snippets, pages, sheets, or slide references only when explicit support exists
74
+ - derive claims, evidence bindings, caveats, unsupported scope, source paths, quotes/snippets, pages, sheets, or slide references only when explicit support exists; distill ingested files by writing Markdown nodes under `revela-narrative/` even when the narrative is incomplete, and represent missing information as research gaps or caveats
75
+ - write `## Relations` sections with plain node-id wikilinks such as `- supports: [[claim-example]]`, `- depends_on: [[evidence-example]]`, `- answers: [[claim-example]]`, or `- constrains: [[claim-example]]` when the relation is explicit; do not use typed wikilinks or hand-written relation ids; compile and fix diagnostics after editing Markdown
66
76
  - ask the smallest missing intent questions after local evidence has been considered
67
77
  - do not require slide count, design choice, layout choice, output path, or visual style unless the user explicitly asks to make an artifact immediately
78
+ - when exporting a vault, say that approvals, render targets, reviews, artifact coverage, actions, deck specs, and source material records remain in `DECKS.json`
68
79
 
69
80
  ## Research Rules
70
81
 
@@ -76,8 +87,9 @@ During research:
76
87
  - delegate external web search to the `revela-research` subagent
77
88
  - save findings through `revela-research-save`
78
89
  - treat `/revela research` as permission to attach findings and bind clearly supported evidence without item-by-item user confirmation
79
- - use `applyEvidenceCandidates` or `upsertNarrative` to create canonical evidence bindings when claim id, quote/snippet, source, support scope, unsupported scope, caveat, and strength are explicit
80
- - narrow overbroad claim scope or relation rationale when the narrower wording preserves strategic meaning and better matches the evidence
90
+ - create canonical evidence bindings only when the supported claim id, quote/snippet, source, support scope, unsupported scope, caveat, and strength are explicit; new evidence should express support with `## Relations` such as `- supports: [[claim-id]]`, while `claimId` remains compatibility fallback
91
+ - compile after Markdown edits, inspect returned `diagnosticReport`, and report remaining blockers or warnings in the research summary
92
+ - narrow overbroad claim scope by editing `revela-narrative/claims/*.md` only when the narrower wording preserves strategic meaning and better matches the evidence; report relation rewrites or strategic claim changes for Story/user confirmation
81
93
  - preserve source path, URL, location/page/sheet/slide, quote/snippet, support scope, unsupported scope, and caveat
82
94
  - keep missing or partial evidence visible instead of filling it with model assumptions; classify remaining caveats as internal-data-needed, not-publicly-researchable, source-quality-limit, or still-open
83
95
 
@@ -85,6 +97,8 @@ During research:
85
97
 
86
98
  When the user invokes `/revela story`, open the read-only story workspace UI. Do not turn that command into a blocking readiness report.
87
99
 
100
+ If a Markdown vault has compile diagnostics, surface the diagnostic summary alongside the Story output without mutating state. Errors should identify the Markdown file/node and the next smallest fix.
101
+
88
102
  When the user explicitly asks for a readiness report, call `revela-decks` action `reviewNarrative` and report the tool result as authoritative.
89
103
 
90
104
  Use this report shape:
@@ -106,6 +120,7 @@ For `/revela make --deck` deck handoff:
106
120
 
107
121
  - switch to deck-render mode through the command workflow
108
122
  - check narrative readiness and current approval before compiling deck specs
123
+ - stop before deck planning when `vaultDiagnostics.blockers` exist; report the Markdown file/node/code and suggested next action
109
124
  - use `compileDeckPlan` as the canonical narrative-to-deck planning path
110
125
  - run the deck/artifact gate with `revela-decks review` before writing HTML
111
126
  - fetch design layouts/components only after narrative handoff is valid