@cyber-dash-tech/revela 0.16.3 → 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.
- package/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/brief.ts +9 -0
- package/lib/commands/help.ts +5 -2
- package/lib/commands/init.ts +42 -27
- package/lib/commands/narrative.ts +26 -2
- package/lib/commands/research.ts +36 -20
- package/lib/commands/review.ts +21 -18
- package/lib/ctx.ts +1 -1
- package/lib/decks-state.ts +38 -4
- package/lib/edit/prompt.ts +1 -1
- package/lib/hook-notifications.ts +53 -0
- package/lib/narrative-state/render-plan.ts +114 -27
- package/lib/narrative-state/research-binding-eval.ts +260 -0
- package/lib/narrative-state/research-gaps.ts +2 -88
- package/lib/narrative-vault/authoring-contract.ts +127 -0
- package/lib/narrative-vault/authoring-guard.ts +122 -0
- package/lib/narrative-vault/auto-compile.ts +134 -0
- package/lib/narrative-vault/bootstrap.ts +63 -0
- package/lib/narrative-vault/cache.ts +14 -0
- package/lib/narrative-vault/compile-mirror.ts +45 -0
- package/lib/narrative-vault/compile.ts +350 -0
- package/lib/narrative-vault/constants.ts +6 -0
- package/lib/narrative-vault/diagnostic-report.ts +117 -0
- package/lib/narrative-vault/export.ts +71 -0
- package/lib/narrative-vault/frontmatter.ts +41 -0
- package/lib/narrative-vault/hook-targets.ts +40 -0
- package/lib/narrative-vault/index.ts +18 -0
- package/lib/narrative-vault/inventory.ts +392 -0
- package/lib/narrative-vault/markdown-qa.ts +237 -0
- package/lib/narrative-vault/markdown.ts +34 -0
- package/lib/narrative-vault/migration.ts +52 -0
- package/lib/narrative-vault/mutate.ts +361 -0
- package/lib/narrative-vault/paths.ts +19 -0
- package/lib/narrative-vault/read.ts +52 -0
- package/lib/narrative-vault/relations.ts +32 -0
- package/lib/narrative-vault/source-loader.ts +19 -0
- package/lib/narrative-vault/timestamp.ts +32 -0
- package/lib/narrative-vault/types.ts +44 -0
- package/lib/refine/server.ts +472 -5
- package/lib/refine/visual-targets.ts +295 -0
- package/lib/source-materials.ts +98 -0
- package/lib/tool-result.ts +34 -0
- package/package.json +2 -2
- package/plugin.ts +60 -22
- package/skill/NARRATIVE_SKILL.md +25 -10
- package/tools/decks.ts +363 -67
- package/tools/research-save.ts +3 -0
- package/tools/workspace-scan.ts +1 -0
package/tools/decks.ts
CHANGED
|
@@ -11,13 +11,14 @@ import {
|
|
|
11
11
|
writeDecksState,
|
|
12
12
|
workspaceDeckSlug,
|
|
13
13
|
type DeckSpec,
|
|
14
|
+
type DecksState,
|
|
14
15
|
type NarrativeBrief,
|
|
15
16
|
type RequiredInputs,
|
|
16
17
|
type ResearchAxis,
|
|
17
18
|
type SourceMaterial,
|
|
18
19
|
type SlideSpec,
|
|
19
20
|
} from "../lib/decks-state"
|
|
20
|
-
import { upsertSourceMaterial } from "../lib/source-materials"
|
|
21
|
+
import { classifySourceMaterialIngest, upsertSourceMaterial } from "../lib/source-materials"
|
|
21
22
|
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
22
23
|
import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
|
|
23
24
|
import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
|
|
@@ -31,33 +32,81 @@ import {
|
|
|
31
32
|
import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
|
|
32
33
|
import { backfillSlideClaimRefsFromCoverage } from "../lib/narrative-state/coverage"
|
|
33
34
|
import { closeResearchGapInState, deriveResearchGapsFromReadiness, deriveResearchTargets, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import
|
|
35
|
+
import { evaluateResearchFindingsBinding } from "../lib/narrative-state/research-binding-eval"
|
|
36
|
+
import { stableEvidenceId } from "../lib/narrative-state/hash"
|
|
37
|
+
import { normalizeNarrativeState } from "../lib/narrative-state/normalize"
|
|
38
|
+
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
|
+
import { compileCacheMirrorNarrativeVault } from "../lib/narrative-vault/compile-mirror"
|
|
37
40
|
|
|
38
|
-
function
|
|
41
|
+
function missingBindableEvidenceFields(input: Record<string, unknown>): string[] {
|
|
42
|
+
const missing: string[] = []
|
|
43
|
+
for (const key of ["id", "claimId", "source", "quote", "supportScope", "unsupportedScope", "caveat", "strength"] as const) {
|
|
44
|
+
if (!String(input[key] ?? "").trim()) missing.push(key)
|
|
45
|
+
}
|
|
46
|
+
if (!String(input.sourcePath ?? "").trim() && !String(input.url ?? "").trim() && !String(input.findingsFile ?? "").trim()) missing.push("sourcePath|url|findingsFile")
|
|
47
|
+
return missing
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function exactResearchGapForBinding(state: DecksState, findingsFile: string, claimId: string) {
|
|
51
|
+
const gaps = state.narrative?.researchGaps ?? []
|
|
52
|
+
const exact = gaps.filter((gap) => gap.targetType === "claim" && gap.targetId === claimId && gap.findingsFile === findingsFile)
|
|
53
|
+
if (exact.length === 1) return exact[0]
|
|
54
|
+
if (exact.length > 1) return undefined
|
|
55
|
+
const byClaim = gaps.filter((gap) => gap.targetType === "claim" && gap.targetId === claimId && !gap.findingsFile)
|
|
56
|
+
return byClaim.length === 1 ? byClaim[0] : undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function forbiddenVaultCompatibilityAction(action: string, replacement: string): string {
|
|
60
|
+
return `${action} is a JSON-era compatibility action and is blocked in vault workspaces. Use ${replacement}, or patch the existing Markdown node only for a small read-before-edit repair.`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function researchGapHelperRecovery(action: "updateVaultResearchGap" | "upsertVaultResearchGap", missingFields?: string[]) {
|
|
64
|
+
const missing = missingFields?.length ? ` Missing fields: ${missingFields.join(", ")}.` : ""
|
|
39
65
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
66
|
+
nextActions: [
|
|
67
|
+
"If the research gap already exists, read the matching revela-narrative/research-gaps/*.md file and patch only the lifecycle fields.",
|
|
68
|
+
"If this is a new research gap, provide id, targetType, targetId, question, status, and priority, or create the Markdown node directly after checking narrativeInventory with a ## Relations depends_on wikilink.",
|
|
69
|
+
"Rerun revela-decks markdownQa and compileNarrativeVault after the repair.",
|
|
70
|
+
],
|
|
71
|
+
examples: {
|
|
72
|
+
tool: {
|
|
73
|
+
action,
|
|
74
|
+
researchGaps: [{ id: "gap-market-size", targetType: "claim", targetId: "claim-market-context", question: "What source supports the market-size claim?", status: "open", priority: "high" }],
|
|
75
|
+
},
|
|
76
|
+
markdown: "---\ntype: research-gap\nid: gap-market-size\ntargetType: claim\ntargetId: claim-market-context\nquestion: What source supports the market-size claim?\nstatus: open\npriority: high\n---\nWhat source supports the market-size claim?\n\n## Relations\n\n- depends_on: [[claim-market-context]]\n",
|
|
47
77
|
},
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
78
|
+
message: `Research gap helper could not complete.${missing}`,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function relationHelperRecovery(action: "upsertVaultRelation" | "removeVaultRelation", missingFields?: string[]) {
|
|
83
|
+
const missing = missingFields?.length ? ` Missing fields: ${missingFields.join(", ")}.` : ""
|
|
84
|
+
return {
|
|
85
|
+
message: `Relation registry helper is disabled in Markdown vault workspaces.${missing}`,
|
|
86
|
+
nextActions: [
|
|
87
|
+
"Run narrativeInventory before adding or removing relation edges; reuse existing node ids exactly.",
|
|
88
|
+
"Patch the source node's ## Relations section with a plain wikilink relation line; do not hand-write relation ids.",
|
|
89
|
+
"Rerun markdownQa and compileNarrativeVault so compiler-generated relation ids and diagnostics refresh.",
|
|
90
|
+
],
|
|
91
|
+
examples: {
|
|
92
|
+
tool: { action, note: "Deprecated compatibility action; edit Markdown instead." },
|
|
93
|
+
markdown: "## Relations\n\n- supports: [[claim-execution-readiness]] - Pilot recommendation is supported by execution framing.\n",
|
|
51
94
|
},
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function strictVaultMarkdownQaGate(workspaceRoot: string, strictness: "readiness" | "render", action: string) {
|
|
99
|
+
if (!hasNarrativeVault(workspaceRoot)) return undefined
|
|
100
|
+
const markdownQa = runNarrativeMarkdownQa(workspaceRoot, { scope: "full", strictness })
|
|
101
|
+
if (markdownQa.ok) return undefined
|
|
102
|
+
return {
|
|
103
|
+
ok: false,
|
|
104
|
+
skipped: true,
|
|
105
|
+
action,
|
|
106
|
+
reason: `Markdown QA ${strictness} blockers must be repaired before ${action}.`,
|
|
107
|
+
markdownQa,
|
|
108
|
+
narrativeInventory: buildNarrativeVaultInventory(workspaceRoot),
|
|
109
|
+
authoringContract: narrativeVaultAuthoringContract(),
|
|
61
110
|
}
|
|
62
111
|
}
|
|
63
112
|
|
|
@@ -68,7 +117,7 @@ export default tool({
|
|
|
68
117
|
"It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
|
|
69
118
|
args: {
|
|
70
119
|
action: tool.schema
|
|
71
|
-
.enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "deriveResearchTargets", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
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"])
|
|
72
121
|
.describe("Action to perform on DECKS.json."),
|
|
73
122
|
summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
|
|
74
123
|
goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
|
|
@@ -127,7 +176,7 @@ export default tool({
|
|
|
127
176
|
})).optional().describe("Canonical claim-to-claim narrative progression relationships. These affect narrative approval hash."),
|
|
128
177
|
evidenceBindings: tool.schema.array(tool.schema.object({
|
|
129
178
|
id: tool.schema.string().optional(),
|
|
130
|
-
claimId: tool.schema.string().describe("
|
|
179
|
+
claimId: tool.schema.string().describe("Compatibility fallback claim id this evidence supports; helpers also write a ## Relations supports wikilink."),
|
|
131
180
|
source: tool.schema.string().describe("Source file, URL, research finding, or material name."),
|
|
132
181
|
sourcePath: tool.schema.string().optional(),
|
|
133
182
|
findingsFile: tool.schema.string().optional(),
|
|
@@ -165,7 +214,7 @@ export default tool({
|
|
|
165
214
|
createdFromIssueType: tool.schema.string().optional(),
|
|
166
215
|
notes: tool.schema.string().optional(),
|
|
167
216
|
})).optional(),
|
|
168
|
-
}).optional().describe("For
|
|
217
|
+
}).optional().describe("For initNarrativeVault and targeted vault actions: canonical narrative fields to write into Markdown nodes. upsertNarrative is deprecated."),
|
|
169
218
|
design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
|
|
170
219
|
domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
|
|
171
220
|
memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
|
|
@@ -249,7 +298,29 @@ export default tool({
|
|
|
249
298
|
notes: tool.schema.string().optional().describe("Implementation notes for this slide."),
|
|
250
299
|
})).optional().describe("For upsertSlides: complete or partial slide specs."),
|
|
251
300
|
candidateIds: tool.schema.array(tool.schema.string()).optional().describe("For applyEvidenceCandidates: candidate IDs returned by revela-decks review to explicitly bind proposed evidenceDraft records into slide evidence."),
|
|
252
|
-
|
|
301
|
+
evidence: tool.schema.object({
|
|
302
|
+
id: tool.schema.string().describe("For upsertVaultEvidence or bindResearchFindings: canonical evidence binding id."),
|
|
303
|
+
claimId: tool.schema.string().describe("For upsertVaultEvidence: canonical claim id this evidence supports."),
|
|
304
|
+
source: tool.schema.string().describe("For upsertVaultEvidence: source name, file, URL, or research finding label."),
|
|
305
|
+
sourcePath: tool.schema.string().optional(),
|
|
306
|
+
findingsFile: tool.schema.string().optional(),
|
|
307
|
+
quote: tool.schema.string().describe("For upsertVaultEvidence: exact quote or snippet supporting the claim."),
|
|
308
|
+
location: tool.schema.string().optional(),
|
|
309
|
+
url: tool.schema.string().optional(),
|
|
310
|
+
caveat: tool.schema.string().describe("For upsertVaultEvidence: evidence limitation that must travel with the claim."),
|
|
311
|
+
supportScope: tool.schema.string().describe("For upsertVaultEvidence: scope explicitly supported by the evidence."),
|
|
312
|
+
unsupportedScope: tool.schema.string().describe("For upsertVaultEvidence: scope not supported by the evidence."),
|
|
313
|
+
strength: tool.schema.enum(["strong", "partial", "weak"]).describe("For upsertVaultEvidence: support strength."),
|
|
314
|
+
}).optional().describe("For upsertVaultEvidence: canonical evidence node to write under revela-narrative/evidence/*.md. For bindResearchFindings, only id is used as an optional override."),
|
|
315
|
+
relation: tool.schema.object({
|
|
316
|
+
id: tool.schema.string().describe("For upsertVaultRelation/removeVaultRelation: stable relation edge id."),
|
|
317
|
+
from: tool.schema.string().optional().describe("For upsertVaultRelation: source node id, e.g. claim:pilot or evidence:pilot."),
|
|
318
|
+
to: tool.schema.string().optional().describe("For upsertVaultRelation: target node id, e.g. claim:execution."),
|
|
319
|
+
type: tool.schema.enum(["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"]).optional().describe("For upsertVaultRelation: canonical relation type."),
|
|
320
|
+
rationale: tool.schema.string().optional().describe("Optional relation rationale. Do not invent; only preserve explicit semantic rationale."),
|
|
321
|
+
}).optional().describe("For deprecated upsertVaultRelation/removeVaultRelation compatibility actions. Inline ## Relations in node Markdown are canonical."),
|
|
322
|
+
relationId: tool.schema.string().optional().describe("For deprecated removeVaultRelation compatibility action."),
|
|
323
|
+
findingsFile: tool.schema.string().optional().describe("For attachResearchFindings, evaluateResearchFindings, or bindResearchFindings: workspace-relative researches/{topic}/{axis}.md findings file."),
|
|
253
324
|
researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
|
|
254
325
|
researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
|
|
255
326
|
approvalNote: tool.schema.string().optional().describe("For approveNarrative or confirmDeckPlan: optional note explaining the approval, override, or deck plan confirmation."),
|
|
@@ -267,10 +338,12 @@ export default tool({
|
|
|
267
338
|
evidenceBindingIds: tool.schema.array(tool.schema.string()).optional(),
|
|
268
339
|
createdFromIssueType: tool.schema.string().optional(),
|
|
269
340
|
notes: tool.schema.string().optional(),
|
|
270
|
-
})).optional().describe("For upsertResearchGaps: explicit canonical research gaps to create or update."),
|
|
341
|
+
})).optional().describe("For upsertVaultResearchGap or legacy upsertResearchGaps: explicit canonical research gaps to create or update."),
|
|
271
342
|
gapStatus: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional().describe("For updateResearchGap: lifecycle status."),
|
|
272
343
|
gapNotes: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: notes or close reason."),
|
|
273
344
|
evidenceBindingIds: tool.schema.array(tool.schema.string()).optional().describe("For updateResearchGap: canonical narrative evidence binding ids associated with the gap."),
|
|
345
|
+
markdownQaScope: tool.schema.enum(["touched", "affected", "full"]).optional().describe("For markdownQa: relation sync scope. Hook usage should stay touched; manual checks usually use full."),
|
|
346
|
+
markdownQaStrictness: tool.schema.enum(["authoring", "readiness", "render"]).optional().describe("For markdownQa: relation sync strictness. Render/readiness can upgrade relation sync gaps to blockers."),
|
|
274
347
|
},
|
|
275
348
|
async execute(args, context) {
|
|
276
349
|
try {
|
|
@@ -280,6 +353,7 @@ export default tool({
|
|
|
280
353
|
|
|
281
354
|
if (args.action === "init") {
|
|
282
355
|
const discovered: SourceMaterial[] = []
|
|
356
|
+
const ingest = classifySourceMaterialIngest(state, (args.sourceMaterials ?? []) as SourceMaterial[], workspaceRoot, narrativeVaultTimestampMs(workspaceRoot))
|
|
283
357
|
for (const material of (args.sourceMaterials ?? []) as SourceMaterial[]) {
|
|
284
358
|
upsertSourceMaterial(state, material, material.status ?? "discovered")
|
|
285
359
|
discovered.push(material)
|
|
@@ -295,18 +369,252 @@ export default tool({
|
|
|
295
369
|
})
|
|
296
370
|
}
|
|
297
371
|
writeDecksState(workspaceRoot, state)
|
|
298
|
-
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, state }, null, 2)
|
|
372
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, ingest, state }, null, 2)
|
|
299
373
|
}
|
|
300
374
|
|
|
301
375
|
if (args.action === "read") {
|
|
302
376
|
const deckKey = state.activeDeck
|
|
303
377
|
if (args.summary) {
|
|
304
378
|
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
305
|
-
|
|
379
|
+
const vaultDiagnostics = hasNarrativeVault(workspaceRoot)
|
|
380
|
+
? formatVaultDiagnosticReport(compileNarrativeVault(workspaceRoot, { fallbackApprovals: state.narrative?.approvals ?? [] }).diagnostics)
|
|
381
|
+
: undefined
|
|
382
|
+
const migration = getNarrativeVaultMigrationHint(workspaceRoot, state)
|
|
383
|
+
const authoringContract = hasNarrativeVault(workspaceRoot) || migration.available ? narrativeVaultAuthoringContract() : undefined
|
|
384
|
+
const narrativeInventory = hasNarrativeVault(workspaceRoot) ? buildNarrativeVaultInventory(workspaceRoot) : undefined
|
|
385
|
+
const markdownQa = hasNarrativeVault(workspaceRoot) ? runNarrativeMarkdownQa(workspaceRoot) : undefined
|
|
386
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, activeDeck: state.activeDeck, deck, vaultDiagnostics, markdownQa, migration, authoringContract, narrativeInventory }, null, 2)
|
|
306
387
|
}
|
|
307
388
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, state }, null, 2)
|
|
308
389
|
}
|
|
309
390
|
|
|
391
|
+
if (args.action === "narrativeInventory" || args.action === "vaultInventory") {
|
|
392
|
+
if (!hasNarrativeVault(workspaceRoot)) {
|
|
393
|
+
const migration = getNarrativeVaultMigrationHint(workspaceRoot, state)
|
|
394
|
+
return JSON.stringify({ ok: false, path: "revela-narrative", error: "narrativeInventory requires revela-narrative/ to exist. Use initNarrativeVault or exportNarrativeVault first.", migration, authoringContract: migration.available ? narrativeVaultAuthoringContract() : undefined }, null, 2)
|
|
395
|
+
}
|
|
396
|
+
const inventory = buildNarrativeVaultInventory(workspaceRoot)
|
|
397
|
+
return JSON.stringify({ ok: inventory.ok, path: inventory.path, narrativeInventory: inventory, authoringContract: narrativeVaultAuthoringContract() }, null, 2)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (args.action === "markdownQa") {
|
|
401
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, path: "revela-narrative", error: "markdownQa requires revela-narrative/ to exist. Use initNarrativeVault or exportNarrativeVault first." }, null, 2)
|
|
402
|
+
const markdownQa = runNarrativeMarkdownQa(workspaceRoot, { scope: args.markdownQaScope ?? "full", strictness: args.markdownQaStrictness ?? "authoring" })
|
|
403
|
+
return JSON.stringify({ ok: markdownQa.ok, path: "revela-narrative", markdownQa, narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (args.action === "compileNarrativeVault") {
|
|
407
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
408
|
+
const { result, diagnosticReport } = compiled
|
|
409
|
+
const markdownQa = hasNarrativeVault(workspaceRoot) ? runNarrativeMarkdownQa(workspaceRoot) : undefined
|
|
410
|
+
const narrativeInventory = hasNarrativeVault(workspaceRoot) ? buildNarrativeVaultInventory(workspaceRoot) : undefined
|
|
411
|
+
return JSON.stringify({ ok: result.ok, path: DECKS_STATE_FILE, result, diagnosticReport, markdownQa, narrativeInventory }, null, 2)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (args.action === "exportNarrativeVault") {
|
|
415
|
+
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
|
+
const narrative = state.narrative ?? normalizeNarrativeState(state)
|
|
417
|
+
const result = exportNarrativeStateToVault(workspaceRoot, narrative)
|
|
418
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state, fallbackApprovals: narrative.approvals })
|
|
419
|
+
return JSON.stringify({
|
|
420
|
+
ok: compiled.result.ok,
|
|
421
|
+
path: "revela-narrative",
|
|
422
|
+
result,
|
|
423
|
+
files: result.files,
|
|
424
|
+
diagnostics: compiled.result.diagnostics,
|
|
425
|
+
diagnosticReport: compiled.diagnosticReport,
|
|
426
|
+
migrationNote: "Exported canonical narrative nodes to revela-narrative/. Approvals, render targets, reviews, artifact coverage, actions, deck specs, and source material records remain in DECKS.json.",
|
|
427
|
+
preservedInDecksJson: VAULT_MIGRATION_PRESERVED_IN_DECKS_JSON,
|
|
428
|
+
nextActions: [
|
|
429
|
+
"Review diagnosticReport for any source trace, evidence, relation, or approval warnings.",
|
|
430
|
+
"Edit narrative meaning through targeted vault actions or Markdown nodes, then run compileNarrativeVault.",
|
|
431
|
+
"Keep approvals, render targets, reviews, artifact coverage, actions, deck specs, and source material records in DECKS.json.",
|
|
432
|
+
],
|
|
433
|
+
}, null, 2)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (args.action === "initNarrativeVault") {
|
|
437
|
+
if (hasNarrativeVault(workspaceRoot)) {
|
|
438
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
439
|
+
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
|
+
}
|
|
441
|
+
const result = initNarrativeVault(workspaceRoot, {
|
|
442
|
+
id: `narrative:${defaultSlug}`,
|
|
443
|
+
status: args.narrative?.status as any ?? "draft",
|
|
444
|
+
audience: args.narrative?.audience as any,
|
|
445
|
+
decision: args.narrative?.decision as any,
|
|
446
|
+
thesis: args.narrative?.thesis as any,
|
|
447
|
+
})
|
|
448
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
449
|
+
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
|
+
}
|
|
451
|
+
|
|
452
|
+
if (args.action === "upsertVaultEvidence") {
|
|
453
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultEvidence requires revela-narrative/ to exist. Use initNarrativeVault first, then write explicit evidence source trace." })
|
|
454
|
+
if (!args.evidence) return JSON.stringify({ ok: false, error: "evidence is required for upsertVaultEvidence" })
|
|
455
|
+
const mutation = upsertVaultEvidenceNode(workspaceRoot, args.evidence as any)
|
|
456
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
|
|
457
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
458
|
+
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
|
+
}
|
|
460
|
+
|
|
461
|
+
if (args.action === "bindResearchFindings") {
|
|
462
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "bindResearchFindings requires revela-narrative/ to exist. Use initNarrativeVault first, then evaluateResearchFindings." })
|
|
463
|
+
if (!args.findingsFile?.trim()) return JSON.stringify({ ok: false, error: "findingsFile is required for bindResearchFindings" })
|
|
464
|
+
const bindingEval = evaluateResearchFindingsBinding(state, workspaceRoot, args.findingsFile)
|
|
465
|
+
if (bindingEval.status !== "bindable" || !bindingEval.claimId || !bindingEval.recommendedEvidenceDraft) {
|
|
466
|
+
return JSON.stringify({ ok: false, skipped: true, reason: "findings are not safely bindable", bindingEval }, null, 2)
|
|
467
|
+
}
|
|
468
|
+
const draft = bindingEval.recommendedEvidenceDraft
|
|
469
|
+
const evidence = {
|
|
470
|
+
id: args.evidence?.id?.trim() || stableEvidenceId(bindingEval.claimId, `${bindingEval.findingsFile}:${draft.quote ?? ""}`),
|
|
471
|
+
claimId: bindingEval.claimId,
|
|
472
|
+
source: draft.source,
|
|
473
|
+
sourcePath: draft.sourcePath,
|
|
474
|
+
findingsFile: draft.findingsFile ?? bindingEval.findingsFile,
|
|
475
|
+
quote: draft.quote,
|
|
476
|
+
location: draft.location,
|
|
477
|
+
url: draft.url,
|
|
478
|
+
caveat: draft.caveat,
|
|
479
|
+
supportScope: draft.supportScope,
|
|
480
|
+
unsupportedScope: draft.unsupportedScope,
|
|
481
|
+
strength: draft.strength,
|
|
482
|
+
}
|
|
483
|
+
const missing = missingBindableEvidenceFields(evidence)
|
|
484
|
+
if (missing.length > 0) return JSON.stringify({ ok: false, skipped: true, reason: "recommended evidence draft is incomplete", missingFields: missing, bindingEval }, null, 2)
|
|
485
|
+
|
|
486
|
+
const mutation = upsertVaultEvidenceNode(workspaceRoot, evidence as any)
|
|
487
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation, bindingEval }, null, 2)
|
|
488
|
+
const gap = exactResearchGapForBinding(state, bindingEval.findingsFile, bindingEval.claimId)
|
|
489
|
+
const gapMutation = gap
|
|
490
|
+
? updateVaultResearchGapNode(workspaceRoot, {
|
|
491
|
+
id: gap.id,
|
|
492
|
+
status: "evidence_bound",
|
|
493
|
+
findingsFile: bindingEval.findingsFile,
|
|
494
|
+
evidenceBindingIds: [...new Set([...(gap.evidenceBindingIds ?? []), evidence.id])],
|
|
495
|
+
notes: gap.notes,
|
|
496
|
+
})
|
|
497
|
+
: undefined
|
|
498
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
499
|
+
return JSON.stringify({
|
|
500
|
+
ok: compiled.result.ok,
|
|
501
|
+
path: mutation.file,
|
|
502
|
+
bindingEval,
|
|
503
|
+
mutation,
|
|
504
|
+
gapMutation: gapMutation ?? { ok: true, skipped: true, reason: "no exact single research gap matched this findings file and claim" },
|
|
505
|
+
evidence,
|
|
506
|
+
diagnostics: compiled.result.diagnostics,
|
|
507
|
+
diagnosticReport: compiled.diagnosticReport,
|
|
508
|
+
narrative: compiled.result.narrative,
|
|
509
|
+
}, null, 2)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (args.action === "updateVaultResearchGap") {
|
|
513
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "updateVaultResearchGap requires revela-narrative/ to exist. Use updateResearchGap only in compatibility JSON workspaces." })
|
|
514
|
+
if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for updateVaultResearchGap" })
|
|
515
|
+
const mutation = updateVaultResearchGapNode(workspaceRoot, {
|
|
516
|
+
...(args.researchGaps?.[0] as any ?? {}),
|
|
517
|
+
id: args.gapId,
|
|
518
|
+
status: args.gapStatus as any,
|
|
519
|
+
findingsFile: args.findingsFile,
|
|
520
|
+
evidenceBindingIds: args.evidenceBindingIds,
|
|
521
|
+
notes: args.gapNotes,
|
|
522
|
+
})
|
|
523
|
+
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 })
|
|
525
|
+
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
|
+
}
|
|
527
|
+
|
|
528
|
+
if (args.action === "upsertVaultResearchGap") {
|
|
529
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultResearchGap requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
530
|
+
const gaps = args.researchGaps?.length ? (args.researchGaps as any[]) : [undefined]
|
|
531
|
+
const mutations = []
|
|
532
|
+
for (const gap of gaps) {
|
|
533
|
+
const rawId = gap?.id ?? (gaps.length === 1 ? args.gapId : undefined)
|
|
534
|
+
const id = typeof rawId === "string" ? rawId.trim() : ""
|
|
535
|
+
if (!id) return JSON.stringify({ ok: false, error: "gapId or every researchGaps[].id is required for upsertVaultResearchGap", recovery: researchGapHelperRecovery("upsertVaultResearchGap"), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
|
|
536
|
+
const mutation = updateVaultResearchGapNode(workspaceRoot, {
|
|
537
|
+
...gap,
|
|
538
|
+
id,
|
|
539
|
+
status: args.gapStatus as any ?? gap?.status,
|
|
540
|
+
findingsFile: args.findingsFile ?? gap?.findingsFile,
|
|
541
|
+
evidenceBindingIds: args.evidenceBindingIds ?? gap?.evidenceBindingIds,
|
|
542
|
+
notes: args.gapNotes ?? gap?.notes,
|
|
543
|
+
})
|
|
544
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation, recovery: researchGapHelperRecovery("upsertVaultResearchGap", mutation.missingFields), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
|
|
545
|
+
mutations.push(mutation)
|
|
546
|
+
}
|
|
547
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
548
|
+
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
|
+
}
|
|
550
|
+
|
|
551
|
+
if (args.action === "upsertVaultRelation") {
|
|
552
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultRelation requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
553
|
+
const relation = args.relation as any
|
|
554
|
+
if (!relation?.id) return JSON.stringify({ ok: false, error: "relation.id is required for upsertVaultRelation", recovery: relationHelperRecovery("upsertVaultRelation"), narrativeInventory: buildNarrativeVaultInventory(workspaceRoot), authoringContract: narrativeVaultAuthoringContract() }, null, 2)
|
|
555
|
+
const mutation = upsertVaultRelation(workspaceRoot, {
|
|
556
|
+
id: relation.id,
|
|
557
|
+
fromId: relation.from,
|
|
558
|
+
toId: relation.to,
|
|
559
|
+
relation: relation.type,
|
|
560
|
+
rationale: relation.rationale,
|
|
561
|
+
})
|
|
562
|
+
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 })
|
|
564
|
+
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
|
+
}
|
|
566
|
+
|
|
567
|
+
if (args.action === "removeVaultRelation") {
|
|
568
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "removeVaultRelation requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
569
|
+
const relationId = args.relationId?.trim() || (args.relation as any)?.id?.trim()
|
|
570
|
+
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
|
+
const mutation = removeVaultRelation(workspaceRoot, relationId)
|
|
572
|
+
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 })
|
|
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)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (args.action === "upsertVaultClaim") {
|
|
578
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultClaim requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
579
|
+
const claim = (args.narrative?.claims?.[0] as any) ?? undefined
|
|
580
|
+
if (!claim?.id) return JSON.stringify({ ok: false, error: "narrative.claims[0].id is required for upsertVaultClaim" })
|
|
581
|
+
const relationHint = args.narrative?.claimRelations?.length
|
|
582
|
+
? "upsertVaultClaim does not write graph edges. Add explicit ## Relations lines in the source node after checking narrativeInventory; compileNarrativeVault will generate relation ids."
|
|
583
|
+
: undefined
|
|
584
|
+
const mutation = upsertVaultClaimNode(workspaceRoot, claim)
|
|
585
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
|
|
586
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
587
|
+
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
|
+
}
|
|
589
|
+
|
|
590
|
+
if (args.action === "upsertVaultObjection") {
|
|
591
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultObjection requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
592
|
+
const objection = (args.narrative?.objections?.[0] as any) ?? undefined
|
|
593
|
+
if (!objection?.id) return JSON.stringify({ ok: false, error: "narrative.objections[0].id is required for upsertVaultObjection" })
|
|
594
|
+
const mutation = upsertVaultObjectionNode(workspaceRoot, objection)
|
|
595
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
|
|
596
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
597
|
+
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
|
+
}
|
|
599
|
+
|
|
600
|
+
if (args.action === "upsertVaultRisk") {
|
|
601
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "upsertVaultRisk requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
602
|
+
const risk = (args.narrative?.risks?.[0] as any) ?? undefined
|
|
603
|
+
if (!risk?.id) return JSON.stringify({ ok: false, error: "narrative.risks[0].id is required for upsertVaultRisk" })
|
|
604
|
+
const mutation = upsertVaultRiskNode(workspaceRoot, risk)
|
|
605
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
|
|
606
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
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)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (args.action === "updateVaultCoreNarrative") {
|
|
611
|
+
if (!hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: "updateVaultCoreNarrative requires revela-narrative/ to exist. Use initNarrativeVault first." })
|
|
612
|
+
const mutation = updateVaultCoreNodes(workspaceRoot, args.narrative as any ?? {})
|
|
613
|
+
if (!mutation.ok) return JSON.stringify({ ok: false, mutation }, null, 2)
|
|
614
|
+
const compiled = compileCacheMirrorNarrativeVault(workspaceRoot, { state })
|
|
615
|
+
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
|
+
}
|
|
617
|
+
|
|
310
618
|
if (args.action === "upsertDeck") {
|
|
311
619
|
const deckKey = defaultSlug
|
|
312
620
|
const existing = state.decks[deckKey]
|
|
@@ -342,45 +650,12 @@ export default tool({
|
|
|
342
650
|
}
|
|
343
651
|
|
|
344
652
|
if (args.action === "upsertNarrative") {
|
|
345
|
-
|
|
346
|
-
const current = state.narrative ?? normalizeNarrativeState(state)
|
|
347
|
-
const merged = mergeNarrativeInput(current, args.narrative as Partial<NarrativeStateV1>)
|
|
348
|
-
const normalized = normalizeCanonicalNarrativeState(merged, state.activeDeck ?? defaultSlug)
|
|
349
|
-
if (!normalized) return JSON.stringify({ ok: false, error: "narrative could not be normalized" })
|
|
350
|
-
state.narrative = normalized
|
|
351
|
-
|
|
352
|
-
const deckKey = state.activeDeck
|
|
353
|
-
if (deckKey && state.decks[deckKey]) {
|
|
354
|
-
state = upsertDeck(state, {
|
|
355
|
-
...state.decks[deckKey],
|
|
356
|
-
slug: deckKey,
|
|
357
|
-
audience: normalized.audience.primary || state.decks[deckKey].audience,
|
|
358
|
-
narrativeBrief: narrativeToBrief(normalized),
|
|
359
|
-
})
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
recordWorkspaceAction(state, {
|
|
363
|
-
type: "narrative.upserted",
|
|
364
|
-
actor: "revela-decks",
|
|
365
|
-
inputs: { hadExistingNarrative: Boolean(current), providedFields: Object.keys(args.narrative as object) },
|
|
366
|
-
outputs: {
|
|
367
|
-
narrativeId: normalized.id,
|
|
368
|
-
status: normalized.status,
|
|
369
|
-
claimCount: normalized.claims.length,
|
|
370
|
-
evidenceBindingCount: normalized.evidenceBindings.length,
|
|
371
|
-
objectionCount: normalized.objections.length,
|
|
372
|
-
riskCount: normalized.risks.length,
|
|
373
|
-
},
|
|
374
|
-
status: "success",
|
|
375
|
-
summary: `Updated canonical narrative state with ${normalized.claims.length} claim${normalized.claims.length === 1 ? "" : "s"}.`,
|
|
376
|
-
nodeIds: [normalized.id],
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
writeDecksState(workspaceRoot, state)
|
|
380
|
-
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, narrative: state.narrative, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
|
|
653
|
+
return JSON.stringify({ ok: false, deprecated: true, error: "upsertNarrative is deprecated. Use initNarrativeVault to create revela-narrative/, then update canonical narrative meaning through structured vault actions such as updateVaultCoreNarrative, upsertVaultClaim, upsertVaultEvidence, upsertVaultObjection, upsertVaultRisk, upsertVaultResearchGap, updateVaultResearchGap, or bindResearchFindings.", authoringContract: narrativeVaultAuthoringContract() })
|
|
381
654
|
}
|
|
382
655
|
|
|
383
656
|
if (args.action === "review") {
|
|
657
|
+
const qaGate = strictVaultMarkdownQaGate(workspaceRoot, "render", "review")
|
|
658
|
+
if (qaGate) return JSON.stringify(qaGate, null, 2)
|
|
384
659
|
const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
|
|
385
660
|
const targetId = activeReviewTargetId(reviewed.state)
|
|
386
661
|
const snapshot = latestReviewSnapshotForTarget(reviewed.state, targetId)
|
|
@@ -409,6 +684,8 @@ export default tool({
|
|
|
409
684
|
}
|
|
410
685
|
|
|
411
686
|
if (args.action === "compileDeckPlan") {
|
|
687
|
+
const qaGate = strictVaultMarkdownQaGate(workspaceRoot, "render", "compileDeckPlan")
|
|
688
|
+
if (qaGate) return JSON.stringify(qaGate, null, 2)
|
|
412
689
|
const compiled = compileDeckPlanFromNarrative(state)
|
|
413
690
|
if (compiled.result.compiled) {
|
|
414
691
|
recordWorkspaceAction(compiled.state, {
|
|
@@ -461,6 +738,8 @@ export default tool({
|
|
|
461
738
|
}
|
|
462
739
|
|
|
463
740
|
if (args.action === "reviewNarrative") {
|
|
741
|
+
const qaGate = strictVaultMarkdownQaGate(workspaceRoot, "readiness", "reviewNarrative")
|
|
742
|
+
if (qaGate) return JSON.stringify(qaGate, null, 2)
|
|
464
743
|
const reviewed = reviewNarrativeState(state)
|
|
465
744
|
recordNarrativeReviewAction(reviewed.state, reviewed.result)
|
|
466
745
|
writeDecksState(workspaceRoot, reviewed.state)
|
|
@@ -468,6 +747,8 @@ export default tool({
|
|
|
468
747
|
}
|
|
469
748
|
|
|
470
749
|
if (args.action === "approveNarrative") {
|
|
750
|
+
const qaGate = strictVaultMarkdownQaGate(workspaceRoot, "readiness", "approveNarrative")
|
|
751
|
+
if (qaGate) return JSON.stringify(qaGate, null, 2)
|
|
471
752
|
const approved = approveNarrativeState(state, {
|
|
472
753
|
approvedBy: args.approvalBy,
|
|
473
754
|
scope: args.approvalScope,
|
|
@@ -479,6 +760,7 @@ export default tool({
|
|
|
479
760
|
}
|
|
480
761
|
|
|
481
762
|
if (args.action === "deriveResearchGaps") {
|
|
763
|
+
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() })
|
|
482
764
|
const derived = deriveResearchGapsFromReadiness(state)
|
|
483
765
|
writeDecksState(workspaceRoot, derived.state)
|
|
484
766
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: derived.result, narrative: derived.state.narrative }, null, 2)
|
|
@@ -489,7 +771,18 @@ export default tool({
|
|
|
489
771
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
490
772
|
}
|
|
491
773
|
|
|
774
|
+
if (args.action === "evaluateResearchFindings") {
|
|
775
|
+
if (!args.findingsFile?.trim()) return JSON.stringify({ ok: false, error: "findingsFile is required for evaluateResearchFindings" })
|
|
776
|
+
const bindingEval = evaluateResearchFindingsBinding(state, workspaceRoot, args.findingsFile)
|
|
777
|
+
const targets = deriveResearchTargets(state, { workspaceRoot })
|
|
778
|
+
const vaultDiagnostics = hasNarrativeVault(workspaceRoot)
|
|
779
|
+
? formatVaultDiagnosticReport(compileNarrativeVault(workspaceRoot, { fallbackApprovals: state.narrative?.approvals ?? [] }).diagnostics)
|
|
780
|
+
: undefined
|
|
781
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: { bindingEval, selected: targets.selected, vaultDiagnostics } }, null, 2)
|
|
782
|
+
}
|
|
783
|
+
|
|
492
784
|
if (args.action === "upsertResearchGaps") {
|
|
785
|
+
if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("upsertResearchGaps", "upsertVaultResearchGap for new or updated research gap nodes"), authoringContract: narrativeVaultAuthoringContract() })
|
|
493
786
|
if (!args.researchGaps?.length) return JSON.stringify({ ok: false, error: "researchGaps are required for upsertResearchGaps" })
|
|
494
787
|
const upserted = upsertResearchGapsInState(state, args.researchGaps as any[])
|
|
495
788
|
writeDecksState(workspaceRoot, upserted.state)
|
|
@@ -497,6 +790,7 @@ export default tool({
|
|
|
497
790
|
}
|
|
498
791
|
|
|
499
792
|
if (args.action === "updateResearchGap") {
|
|
793
|
+
if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("updateResearchGap", "updateVaultResearchGap or upsertVaultResearchGap"), authoringContract: narrativeVaultAuthoringContract() })
|
|
500
794
|
if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for updateResearchGap" })
|
|
501
795
|
const updated = updateResearchGapInState(state, {
|
|
502
796
|
id: args.gapId,
|
|
@@ -510,6 +804,7 @@ export default tool({
|
|
|
510
804
|
}
|
|
511
805
|
|
|
512
806
|
if (args.action === "closeResearchGap") {
|
|
807
|
+
if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("closeResearchGap", "updateVaultResearchGap with gapStatus=closed"), authoringContract: narrativeVaultAuthoringContract() })
|
|
513
808
|
if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for closeResearchGap" })
|
|
514
809
|
const closed = closeResearchGapInState(state, args.gapId, args.gapNotes)
|
|
515
810
|
writeDecksState(workspaceRoot, closed.state)
|
|
@@ -517,6 +812,7 @@ export default tool({
|
|
|
517
812
|
}
|
|
518
813
|
|
|
519
814
|
if (args.action === "applyEvidenceCandidates") {
|
|
815
|
+
if (hasNarrativeVault(workspaceRoot)) return JSON.stringify({ ok: false, error: forbiddenVaultCompatibilityAction("applyEvidenceCandidates", "bindResearchFindings for bindable findings or upsertVaultEvidence for explicit evidence source trace"), authoringContract: narrativeVaultAuthoringContract() })
|
|
520
816
|
const candidateIds = args.candidateIds ?? []
|
|
521
817
|
if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
|
|
522
818
|
const result = applyEvidenceBindings(workspaceRoot, candidateIds)
|
package/tools/research-save.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { tool } from "@opencode-ai/plugin"
|
|
|
2
2
|
import { mkdirSync, writeFileSync } from "fs"
|
|
3
3
|
import { join } from "path"
|
|
4
4
|
import { hasDecksState, readDecksState, writeDecksState } from "../lib/decks-state"
|
|
5
|
+
import { evaluateResearchFindingsBinding } from "../lib/narrative-state/research-binding-eval"
|
|
5
6
|
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -100,7 +101,9 @@ export default tool({
|
|
|
100
101
|
summary: `Saved research findings for ${topicKey}/${fileKey}.`,
|
|
101
102
|
nodeIds: [`finding:${relPath}`],
|
|
102
103
|
})
|
|
104
|
+
const bindingEval = evaluateResearchFindingsBinding(state, workspaceDir, relPath)
|
|
103
105
|
writeDecksState(workspaceDir, state)
|
|
106
|
+
return JSON.stringify({ ok: true, path: relPath, bindingEval })
|
|
104
107
|
}
|
|
105
108
|
|
|
106
109
|
return JSON.stringify({ ok: true, path: relPath })
|
package/tools/workspace-scan.ts
CHANGED
|
@@ -15,6 +15,7 @@ const DOC_EXTENSIONS = new Set([
|
|
|
15
15
|
const EXCLUDE_DIRS = new Set([
|
|
16
16
|
"node_modules", ".git", "dist", ".opencode",
|
|
17
17
|
"researches", // Exclude revela's own research output
|
|
18
|
+
"revela-narrative", // Exclude canonical narrative vault source files
|
|
18
19
|
"designs", "domains", // Exclude revela plugin assets
|
|
19
20
|
])
|
|
20
21
|
|