@cyber-dash-tech/revela 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +35 -29
  2. package/README.zh-CN.md +35 -29
  3. package/lib/commands/brief.ts +63 -0
  4. package/lib/commands/designs.ts +2 -2
  5. package/lib/commands/domains.ts +2 -2
  6. package/lib/commands/enable.ts +19 -19
  7. package/lib/commands/help.ts +7 -3
  8. package/lib/commands/init.ts +30 -19
  9. package/lib/commands/narrative.ts +160 -0
  10. package/lib/commands/review.ts +115 -1
  11. package/lib/decks-state.ts +46 -3
  12. package/lib/edit/prompt.ts +3 -0
  13. package/lib/inspection-context/compile.ts +159 -5
  14. package/lib/inspection-context/project.ts +20 -0
  15. package/lib/narrative-state/coverage.ts +100 -0
  16. package/lib/narrative-state/display.ts +219 -0
  17. package/lib/narrative-state/executive-brief.ts +246 -0
  18. package/lib/narrative-state/hash.ts +61 -0
  19. package/lib/narrative-state/map-html.ts +348 -0
  20. package/lib/narrative-state/map.ts +282 -0
  21. package/lib/narrative-state/normalize.ts +361 -0
  22. package/lib/narrative-state/project-compat.ts +14 -0
  23. package/lib/narrative-state/queries.ts +433 -0
  24. package/lib/narrative-state/readiness.ts +359 -0
  25. package/lib/narrative-state/render-plan.ts +250 -0
  26. package/lib/narrative-state/research-gaps.ts +191 -0
  27. package/lib/narrative-state/types.ts +172 -0
  28. package/lib/prompt-builder.ts +59 -26
  29. package/lib/workspace-state/evidence-status.ts +21 -1
  30. package/lib/workspace-state/graph.ts +174 -2
  31. package/lib/workspace-state/types.ts +13 -1
  32. package/package.json +1 -1
  33. package/plugin.ts +58 -2
  34. package/skill/NARRATIVE_SKILL.md +64 -0
  35. package/tools/decks.ts +265 -2
  36. package/tools/narrative-view.ts +84 -0
  37. package/tools/workspace-scan.ts +14 -1
