@cyber-dash-tech/revela 0.11.0 → 0.12.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 +35 -29
- package/README.zh-CN.md +35 -29
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +5 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +13 -3
- package/lib/narrative-state/hash.ts +52 -0
- package/lib/narrative-state/normalize.ts +307 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/readiness.ts +289 -0
- package/lib/narrative-state/render-plan.ts +207 -0
- package/lib/narrative-state/types.ts +139 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/graph.ts +120 -2
- package/lib/workspace-state/types.ts +3 -0
- package/package.json +1 -1
- package/plugin.ts +27 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +180 -2
- package/tools/workspace-scan.ts +14 -1
package/lib/prompt-builder.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt builder — assembles
|
|
2
|
+
* Prompt builder — assembles Revela system prompts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Narrative mode:
|
|
5
|
+
* Layer 1: NARRATIVE_SKILL.md — audience/decision/claim/evidence readiness
|
|
6
|
+
* Layer 2: DOMAIN.md — domain reasoning guidance when present
|
|
7
|
+
*
|
|
8
|
+
* Deck-render mode:
|
|
9
|
+
* Layer 1: SKILL.md — legacy deck render protocol (HTML rules, quality)
|
|
10
|
+
* Layer 2: DOMAIN.md — domain structure/terminology
|
|
11
|
+
* Layer 3: DESIGN.md — visual style (colors, fonts, animations, layout)
|
|
7
12
|
*
|
|
8
13
|
* When the active DESIGN.md has @section markers, only the global section,
|
|
9
14
|
* layouts section, and a generated component index are injected into the
|
|
@@ -37,22 +42,43 @@ import { childLog } from "./log"
|
|
|
37
42
|
|
|
38
43
|
const promptLog = childLog("prompt-builder")
|
|
39
44
|
|
|
40
|
-
|
|
45
|
+
export type PromptMode = "narrative" | "deck-render"
|
|
46
|
+
|
|
47
|
+
export interface BuildPromptOptions {
|
|
48
|
+
mode?: PromptMode
|
|
49
|
+
designName?: string
|
|
50
|
+
domainName?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Path to deck-render SKILL.md shipped with this package. */
|
|
41
54
|
const SKILL_MD_PATH = resolve(__dirname, "..", "skill", "SKILL.md")
|
|
55
|
+
const NARRATIVE_SKILL_MD_PATH = resolve(__dirname, "..", "skill", "NARRATIVE_SKILL.md")
|
|
42
56
|
|
|
43
57
|
/**
|
|
44
|
-
* Build the
|
|
58
|
+
* Build the active system prompt and write it to _active-prompt.md.
|
|
59
|
+
*
|
|
60
|
+
* Backward-compatible call form:
|
|
61
|
+
* - buildPrompt() builds the default narrative prompt.
|
|
62
|
+
* - buildPrompt("aurora", "general") builds the default narrative prompt with active metadata overrides.
|
|
63
|
+
*
|
|
64
|
+
* New call form:
|
|
65
|
+
* - buildPrompt({ mode: "narrative" }) avoids design/HTML instructions.
|
|
66
|
+
* - buildPrompt({ mode: "deck-render" }) preserves the legacy deck render prompt.
|
|
45
67
|
*
|
|
46
|
-
* @param designName - Override design (defaults to active)
|
|
47
|
-
* @param domainName - Override domain (defaults to active)
|
|
48
68
|
* @returns The path to the written file.
|
|
49
69
|
*/
|
|
50
|
-
export function buildPrompt(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
70
|
+
export function buildPrompt(options?: BuildPromptOptions): string
|
|
71
|
+
export function buildPrompt(designName?: string, domainName?: string): string
|
|
72
|
+
export function buildPrompt(optionsOrDesignName?: BuildPromptOptions | string, legacyDomainName?: string): string {
|
|
73
|
+
const options = typeof optionsOrDesignName === "object" && optionsOrDesignName !== null
|
|
74
|
+
? optionsOrDesignName
|
|
75
|
+
: { designName: optionsOrDesignName, domainName: legacyDomainName }
|
|
76
|
+
const mode: PromptMode = options.mode || "narrative"
|
|
77
|
+
const design = options.designName || activeDesign()
|
|
78
|
+
const domain = options.domainName || activeDomain()
|
|
79
|
+
|
|
80
|
+
// Layer 1 — core skill for the selected prompt mode.
|
|
81
|
+
const coreSkill = readFileSync(mode === "deck-render" ? SKILL_MD_PATH : NARRATIVE_SKILL_MD_PATH, "utf-8")
|
|
56
82
|
|
|
57
83
|
// Check for preview.html
|
|
58
84
|
const designDir = join(DESIGNS_DIR, design)
|
|
@@ -73,30 +99,37 @@ export function buildPrompt(designName?: string, domainName?: string): string {
|
|
|
73
99
|
})
|
|
74
100
|
}
|
|
75
101
|
|
|
76
|
-
// Layer 3 — DESIGN.md:
|
|
77
|
-
|
|
102
|
+
// Layer 3 — DESIGN.md: deck-render only. Narrative mode must not inject
|
|
103
|
+
// visual CSS, layout catalogs, component indexes, or HTML skeleton rules.
|
|
104
|
+
const designSkill = mode === "deck-render" ? buildDesignLayer(design) : ""
|
|
78
105
|
|
|
79
106
|
// Assemble header
|
|
80
|
-
const header =
|
|
81
|
-
`<!--
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
const header = mode === "deck-render"
|
|
108
|
+
? `<!-- Revela prompt mode: deck-render -->\n` +
|
|
109
|
+
`<!-- Active design: ${design} -->\n` +
|
|
110
|
+
`<!-- Active domain: ${domain} -->\n` +
|
|
111
|
+
`<!-- Design files: ${designDir}/ -->\n` +
|
|
112
|
+
`<!-- - DESIGN.md — metadata + style instructions (injected below) -->\n` +
|
|
113
|
+
`${previewLine}\n\n`
|
|
114
|
+
: `<!-- Revela prompt mode: narrative -->\n` +
|
|
115
|
+
`<!-- Active domain: ${domain} -->\n` +
|
|
116
|
+
`<!-- Design layer intentionally omitted in narrative mode. Use deck-render mode before writing deck artifacts. -->\n\n`
|
|
117
|
+
|
|
118
|
+
// Concatenation: Header → Skill → Domain → Design (deck-render only)
|
|
88
119
|
const parts = [header, coreSkill]
|
|
89
120
|
if (domainSkill) {
|
|
90
121
|
parts.push(`\n\n---\n\n${domainSkill}`)
|
|
91
122
|
}
|
|
92
|
-
|
|
123
|
+
if (designSkill) {
|
|
124
|
+
parts.push(`\n\n---\n\n${designSkill}`)
|
|
125
|
+
}
|
|
93
126
|
|
|
94
127
|
const prompt = parts.join("")
|
|
95
128
|
|
|
96
129
|
// Write to _active-prompt.md
|
|
97
130
|
mkdirSync(CONFIG_DIR, { recursive: true })
|
|
98
131
|
writeFileSync(ACTIVE_PROMPT_FILE, prompt, "utf-8")
|
|
99
|
-
promptLog.info("prompt rebuilt", { design, domain, bytes: prompt.length })
|
|
132
|
+
promptLog.info("prompt rebuilt", { mode, design, domain, bytes: prompt.length })
|
|
100
133
|
|
|
101
134
|
return ACTIVE_PROMPT_FILE
|
|
102
135
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "crypto"
|
|
2
2
|
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, ResearchAxis, SlideSpec, SourceMaterial } from "../decks-state"
|
|
3
|
+
import type { NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
|
|
3
4
|
import { renderTargetId } from "./render-targets"
|
|
4
5
|
import type { GraphEdge, GraphEdgeType, GraphNode, GraphNodeType, RenderTarget, WorkspaceGraph } from "./types"
|
|
5
6
|
|
|
@@ -19,7 +20,7 @@ export function projectWorkspaceGraph(state: DecksState, options: ProjectWorkspa
|
|
|
19
20
|
for (const material of state.workspace.sourceMaterials ?? []) addSourceMaterial(builder, material)
|
|
20
21
|
for (const axis of deck.researchPlan ?? []) addResearchFinding(builder, axis)
|
|
21
22
|
|
|
22
|
-
const narrativeId = addNarrative(builder, deck)
|
|
23
|
+
const narrativeId = addNarrative(builder, state, deck)
|
|
23
24
|
for (const slide of deck.slides.slice().sort((a, b) => a.index - b.index)) addSlide(builder, slide)
|
|
24
25
|
for (const slide of deck.slides.slice().sort((a, b) => a.index - b.index)) addSlideClaimsAndEvidence(builder, slide)
|
|
25
26
|
const targets = renderTargetsForDeck(state, deck)
|
|
@@ -84,7 +85,9 @@ function addResearchFinding(builder: GraphBuilder, axis: ResearchAxis): void {
|
|
|
84
85
|
})
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function addNarrative(builder: GraphBuilder, deck: DeckSpec): string | undefined {
|
|
88
|
+
function addNarrative(builder: GraphBuilder, state: DecksState, deck: DeckSpec): string | undefined {
|
|
89
|
+
if (hasCanonicalNarrative(state.narrative)) return addCanonicalNarrative(builder, state.narrative, deck)
|
|
90
|
+
|
|
88
91
|
const brief = deck.narrativeBrief
|
|
89
92
|
if (!hasNarrativeBrief(brief)) return undefined
|
|
90
93
|
|
|
@@ -122,6 +125,84 @@ function addNarrative(builder: GraphBuilder, deck: DeckSpec): string | undefined
|
|
|
122
125
|
return narrativeId
|
|
123
126
|
}
|
|
124
127
|
|
|
128
|
+
function addCanonicalNarrative(builder: GraphBuilder, narrative: NarrativeStateV1, deck: DeckSpec): string {
|
|
129
|
+
addNode(builder, {
|
|
130
|
+
id: narrative.id,
|
|
131
|
+
type: "narrativeIntent",
|
|
132
|
+
label: narrative.thesis?.statement || deck.goal || narrative.id,
|
|
133
|
+
data: compactData({
|
|
134
|
+
status: narrative.status,
|
|
135
|
+
goal: deck.goal,
|
|
136
|
+
audience: narrative.audience.primary || deck.audience,
|
|
137
|
+
language: deck.language,
|
|
138
|
+
beliefBefore: narrative.audience.beliefBefore,
|
|
139
|
+
beliefAfter: narrative.audience.beliefAfter,
|
|
140
|
+
decisionOrAction: narrative.decision.action,
|
|
141
|
+
thesis: narrative.thesis?.statement,
|
|
142
|
+
}),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
for (const claim of narrative.claims) {
|
|
146
|
+
addNode(builder, {
|
|
147
|
+
id: claim.id,
|
|
148
|
+
type: "claim",
|
|
149
|
+
label: claim.text,
|
|
150
|
+
data: compactData({
|
|
151
|
+
text: claim.text,
|
|
152
|
+
kind: claim.kind,
|
|
153
|
+
importance: claim.importance,
|
|
154
|
+
evidenceRequired: claim.evidenceRequired,
|
|
155
|
+
evidenceStatus: claim.evidenceStatus,
|
|
156
|
+
supportedScope: claim.supportedScope,
|
|
157
|
+
unsupportedScope: claim.unsupportedScope,
|
|
158
|
+
caveats: claim.caveats,
|
|
159
|
+
source: "canonicalNarrative",
|
|
160
|
+
}),
|
|
161
|
+
})
|
|
162
|
+
addEdge(builder, "contains", narrative.id, claim.id)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const binding of narrative.evidenceBindings) {
|
|
166
|
+
const supportId = addNarrativeEvidenceSupportNode(builder, binding)
|
|
167
|
+
addEdge(builder, "supports", supportId, binding.claimId, compactData({
|
|
168
|
+
strength: binding.strength,
|
|
169
|
+
source: binding.source,
|
|
170
|
+
quote: binding.quote,
|
|
171
|
+
url: binding.url,
|
|
172
|
+
sourcePath: binding.sourcePath,
|
|
173
|
+
location: binding.location,
|
|
174
|
+
findingsFile: binding.findingsFile,
|
|
175
|
+
caveat: binding.caveat,
|
|
176
|
+
supportScope: binding.supportScope,
|
|
177
|
+
unsupportedScope: binding.unsupportedScope,
|
|
178
|
+
}))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for (const objection of narrative.objections) {
|
|
182
|
+
addNode(builder, {
|
|
183
|
+
id: objection.id,
|
|
184
|
+
type: "objection",
|
|
185
|
+
label: objection.text,
|
|
186
|
+
data: compactData({ text: objection.text, priority: objection.priority, response: objection.response }),
|
|
187
|
+
})
|
|
188
|
+
addEdge(builder, "contains", narrative.id, objection.id)
|
|
189
|
+
addEdge(builder, "challenges", objection.id, objection.claimId || narrative.id)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
for (const risk of narrative.risks) {
|
|
193
|
+
addNode(builder, {
|
|
194
|
+
id: risk.id,
|
|
195
|
+
type: "risk",
|
|
196
|
+
label: risk.text,
|
|
197
|
+
data: compactData({ text: risk.text, severity: risk.severity, mitigation: risk.mitigation }),
|
|
198
|
+
})
|
|
199
|
+
addEdge(builder, "contains", narrative.id, risk.id)
|
|
200
|
+
addEdge(builder, "constrained_by", risk.claimId || narrative.id, risk.id)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return narrative.id
|
|
204
|
+
}
|
|
205
|
+
|
|
125
206
|
function addSlide(builder: GraphBuilder, slide: SlideSpec): void {
|
|
126
207
|
addNode(builder, {
|
|
127
208
|
id: slideNodeId(slide.index),
|
|
@@ -222,6 +303,29 @@ function addEvidenceSupportNode(builder: GraphBuilder, evidence: EvidenceRef): s
|
|
|
222
303
|
return id
|
|
223
304
|
}
|
|
224
305
|
|
|
306
|
+
function addNarrativeEvidenceSupportNode(builder: GraphBuilder, binding: NarrativeEvidenceBinding): string {
|
|
307
|
+
if (binding.findingsFile?.trim()) {
|
|
308
|
+
const id = findingNodeId(binding.findingsFile)
|
|
309
|
+
addNode(builder, {
|
|
310
|
+
id,
|
|
311
|
+
type: "finding",
|
|
312
|
+
label: binding.findingsFile,
|
|
313
|
+
data: compactData({ findingsFile: binding.findingsFile, source: binding.source, quote: binding.quote, location: binding.location, caveat: binding.caveat }),
|
|
314
|
+
})
|
|
315
|
+
return id
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sourceKey = binding.sourcePath || binding.source || binding.url || "unknown-narrative-evidence-source"
|
|
319
|
+
const id = sourceNodeId(sourceKey)
|
|
320
|
+
addNode(builder, {
|
|
321
|
+
id,
|
|
322
|
+
type: "source",
|
|
323
|
+
label: sourceKey,
|
|
324
|
+
data: compactData({ source: binding.source, sourcePath: binding.sourcePath, url: binding.url }),
|
|
325
|
+
})
|
|
326
|
+
return id
|
|
327
|
+
}
|
|
328
|
+
|
|
225
329
|
function addArtifact(builder: GraphBuilder, deck: DeckSpec, target: RenderTarget, narrativeId: string | undefined, targets: RenderTarget[]): void {
|
|
226
330
|
const artifactId = artifactNodeId(target.outputPath ?? deck.outputPath)
|
|
227
331
|
addNode(builder, {
|
|
@@ -308,6 +412,20 @@ function hasNarrativeBrief(brief: NarrativeBrief | undefined): boolean {
|
|
|
308
412
|
)
|
|
309
413
|
}
|
|
310
414
|
|
|
415
|
+
function hasCanonicalNarrative(narrative: NarrativeStateV1 | undefined): narrative is NarrativeStateV1 {
|
|
416
|
+
return Boolean(
|
|
417
|
+
narrative?.audience.primary?.trim() ||
|
|
418
|
+
narrative?.audience.beliefBefore?.trim() ||
|
|
419
|
+
narrative?.audience.beliefAfter?.trim() ||
|
|
420
|
+
narrative?.decision.action?.trim() ||
|
|
421
|
+
narrative?.thesis?.statement?.trim() ||
|
|
422
|
+
narrative?.claims.length ||
|
|
423
|
+
narrative?.evidenceBindings.length ||
|
|
424
|
+
narrative?.objections.length ||
|
|
425
|
+
narrative?.risks.length,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
311
429
|
function hasEvidenceDetail(evidence: EvidenceRef): boolean {
|
|
312
430
|
return Boolean(
|
|
313
431
|
evidence.quote?.trim() ||
|
|
@@ -88,8 +88,11 @@ export type WorkspaceActionType =
|
|
|
88
88
|
| "source.extracted"
|
|
89
89
|
| "research.findings_saved"
|
|
90
90
|
| "research.findings_attached"
|
|
91
|
+
| "narrative.upserted"
|
|
92
|
+
| "deck.plan_compiled"
|
|
91
93
|
| "evidence.candidate_generated"
|
|
92
94
|
| "evidence.binding_applied"
|
|
95
|
+
| "narrative.approved"
|
|
93
96
|
| "review.performed"
|
|
94
97
|
| "artifact.rendered"
|
|
95
98
|
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
} from "./lib/commands/designs-new"
|
|
61
61
|
import { buildInitPrompt } from "./lib/commands/init"
|
|
62
62
|
import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
|
|
63
|
-
import { buildReviewPrompt } from "./lib/commands/review"
|
|
63
|
+
import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
|
|
64
64
|
import {
|
|
65
65
|
extractDeckHtmlTargetsFromPatch,
|
|
66
66
|
extractPatchTextArg,
|
|
@@ -326,6 +326,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
326
326
|
throw new Error("__REVELA_DISABLE_HANDLED__")
|
|
327
327
|
}
|
|
328
328
|
if (sub === "init") {
|
|
329
|
+
buildPrompt({ mode: "narrative" })
|
|
329
330
|
output.parts.length = 0
|
|
330
331
|
output.parts.push({
|
|
331
332
|
type: "text",
|
|
@@ -339,6 +340,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
339
340
|
await send(parsed.error)
|
|
340
341
|
throw new Error("__REVELA_REMEMBER_USAGE_HANDLED__")
|
|
341
342
|
}
|
|
343
|
+
buildPrompt({ mode: "narrative" })
|
|
342
344
|
output.parts.length = 0
|
|
343
345
|
output.parts.push({
|
|
344
346
|
type: "text",
|
|
@@ -348,9 +350,10 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
348
350
|
}
|
|
349
351
|
if (sub === "review") {
|
|
350
352
|
if (param) {
|
|
351
|
-
await send("`/revela review` no longer accepts a deck name. It reviews the current workspace deck.")
|
|
353
|
+
await send("`/revela review` no longer accepts a deck name. It reviews the current workspace narrative. Use `/revela deck --review` for deck/artifact readiness.")
|
|
352
354
|
throw new Error("__REVELA_REVIEW_USAGE_HANDLED__")
|
|
353
355
|
}
|
|
356
|
+
buildPrompt({ mode: "narrative" })
|
|
354
357
|
output.parts.length = 0
|
|
355
358
|
output.parts.push({
|
|
356
359
|
type: "text",
|
|
@@ -358,6 +361,28 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
358
361
|
} as any)
|
|
359
362
|
return
|
|
360
363
|
}
|
|
364
|
+
if (sub === "deck") {
|
|
365
|
+
if (param && param !== "--review") {
|
|
366
|
+
await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness.")
|
|
367
|
+
throw new Error("__REVELA_DECK_USAGE_HANDLED__")
|
|
368
|
+
}
|
|
369
|
+
if (!param) {
|
|
370
|
+
buildPrompt({ mode: "deck-render" })
|
|
371
|
+
output.parts.length = 0
|
|
372
|
+
output.parts.push({
|
|
373
|
+
type: "text",
|
|
374
|
+
text: buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
|
|
375
|
+
} as any)
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
buildPrompt({ mode: "deck-render" })
|
|
379
|
+
output.parts.length = 0
|
|
380
|
+
output.parts.push({
|
|
381
|
+
type: "text",
|
|
382
|
+
text: buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
|
|
383
|
+
} as any)
|
|
384
|
+
return
|
|
385
|
+
}
|
|
361
386
|
if (sub === "refine") {
|
|
362
387
|
if (param) {
|
|
363
388
|
await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: revela-narrative
|
|
3
|
+
description: Build trusted narrative readiness before rendering deck artifacts
|
|
4
|
+
compatibility: opencode
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Revela — Narrative Workspace
|
|
8
|
+
|
|
9
|
+
You help the user turn source materials, research, and intent into a trusted communication narrative before any deck is rendered.
|
|
10
|
+
|
|
11
|
+
Default mode is narrative-first. Do not generate HTML slides, choose visual layouts, fetch design components, or ask for slide count unless the user explicitly enters a deck-render workflow.
|
|
12
|
+
|
|
13
|
+
## Core Job
|
|
14
|
+
|
|
15
|
+
Build and review the narrative state around:
|
|
16
|
+
- primary audience and stakeholder context
|
|
17
|
+
- audience belief before and desired belief after
|
|
18
|
+
- decision or action required
|
|
19
|
+
- thesis or central recommendation
|
|
20
|
+
- central claims and their evidence boundaries
|
|
21
|
+
- objections, risks, assumptions, caveats, and unsupported scope
|
|
22
|
+
- narrative approval state and whether approval is stale
|
|
23
|
+
|
|
24
|
+
## Workspace State
|
|
25
|
+
|
|
26
|
+
Use `DECKS.json` as Revela's current compatibility workspace-state file. Do not write or patch it directly.
|
|
27
|
+
|
|
28
|
+
Use `revela-decks` for state operations:
|
|
29
|
+
- `read` to inspect current workspace state
|
|
30
|
+
- `init` to register discovered source material candidates during workspace initialization
|
|
31
|
+
- `upsertNarrative` to preserve canonical audience, decision, thesis, claims, evidence bindings, objections, and risks
|
|
32
|
+
- `upsertDeck` or `upsertSlides` only when explicitly needed by a deck/artifact workflow prompt
|
|
33
|
+
- `reviewNarrative` to run deterministic narrative readiness
|
|
34
|
+
- `approveNarrative` only when the user explicitly approves or requests an override
|
|
35
|
+
|
|
36
|
+
Never treat `writeReadiness.status`, old review snapshots, existing `decks/*.html`, or saved research actions as narrative approval.
|
|
37
|
+
|
|
38
|
+
## Narrative Review Rules
|
|
39
|
+
|
|
40
|
+
When reviewing, call `revela-decks` action `reviewNarrative` and report the tool result as authoritative.
|
|
41
|
+
|
|
42
|
+
Use this report shape:
|
|
43
|
+
- `Narrative readiness: <status>`
|
|
44
|
+
- `Narrative hash: <hash>` when available
|
|
45
|
+
- blockers first, with issue type, claim text when available, and suggested next action
|
|
46
|
+
- warnings second, as residual risks
|
|
47
|
+
- approval state last, clearly distinguishing `ready_for_approval`, `approved`, stale approval, and render override
|
|
48
|
+
|
|
49
|
+
If evidence is missing, say what is missing and what should happen next. Do not invent quotes, sources, page locations, URLs, caveats, or research findings.
|
|
50
|
+
|
|
51
|
+
If research findings were saved but not attached or bound, describe them as unattached research state, not proof.
|
|
52
|
+
|
|
53
|
+
If the narrative is ready for approval, ask the user whether to approve or revise it. Do not approve automatically.
|
|
54
|
+
|
|
55
|
+
## Boundaries
|
|
56
|
+
|
|
57
|
+
- Do not write or overwrite `decks/*.html` in narrative mode.
|
|
58
|
+
- Do not call `revela-decks review` in narrative mode; that is the deck/artifact gate.
|
|
59
|
+
- Do not apply evidence candidates, bind evidence, or rewrite slide text unless the user explicitly asks.
|
|
60
|
+
- Do not fetch design CSS, layouts, components, chart rules, or HTML skeletons in narrative mode.
|
|
61
|
+
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
62
|
+
- Do not infer long-term user preferences from one-off tasks.
|
|
63
|
+
|
|
64
|
+
When the user wants deck/artifact readiness, direct them to `/revela deck --review`. When they want to render a deck, wait for the explicit deck workflow.
|
package/tools/decks.ts
CHANGED
|
@@ -21,15 +21,49 @@ import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
|
21
21
|
import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
|
|
22
22
|
import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
|
|
23
23
|
import { activeReviewTargetId, latestReviewSnapshotForTarget } from "../lib/workspace-state/review-snapshots"
|
|
24
|
+
import {
|
|
25
|
+
approveNarrativeState,
|
|
26
|
+
recordNarrativeApprovalAction,
|
|
27
|
+
recordNarrativeReviewAction,
|
|
28
|
+
reviewNarrativeState,
|
|
29
|
+
} from "../lib/narrative-state/readiness"
|
|
30
|
+
import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
|
|
31
|
+
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "../lib/narrative-state/normalize"
|
|
32
|
+
import { narrativeToBrief } from "../lib/narrative-state/project-compat"
|
|
33
|
+
import type { NarrativeStateV1 } from "../lib/narrative-state/types"
|
|
34
|
+
|
|
35
|
+
function mergeNarrativeInput(current: NarrativeStateV1, input: Partial<NarrativeStateV1>): Partial<NarrativeStateV1> {
|
|
36
|
+
return {
|
|
37
|
+
...current,
|
|
38
|
+
...input,
|
|
39
|
+
id: current.id,
|
|
40
|
+
version: 1,
|
|
41
|
+
audience: {
|
|
42
|
+
...current.audience,
|
|
43
|
+
...(input.audience ?? {}),
|
|
44
|
+
},
|
|
45
|
+
decision: {
|
|
46
|
+
...current.decision,
|
|
47
|
+
...(input.decision ?? {}),
|
|
48
|
+
},
|
|
49
|
+
thesis: input.thesis ? { ...current.thesis, ...input.thesis } as NarrativeStateV1["thesis"] : current.thesis,
|
|
50
|
+
claims: input.claims ?? current.claims,
|
|
51
|
+
evidenceBindings: input.evidenceBindings ?? current.evidenceBindings,
|
|
52
|
+
objections: input.objections ?? current.objections,
|
|
53
|
+
risks: input.risks ?? current.risks,
|
|
54
|
+
approvals: current.approvals,
|
|
55
|
+
updatedAt: new Date().toISOString(),
|
|
56
|
+
}
|
|
57
|
+
}
|
|
24
58
|
|
|
25
59
|
export default tool({
|
|
26
60
|
description:
|
|
27
61
|
`Read and update ${DECKS_STATE_FILE}, Revela's workspace deck state file. ` +
|
|
28
62
|
"Use this tool instead of writing or patching the state file directly. " +
|
|
29
|
-
"It stores active deck specs, per-slide content/layout/components, and computes
|
|
63
|
+
"It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
|
|
30
64
|
args: {
|
|
31
65
|
action: tool.schema
|
|
32
|
-
.enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
66
|
+
.enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "review", "reviewNarrative", "approveNarrative", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
33
67
|
.describe("Action to perform on DECKS.json."),
|
|
34
68
|
summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
|
|
35
69
|
goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
|
|
@@ -45,6 +79,69 @@ export default tool({
|
|
|
45
79
|
objections: tool.schema.array(tool.schema.string()).optional().describe("Likely stakeholder objections or questions the narrative should handle."),
|
|
46
80
|
risks: tool.schema.array(tool.schema.string()).optional().describe("Risks, assumptions, caveats, or tradeoffs that should travel with the narrative."),
|
|
47
81
|
}).optional().describe("For upsertDeck: 0.9 Narrative Compiler brief used to review story intent before writing."),
|
|
82
|
+
narrative: tool.schema.object({
|
|
83
|
+
status: tool.schema.enum(["draft", "needs_research", "needs_user_confirmation", "ready_for_approval", "approved"]).optional(),
|
|
84
|
+
audience: tool.schema.object({
|
|
85
|
+
primary: tool.schema.string().optional(),
|
|
86
|
+
secondary: tool.schema.array(tool.schema.string()).optional(),
|
|
87
|
+
beliefBefore: tool.schema.string().optional(),
|
|
88
|
+
beliefAfter: tool.schema.string().optional(),
|
|
89
|
+
decisionContext: tool.schema.string().optional(),
|
|
90
|
+
successCriteria: tool.schema.array(tool.schema.string()).optional(),
|
|
91
|
+
}).optional(),
|
|
92
|
+
decision: tool.schema.object({
|
|
93
|
+
action: tool.schema.string().optional(),
|
|
94
|
+
owner: tool.schema.string().optional(),
|
|
95
|
+
deadline: tool.schema.string().optional(),
|
|
96
|
+
decisionType: tool.schema.enum(["approve", "invest", "prioritize", "align", "choose", "understand", "other"]).optional(),
|
|
97
|
+
consequenceOfNoDecision: tool.schema.string().optional(),
|
|
98
|
+
}).optional(),
|
|
99
|
+
thesis: tool.schema.object({
|
|
100
|
+
id: tool.schema.string().optional(),
|
|
101
|
+
statement: tool.schema.string().optional(),
|
|
102
|
+
confidence: tool.schema.enum(["high", "medium", "low"]).optional(),
|
|
103
|
+
caveat: tool.schema.string().optional(),
|
|
104
|
+
}).optional(),
|
|
105
|
+
claims: tool.schema.array(tool.schema.object({
|
|
106
|
+
id: tool.schema.string().optional(),
|
|
107
|
+
kind: tool.schema.enum(["context", "problem", "opportunity", "evidence", "recommendation", "risk", "assumption", "ask"]).optional(),
|
|
108
|
+
text: tool.schema.string().describe("Claim text."),
|
|
109
|
+
importance: tool.schema.enum(["central", "supporting", "background"]).optional(),
|
|
110
|
+
evidenceRequired: tool.schema.boolean().optional(),
|
|
111
|
+
evidenceStatus: tool.schema.enum(["supported", "partial", "weak", "missing", "not_required"]).optional(),
|
|
112
|
+
supportedScope: tool.schema.string().optional(),
|
|
113
|
+
unsupportedScope: tool.schema.string().optional(),
|
|
114
|
+
caveats: tool.schema.array(tool.schema.string()).optional(),
|
|
115
|
+
})).optional(),
|
|
116
|
+
evidenceBindings: tool.schema.array(tool.schema.object({
|
|
117
|
+
id: tool.schema.string().optional(),
|
|
118
|
+
claimId: tool.schema.string().describe("Canonical claim id this evidence supports."),
|
|
119
|
+
source: tool.schema.string().describe("Source file, URL, research finding, or material name."),
|
|
120
|
+
sourcePath: tool.schema.string().optional(),
|
|
121
|
+
findingsFile: tool.schema.string().optional(),
|
|
122
|
+
quote: tool.schema.string().optional(),
|
|
123
|
+
location: tool.schema.string().optional(),
|
|
124
|
+
url: tool.schema.string().optional(),
|
|
125
|
+
caveat: tool.schema.string().optional(),
|
|
126
|
+
supportScope: tool.schema.string().optional(),
|
|
127
|
+
unsupportedScope: tool.schema.string().optional(),
|
|
128
|
+
strength: tool.schema.enum(["strong", "partial", "weak"]).optional(),
|
|
129
|
+
})).optional(),
|
|
130
|
+
objections: tool.schema.array(tool.schema.object({
|
|
131
|
+
id: tool.schema.string().optional(),
|
|
132
|
+
text: tool.schema.string().describe("Objection text."),
|
|
133
|
+
claimId: tool.schema.string().optional(),
|
|
134
|
+
priority: tool.schema.enum(["high", "medium", "low"]).optional(),
|
|
135
|
+
response: tool.schema.string().optional(),
|
|
136
|
+
})).optional(),
|
|
137
|
+
risks: tool.schema.array(tool.schema.object({
|
|
138
|
+
id: tool.schema.string().optional(),
|
|
139
|
+
text: tool.schema.string().describe("Risk, assumption, caveat, or tradeoff text."),
|
|
140
|
+
claimId: tool.schema.string().optional(),
|
|
141
|
+
severity: tool.schema.enum(["high", "medium", "low"]).optional(),
|
|
142
|
+
mitigation: tool.schema.string().optional(),
|
|
143
|
+
})).optional(),
|
|
144
|
+
}).optional().describe("For upsertNarrative: canonical narrative state fields to merge into DECKS.json. Replaces provided arrays, preserves approvals."),
|
|
48
145
|
design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
|
|
49
146
|
domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
|
|
50
147
|
memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
|
|
@@ -124,6 +221,9 @@ export default tool({
|
|
|
124
221
|
findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
|
|
125
222
|
researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
|
|
126
223
|
researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
|
|
224
|
+
approvalNote: tool.schema.string().optional().describe("For approveNarrative: optional note explaining the approval or override."),
|
|
225
|
+
approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative: use override only for explicit render overrides, not normal strategic approval."),
|
|
226
|
+
approvalScope: tool.schema.enum(["narrative", "render_override"]).optional().describe("For approveNarrative: narrative approval or explicit render override scope."),
|
|
127
227
|
},
|
|
128
228
|
async execute(args, context) {
|
|
129
229
|
try {
|
|
@@ -194,6 +294,45 @@ export default tool({
|
|
|
194
294
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
|
|
195
295
|
}
|
|
196
296
|
|
|
297
|
+
if (args.action === "upsertNarrative") {
|
|
298
|
+
if (!args.narrative) return JSON.stringify({ ok: false, error: "narrative is required for upsertNarrative" })
|
|
299
|
+
const current = state.narrative ?? normalizeNarrativeState(state)
|
|
300
|
+
const merged = mergeNarrativeInput(current, args.narrative as Partial<NarrativeStateV1>)
|
|
301
|
+
const normalized = normalizeCanonicalNarrativeState(merged, state.activeDeck ?? defaultSlug)
|
|
302
|
+
if (!normalized) return JSON.stringify({ ok: false, error: "narrative could not be normalized" })
|
|
303
|
+
state.narrative = normalized
|
|
304
|
+
|
|
305
|
+
const deckKey = state.activeDeck
|
|
306
|
+
if (deckKey && state.decks[deckKey]) {
|
|
307
|
+
state = upsertDeck(state, {
|
|
308
|
+
...state.decks[deckKey],
|
|
309
|
+
slug: deckKey,
|
|
310
|
+
audience: normalized.audience.primary || state.decks[deckKey].audience,
|
|
311
|
+
narrativeBrief: narrativeToBrief(normalized),
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
recordWorkspaceAction(state, {
|
|
316
|
+
type: "narrative.upserted",
|
|
317
|
+
actor: "revela-decks",
|
|
318
|
+
inputs: { hadExistingNarrative: Boolean(current), providedFields: Object.keys(args.narrative as object) },
|
|
319
|
+
outputs: {
|
|
320
|
+
narrativeId: normalized.id,
|
|
321
|
+
status: normalized.status,
|
|
322
|
+
claimCount: normalized.claims.length,
|
|
323
|
+
evidenceBindingCount: normalized.evidenceBindings.length,
|
|
324
|
+
objectionCount: normalized.objections.length,
|
|
325
|
+
riskCount: normalized.risks.length,
|
|
326
|
+
},
|
|
327
|
+
status: "success",
|
|
328
|
+
summary: `Updated canonical narrative state with ${normalized.claims.length} claim${normalized.claims.length === 1 ? "" : "s"}.`,
|
|
329
|
+
nodeIds: [normalized.id],
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
writeDecksState(workspaceRoot, state)
|
|
333
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, narrative: state.narrative, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
|
|
334
|
+
}
|
|
335
|
+
|
|
197
336
|
if (args.action === "review") {
|
|
198
337
|
const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
|
|
199
338
|
const targetId = activeReviewTargetId(reviewed.state)
|
|
@@ -222,6 +361,45 @@ export default tool({
|
|
|
222
361
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
|
|
223
362
|
}
|
|
224
363
|
|
|
364
|
+
if (args.action === "compileDeckPlan") {
|
|
365
|
+
const compiled = compileDeckPlanFromNarrative(state)
|
|
366
|
+
if (compiled.result.compiled) {
|
|
367
|
+
recordWorkspaceAction(compiled.state, {
|
|
368
|
+
type: "deck.plan_compiled",
|
|
369
|
+
actor: "revela-decks",
|
|
370
|
+
inputs: { narrativeId: compiled.state.narrative?.id, activeDeck: compiled.state.activeDeck },
|
|
371
|
+
outputs: {
|
|
372
|
+
narrativeHash: compiled.result.narrativeHash,
|
|
373
|
+
slideCount: compiled.result.slideCount,
|
|
374
|
+
outputPath: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck]?.outputPath : undefined,
|
|
375
|
+
},
|
|
376
|
+
status: "success",
|
|
377
|
+
summary: `Compiled deck plan from canonical narrative with ${compiled.result.slideCount} slide${compiled.result.slideCount === 1 ? "" : "s"}.`,
|
|
378
|
+
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)),
|
|
379
|
+
})
|
|
380
|
+
}
|
|
381
|
+
writeDecksState(workspaceRoot, compiled.state)
|
|
382
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: compiled.result, deck: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck] : undefined, narrative: compiled.state.narrative }, null, 2)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (args.action === "reviewNarrative") {
|
|
386
|
+
const reviewed = reviewNarrativeState(state)
|
|
387
|
+
recordNarrativeReviewAction(reviewed.state, reviewed.result)
|
|
388
|
+
writeDecksState(workspaceRoot, reviewed.state)
|
|
389
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result, narrative: reviewed.state.narrative }, null, 2)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (args.action === "approveNarrative") {
|
|
393
|
+
const approved = approveNarrativeState(state, {
|
|
394
|
+
approvedBy: args.approvalBy,
|
|
395
|
+
scope: args.approvalScope,
|
|
396
|
+
note: args.approvalNote,
|
|
397
|
+
})
|
|
398
|
+
recordNarrativeApprovalAction(approved.state, approved.result)
|
|
399
|
+
writeDecksState(workspaceRoot, approved.state)
|
|
400
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: approved.result, narrative: approved.state.narrative }, null, 2)
|
|
401
|
+
}
|
|
402
|
+
|
|
225
403
|
if (args.action === "applyEvidenceCandidates") {
|
|
226
404
|
const candidateIds = args.candidateIds ?? []
|
|
227
405
|
if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
|