@@ -0,0 +1,160 @@
1
+ import { mkdirSync, writeFileSync } from "fs"
2
+ import { tmpdir } from "os"
3
+ import { join } from "path"
4
+ import { openUrl as defaultOpenUrl } from "../edit/open"
5
+ import { hasDecksState, readDecksState } from "../decks-state"
6
+ import { buildNarrativeMap, formatNarrativeMap } from "../narrative-state/map"
7
+ import { renderNarrativeMapHtmlWithDisplay } from "../narrative-state/map-html"
8
+ import { emptyDisplayModel, type NarrativeViewLanguage, type ValidatedNarrativeDisplayModel } from "../narrative-state/display"
9
+
10
+ export interface NarrativeArgs {
11
+ language: NarrativeViewLanguage
12
+ raw: boolean
13
+ }
14
+
15
+ export type ParseNarrativeArgsResult = { ok: true; args: NarrativeArgs } | { ok: false; error: string }
16
+
17
+ export function parseNarrativeArgs(param: string): ParseNarrativeArgsResult {
18
+ const tokens = param.trim().split(/\s+/).filter(Boolean)
19
+ let language: NarrativeViewLanguage = "en"
20
+ let raw = false
21
+ const languageParts: string[] = []
22
+ for (const token of tokens) {
23
+ const normalized = token.toLowerCase()
24
+ if (normalized === "--raw") {
25
+ raw = true
26
+ continue
27
+ }
28
+ if (token.startsWith("--") && token.length > 2) {
29
+ language = normalizeLanguageRequest(token.slice(2))
30
+ continue
31
+ }
32
+ languageParts.push(token)
33
+ }
34
+ if (languageParts.length > 0) language = normalizeLanguageRequest(languageParts.join(" "))
35
+ return { ok: true, args: { language, raw } }
36
+ }
37
+
38
+ function normalizeLanguageRequest(value: string): NarrativeViewLanguage {
39
+ const trimmed = value.trim()
40
+ const normalized = trimmed.toLowerCase()
41
+ if (["en", "eng", "english"].includes(normalized)) return "en"
42
+ if (["cn", "zh", "zh-cn", "chinese"].includes(normalized)) return "zh-CN"
43
+ if (["jp", "ja", "ja-jp", "japanese"].includes(normalized)) return "ja-JP"
44
+ return trimmed || "en"
45
+ }
46
+
47
+ export async function handleNarrative(
48
+ options: { workspaceRoot: string; openBrowser?: boolean; openUrl?: (url: string) => void; language?: NarrativeViewLanguage; display?: ValidatedNarrativeDisplayModel },
49
+ send: (text: string) => Promise<void>,
50
+ ): Promise<void> {
51
+ try {
52
+ if (!hasDecksState(options.workspaceRoot)) {
53
+ await send("No `DECKS.json` found. Run `/revela init` first to initialize the narrative workspace.")
54
+ return
55
+ }
56
+
57
+ const state = readDecksState(options.workspaceRoot)
58
+ const map = buildNarrativeMap(state)
59
+ const markdown = formatNarrativeMap(map)
60
+
61
+ if (options.openBrowser) {
62
+ const htmlPath = writeNarrativeMapHtml(map, options.display ?? emptyDisplayModel(options.language ?? "en"))
63
+ const url = `file://${htmlPath}`
64
+ try {
65
+ ;(options.openUrl ?? defaultOpenUrl)(url)
66
+ await send(`Opened read-only narrative workspace: ${url}\n\n${markdown}`)
67
+ } catch (e: any) {
68
+ await send(`Read-only narrative workspace generated but could not open automatically: ${url}\n\n${e.message || String(e)}\n\n${markdown}`)
69
+ }
70
+ return
71
+ }
72
+
73
+ await send(markdown)
74
+ } catch (e: any) {
75
+ await send(`**Narrative map failed:** ${e.message || String(e)}`)
76
+ }
77
+ }
78
+
79
+ export function buildNarrativeViewPrompt(options: { workspaceRoot: string; language: NarrativeViewLanguage }): string {
80
+ if (!hasDecksState(options.workspaceRoot)) {
81
+ return "No `DECKS.json` found. Tell the user to run `/revela init` before opening the narrative view. Do not call any tool."
82
+ }
83
+
84
+ const map = buildNarrativeMap(readDecksState(options.workspaceRoot))
85
+ const projection = {
86
+ narrativeHash: map.snapshot.narrativeHash,
87
+ language: options.language,
88
+ snapshot: map.snapshot,
89
+ claims: map.claimFlow.map((claim) => ({
90
+ id: claim.id,
91
+ kind: claim.kind,
92
+ importance: claim.importance,
93
+ evidenceStatus: claim.evidenceStatus,
94
+ text: claim.text,
95
+ supportedScope: claim.supportedScope,
96
+ unsupportedScope: claim.unsupportedScope,
97
+ evidence: claim.evidence.map((evidence) => ({ source: evidence.source, strength: evidence.strength, findingsFile: evidence.findingsFile, location: evidence.location, quote: evidence.quote, caveat: evidence.caveat, unsupportedScope: evidence.unsupportedScope })),
98
+ })),
99
+ relations: map.claimRelations.map((relation) => ({ id: relation.id, fromClaimId: relation.fromClaimId, toClaimId: relation.toClaimId, relation: relation.relation, rationale: relation.rationale, inferred: relation.inferred })),
100
+ researchGaps: map.researchGaps.map((gap) => ({ id: gap.id, targetType: gap.targetType, targetId: gap.targetId, status: gap.status, priority: gap.priority, question: gap.question })),
101
+ artifactCoverage: map.artifactCoverage.map((artifact) => ({ type: artifact.type, outputPath: artifact.outputPath, stale: artifact.stale, slideRefs: artifact.slideRefs.map((ref) => ({ claimId: ref.claimId, slideIndex: ref.slideIndex, role: ref.role, match: ref.match, location: ref.location })) })),
102
+ }
103
+
104
+ return `Prepare the read-only Revela narrative UI display model.
105
+
106
+ Target language request: ${options.language}
107
+ - The language value is passed from the user's /revela narrative arguments. Interpret it as the desired UI/display language.
108
+ - Examples: --cn maps to zh-CN, --jp maps to ja-JP, while --fr, --de, --es, --ko, --Arabic, --Portuguese-BR, or a written language name should be localized normally into that requested language.
109
+ - Default /revela narrative language is en when the user provides no language request.
110
+
111
+ You must call the \`revela-narrative-view\` tool exactly once.
112
+
113
+ Hard rules:
114
+ - Do not mutate DECKS.json, deck HTML, evidence, claims, relations, approvals, or artifacts.
115
+ - Do not invent new claims, evidence, relations, slide coverage, source paths, findings files, quotes, or caveats.
116
+ - Preserve every claimId exactly.
117
+ - Preserve every relation endpoint exactly: fromClaimId, toClaimId, relation.
118
+ - You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
119
+ - For inferred relations, do not provide relation displayLabel or displayRationale; inferred relations are unconfirmed order notes, not causal/support/dependency judgments.
120
+ - relation displayRationale may only localize or clarify an existing canonical relation rationale. If relation.rationale is missing or the relation is inferred, do not provide displayRationale; the UI will show the missing or inferred status.
121
+ - Keep source paths, findings files, claim IDs, narrative hash, and numbers unchanged.
122
+ - Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
123
+ - Do not translate claim IDs, relation endpoints, narrative hash, source paths, findings files, URLs, numbers, or quoted/source facts.
124
+ - Use natural business and manufacturing terminology in the target language, not word-by-word machine translation.
125
+ - If a fact is missing, describe it as missing instead of filling it in.
126
+
127
+ Chinese localization rules when the target language request is Chinese, zh, zh-CN, --cn, 中文, or Simplified Chinese:
128
+ - Use natural business/manufacturing Chinese, not word-by-word machine translation.
129
+ - In manufacturing, industrial AI, automation, and autonomous systems context, translate "autonomy" as "自主化", "自主能力", or "自主系统". Do not translate it as "自治".
130
+ - Translate "autonomous" as "自主的" / "自主化的" where appropriate, not "自治的".
131
+ - Translate "architectural" as "架构层面的", "架构性", or "架构问题" according to context.
132
+ - Slug-like or kebab-case claim text such as "autonomy-is-architectural" should become a readable displayTitle such as "自主化是架构问题" or "自主化必须作为架构问题处理", not a literal token-by-token translation.
133
+ - If the canonical claim text is only a slug, preserve the claimId exactly but write displayTitle as a readable claim title.
134
+
135
+ Call \`revela-narrative-view\` with:
136
+ - language: ${options.language}
137
+ - narrativeHash: ${map.snapshot.narrativeHash}
138
+ - displayModel.version: 1
139
+ - displayModel.language: ${options.language}
140
+ - displayModel.claimCards only for claim IDs listed below
141
+ - displayModel.relations only for relations listed below
142
+
143
+ Compact deterministic narrative map:
144
+
145
+ \`\`\`json
146
+ ${JSON.stringify(projection, null, 2)}
147
+ \`\`\``
148
+ }
149
+
150
+ export function writeNarrativeMapHtml(map: ReturnType<typeof buildNarrativeMap>, display: ValidatedNarrativeDisplayModel = emptyDisplayModel("en")): string {
151
+ const dir = join(tmpdir(), "revela-narrative")
152
+ mkdirSync(dir, { recursive: true })
153
+ const file = join(dir, `${safeFilePart(map.snapshot.narrativeId)}-${map.snapshot.narrativeHash}.html`)
154
+ writeFileSync(file, renderNarrativeMapHtmlWithDisplay(map, display), "utf-8")
155
+ return file
156
+ }
157
+
158
+ function safeFilePart(value: string): string {
159
+ return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "narrative"
160
+ }
@@ -6,15 +6,129 @@ export function buildReviewPrompt({
6
6
  }: {
7
7
  exists: boolean
8
8
  workspaceRoot?: string
9
+ }): string {
10
+ const state = exists
11
+ ? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
12
+ : `${DECKS_STATE_FILE} does not exist yet. Create or normalize it through the revela-decks tool only if there is enough workspace narrative context.`
13
+
14
+ return `Review Revela narrative readiness.
15
+
16
+ Goal:
17
+ - Use ${DECKS_STATE_FILE} as the compatibility workspace-state file, but review the canonical narrative state first: audience, belief shift, decision/action, thesis, central claims, evidence boundaries, objections, risks, and approval state.
18
+ - Treat this as a narrative readiness review, not a deck HTML write-readiness review.
19
+ - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
20
+ - Call \`revela-decks\` action \`reviewNarrative\` as the authoritative deterministic readiness engine.
21
+ - Do not call \`revela-decks\` action \`review\` here. That action is the deck/artifact gate and belongs to \`/revela deck --review\`.
22
+ - Do not treat legacy \`writeReadiness.status\`, old review snapshots, or an existing HTML deck as narrative approval.
23
+ - Do not write or overwrite \`decks/*.html\` during narrative review.
24
+ - If the narrative is \`ready_for_approval\`, ask whether the user wants to approve it or revise it. Do not approve automatically.
25
+ - Only call \`revela-decks\` action \`approveNarrative\` when the user explicitly asks to approve or override.
26
+
27
+ Current state:
28
+ - ${state}
29
+ ${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
30
+
31
+ Workspace boundary rules:
32
+ - Stay strictly inside the current workspace root for every scan, glob, read, and write.
33
+ - Do not search parent directories, home directories, or unrelated absolute directories.
34
+ - Do not use \`~\`, \`..\`, or parent-directory traversal to discover files.
35
+ - For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
36
+
37
+ Workflow:
38
+ 1. Call \`revela-decks\` with action \`read\` to inspect the current workspace state.
39
+ 2. If ${DECKS_STATE_FILE} is missing or empty, do not invent a deck plan, slide count, design, output path, or visual style. Report the smallest narrative inputs needed, usually audience, belief-before, belief-after, decision/action, thesis, central claims, evidence availability, objections, and risks.
40
+ 3. If legacy deck state exists, let the tool-normalized canonical narrative derived from \`narrativeBrief\`, slide roles, slide content, and slide evidence be reviewed. Do not assume old deck readiness means approval.
41
+ 4. Call \`revela-decks\` action \`reviewNarrative\`. Use its returned \`status\`, \`blockers\`, \`warnings\`, \`issues\`, \`narrativeHash\`, \`approval\`, and \`nextActions\` as authoritative.
42
+ 5. If research findings have been saved but not attached or evidence-bound, report them as unattached research state, not proof.
43
+ 6. If central claims lack required evidence, report the named claim and the exact next action: attach findings, bind evidence, run targeted research, narrow unsupported scope, or rewrite the claim.
44
+ 7. If approval is missing or stale, clearly distinguish \`ready_for_approval\`, \`approved\`, and render override.
45
+
46
+ Report format:
47
+ - Start with \`Narrative readiness: <status>\`.
48
+ - Include \`Narrative hash: <hash>\` when returned.
49
+ - If blocked or needs research, list each blocker with issue type, claim text when available, and suggested next action.
50
+ - If warnings exist, list them after blockers as residual risks.
51
+ - If approval is missing, ask whether the user wants to approve the narrative or revise it.
52
+ - If approval is stale, say the prior approval no longer matches the current narrative hash.
53
+ - Keep deck/artifact readiness separate. If the user wants to review slide-writing readiness, tell them to run \`/revela deck --review\`.
54
+
55
+ Rules:
56
+ - Do not write or overwrite \`decks/*.html\` during narrative review.
57
+ - Do not call \`revela-decks review\` during narrative review.
58
+ - Do not apply evidence candidates, bind evidence, or rewrite slide text unless the user explicitly asks.
59
+ - Do not store secrets, credentials, tokens, or sensitive personal information.
60
+ - Do not add inferred user preferences to long-term preference state.
61
+
62
+ Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, then call \`revela-decks\` action \`reviewNarrative\`.`
63
+ }
64
+
65
+ export function buildDeckPrompt({
66
+ exists,
67
+ workspaceRoot,
68
+ }: {
69
+ exists: boolean
70
+ workspaceRoot?: string
71
+ }): string {
72
+ const state = exists
73
+ ? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
74
+ : `${DECKS_STATE_FILE} does not exist yet. Do not invent a deck; initialize narrative state first with /revela init.`
75
+
76
+ return `Begin Revela deck render handoff.
77
+
78
+ Goal:
79
+ - Treat this as the explicit transition from approved narrative state to deck render planning.
80
+ - Use the deck-render prompt mode for design, layout, component, HTML, QA, and deck artifact rules.
81
+ - Do not write or overwrite \`decks/*.html\` until the narrative handoff and deck/artifact gate are both satisfied.
82
+ - Do not treat legacy \`writeReadiness.status\`, old review snapshots, or existing HTML decks as narrative approval.
83
+ - Do not bypass the deck HTML contract, review snapshot freshness, source-trace expectations, or export preflight protections.
84
+
85
+ Current state:
86
+ - ${state}
87
+ ${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
88
+
89
+ Workflow:
90
+ 1. Call \`revela-decks\` action \`read\`.
91
+ 2. Call \`revela-decks\` action \`reviewNarrative\` before planning deck slides.
92
+ 3. If narrative readiness is \`approved\`, continue. If it is \`ready_for_approval\`, ask the user for explicit approval before continuing. If it is blocked, stale, or needs research, stop and report the smallest next action. Do not call \`approveNarrative\` unless the user explicitly approves or requests a render override.
93
+ 4. After approval or explicit render override exists, call \`revela-decks\` action \`compileDeckPlan\`. This projects canonical narrative claims and evidence bindings into compatibility \`slides[]\` and \`slides[].evidence[]\`; it must not write HTML.
94
+ 5. If \`compileDeckPlan\` returns \`skipped\`, stop and report the reason. Do not invent slide specs manually to bypass approval.
95
+ 6. Ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
96
+ 7. Update only deck/artifact metadata through \`revela-decks upsertDeck\` / \`upsertSlides\` when required by confirmed design/layout choices. Do not change canonical narrative claims unless the user asks to revise the narrative.
97
+ 8. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing.
98
+ 9. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. If not ready, report blockers and stop.
99
+
100
+ Report format before any HTML write:
101
+ - Start with \`Deck handoff: <status>\`.
102
+ - Include narrative readiness status and narrative hash when available.
103
+ - Include whether \`compileDeckPlan\` compiled or skipped.
104
+ - If deck/artifact review is blocked, list blockers separately from narrative blockers.
105
+ - If proceeding to HTML writing, state which approved narrative hash and deck review snapshot authorized the artifact work.
106
+
107
+ Rules:
108
+ - \`compileDeckPlan\` is the canonical narrative-to-deck planning path. Do not manually invent slide specs to avoid it.
109
+ - Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
110
+ - Applying evidence candidates, rewriting canonical claims, or approving narratives requires explicit user instruction.
111
+ - Do not store secrets, credentials, tokens, or sensitive personal information.
112
+
113
+ Start now by reading ${DECKS_STATE_FILE}, reviewing narrative readiness, and then compiling the deck plan only if approval or explicit render override is current.`
114
+ }
115
+
116
+ export function buildDeckReviewPrompt({
117
+ exists,
118
+ workspaceRoot,
119
+ }: {
120
+ exists: boolean
121
+ workspaceRoot?: string
9
122
  }): string {
10
123
  const state = exists
11
124
  ? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
12
125
  : `${DECKS_STATE_FILE} does not exist yet. Create it through the revela-decks tool if there is enough deck context.`
13
126
 
14
- return `Review Revela deck write readiness.
127
+ return `Review Revela deck/artifact write readiness.
15
128
 
16
129
  Goal:
17
130
  - Use ${DECKS_STATE_FILE} as the source of truth for whether the current workspace deck is ready to be written to \`decks/*.html\`.
131
+ - Treat this as an artifact gate for deck rendering, not strategic narrative approval. Narrative readiness is reviewed by \`/revela review\`.
18
132
  - Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, production status, and the 0.9 narrative compiler brief when available.
19
133
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
20
134
  - Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
@@ -17,6 +17,8 @@ import {
17
17
  latestReviewSnapshotForTarget,
18
18
  } from "./workspace-state/review-snapshots"
19
19
  import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
20
+ import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
21
+ import type { NarrativeStateV1 } from "./narrative-state/types"
20
22
 
21
23
  export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
22
24
 
@@ -24,10 +26,12 @@ export type DeckProductionStatus = "planning" | "blocked" | "ready" | "written"
24
26
  export type SlideProductionStatus = "planned" | "ready" | "written" | "qa_passed" | "qa_failed"
25
27
  export type WriteReadinessStatus = "blocked" | "ready" | "written"
26
28
  export type NarrativeRole = "context" | "tension" | "evidence" | "recommendation" | "risk" | "ask" | "appendix" | "close"
29
+ export type SlideClaimRefRole = "primary" | "supporting" | "evidence" | "risk" | "objection"
27
30
 
28
31
  export interface DecksState {
29
32
  version: 1
30
33
  activeDeck?: string
34
+ narrative?: NarrativeStateV1
31
35
  workspace: {
32
36
  brief?: string
33
37
  sourceMaterials: SourceMaterial[]
@@ -132,6 +136,9 @@ export interface SlideSpec {
132
136
  layout: string
133
137
  qa?: boolean
134
138
  components: string[]
139
+ claimIds?: string[]
140
+ claimRefs?: SlideClaimRef[]
141
+ evidenceBindingIds?: string[]
135
142
  content: {
136
143
  headline?: string
137
144
  body?: string[]
@@ -145,6 +152,12 @@ export interface SlideSpec {
145
152
  notes?: string
146
153
  }
147
154
 
155
+ export interface SlideClaimRef {
156
+ claimId: string
157
+ role: SlideClaimRefRole
158
+ note?: string
159
+ }
160
+
148
161
  export interface EvidenceRef {
149
162
  source: string
150
163
  quote?: string
@@ -349,15 +362,15 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
349
362
  }
350
363
 
351
364
  export function readDecksState(workspaceRoot: string): DecksState {
352
- return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
365
+ return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
353
366
  }
354
367
 
355
368
  export function writeDecksState(workspaceRoot: string, state: DecksState): void {
356
- writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
369
+ writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
357
370
  }
358
371
 
359
372
  export function readOrCreateDecksState(workspaceRoot: string): DecksState {
360
- return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
373
+ return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
361
374
  }
362
375
 
363
376
  export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
@@ -717,6 +730,7 @@ function normalizeDecksState(input: DecksState): DecksState {
717
730
  const state: DecksState = {
718
731
  version: 1,
719
732
  activeDeck: input.activeDeck ? normalizeSlug(input.activeDeck) : undefined,
733
+ narrative: normalizeCanonicalNarrativeState(input.narrative, input.activeDeck || "workspace"),
720
734
  workspace: {
721
735
  brief: input.workspace?.brief,
722
736
  sourceMaterials: input.workspace?.sourceMaterials ?? [],
@@ -745,6 +759,12 @@ function normalizeDecksState(input: DecksState): DecksState {
745
759
  return state
746
760
  }
747
761
 
762
+ function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
763
+ const state = normalizeDecksState(input)
764
+ if (!state.narrative && currentDeckKey(state)) state.narrative = normalizeNarrativeState(state)
765
+ return state
766
+ }
767
+
748
768
  function currentDeckKey(state: DecksState): string | undefined {
749
769
  if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
750
770
  const keys = Object.keys(state.decks)
@@ -1468,6 +1488,9 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
1468
1488
  title: slide.title ?? "",
1469
1489
  layout: slide.layout ?? "",
1470
1490
  components: slide.components ?? [],
1491
+ claimIds: normalizeTextList(slide.claimIds),
1492
+ claimRefs: normalizeSlideClaimRefs(slide.claimRefs),
1493
+ evidenceBindingIds: normalizeTextList(slide.evidenceBindingIds),
1471
1494
  content: slide.content ?? {},
1472
1495
  evidence: slide.evidence ?? [],
1473
1496
  status: slide.status ?? "planned",
@@ -1475,6 +1498,26 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
1475
1498
  .sort((a, b) => a.index - b.index)
1476
1499
  }
1477
1500
 
1501
+ function normalizeSlideClaimRefs(refs: SlideClaimRef[] | undefined): SlideClaimRef[] {
1502
+ const seen = new Set<string>()
1503
+ const out: SlideClaimRef[] = []
1504
+ for (const ref of refs ?? []) {
1505
+ const claimId = cleanOptionalText(ref.claimId)
1506
+ if (!claimId) continue
1507
+ const role = isSlideClaimRefRole(ref.role) ? ref.role : "supporting"
1508
+ const key = `${claimId}:${role}`
1509
+ if (seen.has(key)) continue
1510
+ seen.add(key)
1511
+ const note = cleanOptionalText(ref.note)
1512
+ out.push({ claimId, role, ...(note ? { note } : {}) })
1513
+ }
1514
+ return out
1515
+ }
1516
+
1517
+ function isSlideClaimRefRole(value: string | undefined): value is SlideClaimRefRole {
1518
+ return value === "primary" || value === "supporting" || value === "evidence" || value === "risk" || value === "objection"
1519
+ }
1520
+
1478
1521
  function normalizeNarrativeBrief(brief: NarrativeBrief | undefined): NarrativeBrief | undefined {
1479
1522
  if (!brief) return undefined
1480
1523
  const normalized: NarrativeBrief = {
@@ -71,6 +71,9 @@ Instructions:
71
71
  - Make the smallest targeted change that satisfies the user's comment.
72
72
  - If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
73
73
  - Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
74
+ - Preserve the narrative boundary: if the requested edit changes audience framing, belief shift, decision/action, thesis, recommendation, claim wording, evidence scope, caveat, risk, objection, or decision ask, do not patch the HTML directly. Explain that the canonical narrative must be updated first through ${"`revela-decks`"} action ${"`upsertNarrative`"}, then reviewed/approved or explicitly overridden before updating the deck projection.
75
+ - Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, or deck HTML contract fixes may remain an artifact-level edit.
76
+ - If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
74
77
  - Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
75
78
  - Do not rewrite unrelated slides or broad sections of the deck.
76
79
  - Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.