@cyber-dash-tech/revela 0.16.4 → 0.17.1

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 (57) hide show
  1. package/README.md +7 -5
  2. package/README.zh-CN.md +7 -5
  3. package/lib/commands/brief.ts +9 -0
  4. package/lib/commands/help.ts +5 -2
  5. package/lib/commands/init.ts +42 -27
  6. package/lib/commands/narrative.ts +39 -6
  7. package/lib/commands/research.ts +36 -20
  8. package/lib/commands/review.ts +35 -28
  9. package/lib/ctx.ts +1 -1
  10. package/lib/decks-state.ts +38 -4
  11. package/lib/edit/prompt.ts +1 -1
  12. package/lib/hook-notifications.ts +53 -0
  13. package/lib/media/download.ts +23 -3
  14. package/lib/media/save.ts +1 -0
  15. package/lib/media/types.ts +1 -0
  16. package/lib/narrative-state/display.ts +74 -4
  17. package/lib/narrative-state/map-html.ts +242 -107
  18. package/lib/narrative-state/render-plan.ts +238 -35
  19. package/lib/narrative-state/research-binding-eval.ts +260 -0
  20. package/lib/narrative-state/research-gaps.ts +2 -88
  21. package/lib/narrative-vault/authoring-contract.ts +127 -0
  22. package/lib/narrative-vault/authoring-guard.ts +122 -0
  23. package/lib/narrative-vault/auto-compile.ts +134 -0
  24. package/lib/narrative-vault/bootstrap.ts +63 -0
  25. package/lib/narrative-vault/cache.ts +14 -0
  26. package/lib/narrative-vault/compile-mirror.ts +45 -0
  27. package/lib/narrative-vault/compile.ts +350 -0
  28. package/lib/narrative-vault/constants.ts +6 -0
  29. package/lib/narrative-vault/diagnostic-report.ts +117 -0
  30. package/lib/narrative-vault/export.ts +71 -0
  31. package/lib/narrative-vault/frontmatter.ts +41 -0
  32. package/lib/narrative-vault/hook-targets.ts +40 -0
  33. package/lib/narrative-vault/index.ts +18 -0
  34. package/lib/narrative-vault/inventory.ts +392 -0
  35. package/lib/narrative-vault/markdown-qa.ts +237 -0
  36. package/lib/narrative-vault/markdown.ts +34 -0
  37. package/lib/narrative-vault/migration.ts +52 -0
  38. package/lib/narrative-vault/mutate.ts +361 -0
  39. package/lib/narrative-vault/paths.ts +19 -0
  40. package/lib/narrative-vault/read.ts +52 -0
  41. package/lib/narrative-vault/relations.ts +32 -0
  42. package/lib/narrative-vault/source-loader.ts +19 -0
  43. package/lib/narrative-vault/timestamp.ts +32 -0
  44. package/lib/narrative-vault/types.ts +44 -0
  45. package/lib/qa/checks.ts +206 -5
  46. package/lib/qa/measure.ts +63 -1
  47. package/lib/refine/server.ts +157 -20
  48. package/lib/source-materials.ts +98 -0
  49. package/lib/tool-result.ts +34 -0
  50. package/package.json +2 -2
  51. package/plugin.ts +60 -22
  52. package/skill/NARRATIVE_SKILL.md +25 -10
  53. package/skill/SKILL.md +6 -1
  54. package/tools/decks.ts +363 -67
  55. package/tools/narrative-view.ts +16 -0
  56. package/tools/research-save.ts +3 -0
  57. package/tools/workspace-scan.ts +1 -0
@@ -35,10 +35,10 @@ Workspace boundary rules:
35
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
36
 
37
37
  Workflow:
38
- 1. Call \`revela-decks\` with action \`read\` to inspect the current workspace state.
38
+ 1. Call \`revela-decks\` with action \`read\` and \`summary: true\` to inspect the current workspace state, \`vaultDiagnostics\`, \`markdownQa\`, and \`narrativeInventory\` when a vault exists.
39
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
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.
41
+ 4. Call \`revela-decks\` action \`reviewNarrative\`. Use its returned \`status\`, \`blockers\`, \`warnings\`, \`issues\`, \`narrativeHash\`, \`approval\`, and \`nextActions\` as authoritative. If the read summary returned \`markdownQa.repairCards\` or \`vaultDiagnostics\`, report Markdown QA repair cards and compile diagnostics before readiness blockers and include file/node/code/message plus smallest repair or suggested next action.
42
42
  5. If research findings have been saved but not attached or evidence-bound, report them as unattached research state, not proof.
43
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
44
  7. If approval is missing or stale, clearly distinguish \`ready_for_approval\`, \`approved\`, and render override.
@@ -46,7 +46,7 @@ Workflow:
46
46
  Report format:
47
47
  - Start with \`Narrative readiness: <status>\`.
48
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.
49
+ - If blocked or needs research, list each blocker with issue type, claim text when available, and suggested next action. Keep Markdown QA repair cards separate from compiler diagnostics and narrative readiness blockers.
50
50
  - If warnings exist, list them after blockers as residual risks.
51
51
  - If approval is missing, ask whether the user wants to approve the narrative or revise it.
52
52
  - If approval is stale, say the prior approval no longer matches the current narrative hash.
@@ -89,28 +89,30 @@ Current state:
89
89
  ${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
90
90
 
91
91
  Workflow:
92
- 1. Call \`revela-decks\` action \`read\`.
92
+ 1. Call \`revela-decks\` action \`read\` with \`summary: true\`.
93
93
  2. Call \`revela-decks\` action \`reviewNarrative\` before planning deck slides.
94
- 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.
94
+ 3. If the read summary returned \`markdownQa.blockers\` or \`vaultDiagnostics.blockers\`, stop before deck planning and report Markdown QA repair cards separately from compile diagnostics with file/node/code/message and smallest repair or suggested next action. 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.
95
95
  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.
96
96
  5. If \`compileDeckPlan\` returns \`skipped\`, stop and report the reason. Do not invent slide specs manually to bypass approval.
97
- 6. Present the compiled deck plan to the user and include a low-fidelity layout sketch for every slide. The plan must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. The sketch is ASCII/text structure only; do not generate visual images or HTML mockups.
98
- 7. Stop after presenting the plan. Ask the user to confirm or request changes. Do not call \`revela-decks review\`, do not fetch design context, and do not write HTML in the same turn unless the user had already explicitly confirmed the current plan before this command.
99
- 8. Only after explicit user confirmation of the current slide plan, call \`revela-decks\` action \`confirmDeckPlan\` with \`approvalBy=user\` and a compact \`approvalNote\`.
100
- 9. After confirmation is recorded, ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
101
- 10. 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.
102
- 11. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing. If it reports \`slide_plan_unconfirmed\`, stop and ask for explicit deck-plan confirmation.
103
- 12. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
104
- 13. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
105
- 14. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
97
+ 6. Treat each compiled slide's \`visuals[]\` and \`content.data.visualIntent\` as required render instructions, not optional decoration. Do not downgrade a planned chart, metric card, evidence table, comparison grid, risk matrix, steps view, or media brief into generic bullets unless the plan is revised and reconfirmed.
98
+ 7. Present the compiled deck plan to the user and include a low-fidelity layout sketch for every slide. The plan must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. The sketch is ASCII/text structure only; do not generate visual images or HTML mockups.
99
+ 8. Stop after presenting the plan. Ask the user to confirm or request changes. Do not call \`revela-decks review\`, do not fetch design context, and do not write HTML in the same turn unless the user had already explicitly confirmed the current plan before this command.
100
+ 9. Only after explicit user confirmation of the current slide plan, call \`revela-decks\` action \`confirmDeckPlan\` with \`approvalBy=user\` and a compact \`approvalNote\`.
101
+ 10. After confirmation is recorded, ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
102
+ 11. 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.
103
+ 12. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing. If it reports \`slide_plan_unconfirmed\`, stop and ask for explicit deck-plan confirmation.
104
+ 13. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
105
+ 14. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
106
+ 15. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
106
107
 
107
108
  Deck plan report format:
108
109
  - Start with \`Deck plan: awaiting confirmation\` when a plan was compiled and has not yet been confirmed.
109
110
  - Include narrative readiness status and narrative hash when available.
111
+ - Include Markdown QA repair cards and vault diagnostic blockers or warnings when returned by \`read(summary: true)\`; blockers prevent deck planning until fixed.
110
112
  - Include whether \`compileDeckPlan\` compiled or skipped.
111
113
  - Include \`Required structure: Cover + Table of Contents + Closing\` and do not omit any of those slides.
112
114
  - Include a \`Chapters\` section before the slide list. It must list 3-5 TOC headings, their slide ranges, and the non-structural slides assigned to each chapter.
113
- - For every slide, include: slide index, title, purpose, narrative role, low-fidelity layout sketch, layout, components, primary/supporting claim ids, evidence binding ids or source summary, visual intent, and caveats/unsupported scope.
115
+ - For every slide, include: slide index, title, purpose, narrative role, low-fidelity layout sketch, layout, components, primary/supporting claim ids, evidence binding ids or source summary, visual intent from \`content.data.visualIntent\`, visual brief from \`visuals[]\`, and caveats/unsupported scope.
114
116
  - Use this sketch style or similarly simple ASCII boxes:
115
117
 
116
118
  \`\`\`text
@@ -132,6 +134,8 @@ Components:
132
134
  Primary claim:
133
135
  Supporting claims:
134
136
  Evidence bindings:
137
+ Visual intent:
138
+ Visual brief:
135
139
  Caveats / unsupported scope:
136
140
  \`\`\`
137
141
  - End by asking the user to confirm the deck plan or request changes.
@@ -145,6 +149,7 @@ Report format before any HTML write after confirmation:
145
149
 
146
150
  Rules:
147
151
  - \`compileDeckPlan\` is the canonical narrative-to-deck planning path. Do not manually invent slide specs to avoid it.
152
+ - Visual intent is part of the confirmed plan. During HTML generation, satisfy the planned component/visual brief using fetched design components; do not collapse planned visuals into prose-only bullets.
148
153
  - Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
149
154
  - Cover, Table of Contents, and Closing are mandatory deck structure. TOC chapter headings must match the chapter grouping used for generation.
150
155
  - Do not generate the complete deck content in one broad pass after confirmation. Work chapter by chapter while keeping the artifact valid after each write.
@@ -181,7 +186,7 @@ Goal:
181
186
  - Treat \`revela-narrative-reviewer\` findings as advisory critique only. Do not represent them as \`revela-decks\` readiness issues, blockers, or authoritative \`writeReadiness\`.
182
187
  - Treat source trace mapping as part of evidence readiness: when research findings have been read, relevant findings should appear in slide-level \`slides[].evidence[]\` records rather than only in raw research files.
183
188
  - When \`revela-decks review\` returns \`evidenceCandidates\`, treat them as conservative binding candidates only. They are not proof that the full slide is supported, and they are not automatically applied to \`slides[].evidence[]\`. If a candidate has \`sourceKind: "researchesFallback"\`, say it was discovered from workspace \`researches/\` files that are not currently referenced by \`researchPlan\`.
184
- - When an evidence candidate includes \`evidenceDraft\`, report it as a proposed slide evidence record with its \`candidateId\`; it still requires explicit user/agent confirmation before calling \`revela-decks\` action \`applyEvidenceCandidates\`. Also report \`unsupportedScope\` and \`recommendedRewrite\` so partial evidence is not stretched to future-state claims.
189
+ - When an evidence candidate includes \`evidenceDraft\`, report it as a proposed slide evidence record with its \`candidateId\`; it still requires explicit user/agent confirmation before binding. Binding canonical evidence means using \`initNarrativeVault\` if needed, writing \`revela-narrative/evidence/*.md\` with explicit source trace, and running \`compileNarrativeVault\`. Also report \`unsupportedScope\` and \`recommendedRewrite\` so partial evidence is not stretched to future-state claims.
185
190
  - When a missing-evidence issue has \`evidenceCandidateSearch\`, use it to explain search coverage: which \`researchPlan\` findings were searched, which fallback \`researches/**/*.md\` files were searched, and any near misses that were below binding threshold.
186
191
 
187
192
  Current state:
@@ -195,17 +200,18 @@ Workspace boundary rules:
195
200
  - 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.
196
201
 
197
202
  Workflow:
198
- 1. Call \`revela-decks\` with action \`read\` for the current workspace deck.
199
- 2. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, researchPlan, and narrativeBrief if the story intent is clear. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
200
- 3. If \`researchPlan[].status\` is \`done\` or \`read\` and \`researchPlan[].findingsFile\` exists, verify that evidence-sensitive slide claims are backed by compact \`slides[].evidence[]\` records that reference the relevant findings file or source material where known. The review tool may surface conservative \`evidenceCandidates\` for missing evidence by matching slide text against those findings files, and may fall back to bounded workspace \`researches/**/*.md\` discovery when the research plan has no matching findings file; report these as candidate bindings, not as already-bound evidence.
201
- 4. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, narrativeRole, layout, components, structured content, evidence, visuals, and status. Use only lightweight narrativeRole values that are clear from the plan: \`context\`, \`tension\`, \`evidence\`, \`recommendation\`, \`risk\`, \`ask\`, \`appendix\`, or \`close\`.
202
- 5. Prefer evidence records with \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, \`caveat\`, \`extractedTextPath\`, or \`extractedManifestPath\` when those fields are known from research files or extracted workspace materials.
203
- 6. Do not invent quotes, page references, locations, URLs, caveats, or extraction paths. If source trace is missing, preserve the blocker or warning and report exactly what trace is needed.
204
- 7. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
205
- 8. For substantial decision decks, preserve a compact \`narrativeBrief\` through \`upsertDeck\` when the conversation or confirmed plan supports it. Do not invent stakeholder beliefs, objections, or risks; leave gaps visible if unknown.
206
- 9. For substantial decision decks, launch the Task subagent with \`subagent_type: "revela-narrative-reviewer"\` after deck/slides are up to date. Ask it to read the current \`DECKS.json\`, run only its fixed rubric, use stable finding IDs, return \`Findings: none\` when all checks pass, and avoid optional pre-write improvements. Do not ask it to write state, call \`revela-decks review\`, or produce HTML.
207
- 10. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
208
- 11. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool. If warnings exist, list them after blockers as residual risks; separate evidence/source warnings from narrative warnings when possible. If the review result includes \`diagnostics\`, include a \`Plan and coverage diagnostics\` section with plan quality blockers/warnings, artifact \`coverageStatus\`, \`missingClaimIds\`, \`affectedClaimIds\`, stale reasons, and \`nextActions\`. If the review result includes \`evidenceCandidates\`, add a separate \`Candidate evidence bindings\` section with candidateId, slide index/title, supported claim scope, sourceKind, findingsFile/sourcePath, quote/snippet, caveat, evidenceDraft summary, unsupportedScope, and recommendedRewrite. Tell the user they may explicitly ask to apply selected candidate IDs; do not apply them during review. If candidates are absent but \`evidenceCandidateSearch\` is present, briefly report searched file counts and the best near misses so the user can tell whether review failed to search or searched but did not find a bindable match. If the reviewer returned findings, include them in a separate \`Narrative reviewer notes\` section and label them advisory.
203
+ 1. Call \`revela-decks\` with action \`read\` and \`summary: true\` for the current workspace deck and any \`markdownQa\`, \`vaultDiagnostics\`, and \`narrativeInventory\`.
204
+ 2. If the read summary returned \`markdownQa.blockers\` or \`vaultDiagnostics.blockers\`, report Markdown QA repair cards separately from compile diagnostics before artifact readiness with file/node/code/message and smallestRepair/suggestedAction; stop before \`revela-decks review\`, deck HTML writes, or export guidance until the blocker is fixed.
205
+ 3. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, researchPlan, and narrativeBrief if the story intent is clear. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
206
+ 4. If \`researchPlan[].status\` is \`done\` or \`read\` and \`researchPlan[].findingsFile\` exists, verify that evidence-sensitive slide claims are backed by compact \`slides[].evidence[]\` records that reference the relevant findings file or source material where known. The review tool may surface conservative \`evidenceCandidates\` for missing evidence by matching slide text against those findings files, and may fall back to bounded workspace \`researches/**/*.md\` discovery when the research plan has no matching findings file; report these as candidate bindings, not as already-bound evidence.
207
+ 5. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, narrativeRole, layout, components, structured content, evidence, visuals, and status. Use only lightweight narrativeRole values that are clear from the plan: \`context\`, \`tension\`, \`evidence\`, \`recommendation\`, \`risk\`, \`ask\`, \`appendix\`, or \`close\`.
208
+ 6. Prefer evidence records with \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, \`caveat\`, \`extractedTextPath\`, or \`extractedManifestPath\` when those fields are known from research files or extracted workspace materials.
209
+ 7. Do not invent quotes, page references, locations, URLs, caveats, or extraction paths. If source trace is missing, preserve the blocker or warning and report exactly what trace is needed.
210
+ 8. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
211
+ 9. For substantial decision decks, preserve a compact \`narrativeBrief\` through \`upsertDeck\` when the conversation or confirmed plan supports it. Do not invent stakeholder beliefs, objections, or risks; leave gaps visible if unknown.
212
+ 10. For substantial decision decks, launch the Task subagent with \`subagent_type: "revela-narrative-reviewer"\` after deck/slides are up to date. Ask it to read the current \`DECKS.json\`, run only its fixed rubric, use stable finding IDs, return \`Findings: none\` when all checks pass, and avoid optional pre-write improvements. Do not ask it to write state, call \`revela-decks review\`, or produce HTML.
213
+ 11. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
214
+ 12. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool. If warnings exist, list them after blockers as residual risks; separate evidence/source warnings from narrative warnings when possible. If the read summary or review result includes \`markdownQa\`, \`vaultDiagnostics\`, or \`diagnosticReport\`, include \`Markdown QA\` and \`Vault diagnostics\` sections before artifact readiness with file/node/code/message and smallestRepair/suggestedAction. If the review result includes \`diagnostics\`, include a \`Plan and coverage diagnostics\` section with plan quality blockers/warnings, artifact \`coverageStatus\`, \`missingClaimIds\`, \`affectedClaimIds\`, stale reasons, and \`nextActions\`. If the review result includes \`evidenceCandidates\`, add a separate \`Candidate evidence bindings\` section with candidateId, slide index/title, supported claim scope, sourceKind, findingsFile/sourcePath, quote/snippet, caveat, evidenceDraft summary, unsupportedScope, and recommendedRewrite. Tell the user they may explicitly ask to apply selected candidate IDs; do not apply them during review. If candidates are absent but \`evidenceCandidateSearch\` is present, briefly report searched file counts and the best near misses so the user can tell whether review failed to search or searched but did not find a bindable match. If the reviewer returned findings, include them in a separate \`Narrative reviewer notes\` section and label them advisory.
209
215
 
210
216
  Minimum conditions for \`ready\`:
211
217
  - Topic, audience, slide count, language, and visual style/design are decided.
@@ -231,8 +237,9 @@ Report format:
231
237
  - Do not convert \`revela-narrative-reviewer\` advisory findings into tool readiness issues. Keep them separate from \`revela-decks review\` blockers and warnings, and preserve the reviewer's stable finding IDs when reporting them.
232
238
  - When reporting weak evidence, say whether the missing trace is \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, or \`caveat\` if that is clear from the reviewed materials.
233
239
  - When reporting candidate evidence bindings, distinguish partial support from full-slide support. Never say a candidate supports unrelated future-state, recommendation, roadmap, or product-vision claims unless the candidate explicitly supports those claims.
234
- - Treat \`evidenceDraft\` as a proposed record, not a mutation. Do not call \`upsertSlides\` to bind it. Only call \`revela-decks\` action \`applyEvidenceCandidates\` with explicit \`candidateIds\` if the user asks to apply candidate bindings.
240
+ - Treat \`evidenceDraft\` as a proposed record, not a mutation. Do not call \`upsertSlides\` to bind it. If the user asks to apply candidate bindings, use \`initNarrativeVault\` if needed, write \`revela-narrative/evidence/*.md\` directly with explicit source trace, then run \`compileNarrativeVault\`. Use \`upsertVaultEvidence\` only as a fallback helper when direct Markdown editing is unavailable or unsafe.
235
241
  - When reporting candidate search diagnostics, do not present near misses as evidence. Say they are below binding threshold and use them only to explain why no candidate was returned.
242
+ - When reporting vault diagnostics, do not fill missing evidence, source trace, quotes, URLs, page references, or caveats from model memory. Preserve the blocker until the Markdown source is fixed and compiled.
236
243
 
237
244
  Rules:
238
245
  - Do not write or overwrite \`decks/*.html\` during review.
package/lib/ctx.ts CHANGED
@@ -22,6 +22,6 @@ export interface RevelaCtx {
22
22
 
23
23
  /** Global singleton. Import and use directly from any module. */
24
24
  export const ctx: RevelaCtx = {
25
- enabled: false,
25
+ enabled: true,
26
26
  isResearchAgent: false,
27
27
  }
@@ -20,7 +20,8 @@ import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type Work
20
20
  import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
21
21
  import { computeNarrativeHash } from "./narrative-state/hash"
22
22
  import { getArtifactClaimRefs } from "./narrative-state/queries"
23
- import type { NarrativeStateV1 } from "./narrative-state/types"
23
+ import type { NarrativeApproval, NarrativeStateV1 } from "./narrative-state/types"
24
+ import { hasNarrativeVault, loadNarrativeFromPreferredSource } from "./narrative-vault"
24
25
 
25
26
  export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
26
27
 
@@ -34,6 +35,7 @@ export interface DecksState {
34
35
  version: 1
35
36
  activeDeck?: string
36
37
  narrative?: NarrativeStateV1
38
+ narrativeApprovals?: NarrativeApproval[]
37
39
  workspace: {
38
40
  brief?: string
39
41
  sourceMaterials: SourceMaterial[]
@@ -55,6 +57,7 @@ export interface SourceMaterial {
55
57
  type?: string
56
58
  size?: number
57
59
  fingerprint?: string
60
+ lastModified?: string
58
61
  status?: "discovered" | "extracted" | "summarized" | "researched"
59
62
  extraction?: {
60
63
  manifestPath?: string
@@ -483,15 +486,37 @@ export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptio
483
486
  }
484
487
 
485
488
  export function readDecksState(workspaceRoot: string): DecksState {
486
- return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
489
+ return applyPreferredNarrativeSource(workspaceRoot, readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative }))
487
490
  }
488
491
 
489
492
  export function writeDecksState(workspaceRoot: string, state: DecksState): void {
490
- writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
493
+ const vault = hasNarrativeVault(workspaceRoot)
494
+ writeWorkspaceState(workspaceRoot, prepareStateForWrite(workspaceRoot, state), { fileName: DECKS_STATE_FILE, normalize: vault ? normalizeDecksState : normalizeDecksStateWithNarrative })
491
495
  }
492
496
 
493
497
  export function readOrCreateDecksState(workspaceRoot: string): DecksState {
494
- return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
498
+ return applyPreferredNarrativeSource(workspaceRoot, readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative }))
499
+ }
500
+
501
+ function applyPreferredNarrativeSource(workspaceRoot: string, state: DecksState): DecksState {
502
+ const normalized = normalizeDecksStateWithNarrative(state)
503
+ const loaded = loadNarrativeFromPreferredSource(workspaceRoot, normalized.narrative, narrativeApprovalsForHydration(normalized))
504
+ if (loaded.source !== "vault" || !loaded.narrative) return normalized
505
+ return normalizeDecksStateWithNarrative({ ...normalized, narrative: loaded.narrative, narrativeApprovals: loaded.narrative.approvals })
506
+ }
507
+
508
+ function prepareStateForWrite(workspaceRoot: string, state: DecksState): DecksState {
509
+ const normalized = normalizeDecksStateWithNarrative(state)
510
+ if (!hasNarrativeVault(workspaceRoot)) return normalized
511
+ const loaded = loadNarrativeFromPreferredSource(workspaceRoot, normalized.narrative, narrativeApprovalsForHydration(normalized))
512
+ const narrativeApprovals = loaded.narrative?.approvals ?? narrativeApprovalsForHydration(normalized)
513
+ const prepared = normalizeDecksStateWithNarrative({ ...normalized, narrative: loaded.narrative ?? normalized.narrative, narrativeApprovals })
514
+ const { narrative: _narrative, ...withoutNarrative } = prepared
515
+ return withoutNarrative as DecksState
516
+ }
517
+
518
+ function narrativeApprovalsForHydration(state: DecksState): NarrativeApproval[] {
519
+ return state.narrativeApprovals ?? state.narrative?.approvals ?? []
495
520
  }
496
521
 
497
522
  export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
@@ -859,6 +884,7 @@ function normalizeDecksState(input: DecksState): DecksState {
859
884
  version: 1,
860
885
  activeDeck: input.activeDeck ? normalizeSlug(input.activeDeck) : undefined,
861
886
  narrative: normalizeCanonicalNarrativeState(input.narrative, input.activeDeck || "workspace"),
887
+ narrativeApprovals: normalizeNarrativeApprovals([...(input.narrativeApprovals ?? []), ...(input.narrative?.approvals ?? [])]),
862
888
  workspace: {
863
889
  brief: input.workspace?.brief,
864
890
  sourceMaterials: input.workspace?.sourceMaterials ?? [],
@@ -890,9 +916,17 @@ function normalizeDecksState(input: DecksState): DecksState {
890
916
  function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
891
917
  const state = normalizeDecksState(input)
892
918
  if (!state.narrative && currentDeckKey(state)) state.narrative = normalizeNarrativeState(state)
919
+ if (state.narrative && state.narrativeApprovals && state.narrativeApprovals.length > 0) {
920
+ state.narrative = { ...state.narrative, approvals: normalizeNarrativeApprovals([...state.narrative.approvals, ...state.narrativeApprovals]) ?? [] }
921
+ }
893
922
  return state
894
923
  }
895
924
 
925
+ function normalizeNarrativeApprovals(approvals: NarrativeApproval[]): NarrativeApproval[] | undefined {
926
+ const normalized = [...new Map(approvals.filter((approval) => approval?.id).map((approval) => [approval.id, approval])).values()]
927
+ return normalized.length > 0 ? normalized : undefined
928
+ }
929
+
896
930
  function normalizeDeckPlanReview(input: DeckPlanReview | undefined): DeckPlanReview | undefined {
897
931
  if (!input || !input.narrativeHash || !input.planHash) return undefined
898
932
  return {
@@ -75,7 +75,7 @@ Instructions:
75
75
  - Make the smallest targeted change that satisfies the user's comment.
76
76
  - If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
77
77
  - Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
78
- - 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.
78
+ - 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 targeted ${"`revela-decks`"} vault actions (${"`initNarrativeVault`"} if needed, then ${"`updateVaultCoreNarrative`"}, ${"`upsertVaultClaim`"}, ${"`upsertVaultEvidence`"}, ${"`upsertVaultObjection`"}, or ${"`upsertVaultRisk`"}), with manual Markdown edits plus ${"`compileNarrativeVault`"} only for unsupported node changes. Then the narrative must be reviewed/approved or explicitly overridden before updating the deck projection.
79
79
  - Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, runtime JavaScript fixes, or deck HTML contract fixes may remain an artifact-level edit.
80
80
  - If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
81
81
  - Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
@@ -0,0 +1,53 @@
1
+ import type { ArtifactQAReport } from "./qa/artifact"
2
+ import type { AutoCompileNarrativeVaultResult } from "./narrative-vault/auto-compile"
3
+
4
+ export function formatMarkdownQaUserNotice(result: AutoCompileNarrativeVaultResult): string | undefined {
5
+ if (result.ok) return undefined
6
+
7
+ const lines = ["**Markdown QA blocked**"]
8
+ lines.push(`Touched: ${result.touched.length > 0 ? result.touched.map((file) => `\`${file}\``).join(", ") : "unknown"}`)
9
+
10
+ const blockers = result.markdownQa?.blockers ?? []
11
+ if (blockers.length > 0) {
12
+ lines.push("Top repair(s):")
13
+ for (const card of blockers.slice(0, 3)) {
14
+ const location = [card.file, card.nodeId].filter(Boolean).join(" / ")
15
+ lines.push(`- \`${card.issueCode}\`${location ? ` (${location})` : ""}: ${card.smallestRepair}`)
16
+ }
17
+ if (blockers.length > 3) lines.push(`- ... ${blockers.length - 3} more`)
18
+ } else if (result.error) {
19
+ lines.push(`Hook error: ${result.error}`)
20
+ } else {
21
+ lines.push("Compile diagnostics are blocking the vault. See the tool output for details.")
22
+ }
23
+
24
+ return lines.join("\n")
25
+ }
26
+
27
+ export function formatArtifactQaUserNotice(report: ArtifactQAReport): string | undefined {
28
+ if (report.passed) return undefined
29
+
30
+ const lines = ["**Artifact QA failed**"]
31
+ lines.push(`File: \`${report.file}\``)
32
+ lines.push(`Hard errors: ${report.hardErrorCount}; warnings: ${report.warningCount}`)
33
+ if (report.sections.length > 0) {
34
+ lines.push("Top issue area(s):")
35
+ for (const section of report.sections.slice(0, 3)) lines.push(`- ${firstLine(section)}`)
36
+ if (report.sections.length > 3) lines.push(`- ... ${report.sections.length - 3} more`)
37
+ }
38
+ lines.push("Fix the reported artifact issues before treating the deck as ready.")
39
+ return lines.join("\n")
40
+ }
41
+
42
+ export function formatStateGateUserNotice(kind: "write" | "patch", reason: string): string {
43
+ return [
44
+ "**Revela state gate blocked a direct DECKS.json edit**",
45
+ `Operation: ${kind}`,
46
+ `Reason: ${reason}`,
47
+ "Use the `revela-decks` tool for controlled workspace state changes.",
48
+ ].join("\n")
49
+ }
50
+
51
+ function firstLine(text: string): string {
52
+ return text.split(/\r?\n/).map((line) => line.trim()).find(Boolean)?.replace(/^#+\s*/, "") ?? "See report details."
53
+ }
@@ -12,6 +12,10 @@ const MIME_TO_EXT: Record<string, string> = {
12
12
 
13
13
  const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".ico"])
14
14
  const DEFAULT_DOWNLOAD_TIMEOUT_MS = 10_000
15
+ const PRODUCT_USER_AGENT = "Revela/0.17 asset-save"
16
+ const BROWSER_USER_AGENT =
17
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
18
+ "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
15
19
 
16
20
  function normalizeExtension(ext: string): string {
17
21
  const value = ext.toLowerCase()
@@ -47,6 +51,24 @@ export async function downloadImageFromUrl(
47
51
  throw new Error("INVALID_URL")
48
52
  }
49
53
 
54
+ const userAgents = [PRODUCT_USER_AGENT, BROWSER_USER_AGENT]
55
+ let lastError: unknown
56
+ for (const userAgent of userAgents) {
57
+ try {
58
+ return await downloadWithUserAgent(parsed, userAgent, options)
59
+ } catch (error) {
60
+ lastError = error
61
+ }
62
+ }
63
+
64
+ throw lastError instanceof Error ? lastError : new Error(String(lastError))
65
+ }
66
+
67
+ async function downloadWithUserAgent(
68
+ parsed: URL,
69
+ userAgent: string,
70
+ options: { timeoutMs?: number },
71
+ ): Promise<{ buffer: Buffer; contentType: string | null; extension: string }> {
50
72
  const controller = new AbortController()
51
73
  let timedOut = false
52
74
  const timer = setTimeout(() => {
@@ -60,9 +82,7 @@ export async function downloadImageFromUrl(
60
82
  response = await fetch(parsed, {
61
83
  headers: {
62
84
  Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
63
- "User-Agent":
64
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
65
- "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
85
+ "User-Agent": userAgent,
66
86
  },
67
87
  signal: controller.signal,
68
88
  })
package/lib/media/save.ts CHANGED
@@ -141,6 +141,7 @@ function saveFailureResult(
141
141
  path: null,
142
142
  manifestPath: relative(workspaceDir, manifestPath),
143
143
  updated: true,
144
+ failureReason,
144
145
  }
145
146
  }
146
147
 
@@ -59,6 +59,7 @@ export type MediaSaveResult =
59
59
  path: string | null
60
60
  manifestPath: string
61
61
  updated: boolean
62
+ failureReason?: string
62
63
  }
63
64
  | {
64
65
  ok: false
@@ -9,6 +9,7 @@ export interface NarrativeDisplayModel {
9
9
  summaryLine?: string
10
10
  labels?: Partial<NarrativeDisplayLabels>
11
11
  claimCards?: NarrativeDisplayClaimCard[]
12
+ researchGapCards?: NarrativeDisplayResearchGapCard[]
12
13
  relations?: NarrativeDisplayRelation[]
13
14
  }
14
15
 
@@ -17,6 +18,18 @@ export interface NarrativeDisplayLabels {
17
18
  claimFlow: string
18
19
  flowNote: string
19
20
  selectedClaim: string
21
+ selectedEvidence: string
22
+ evidenceList: string
23
+ gap: string
24
+ gaps: string
25
+ noEvidence: string
26
+ selectEvidencePrompt: string
27
+ sourceTrace: string
28
+ evidenceSource: string
29
+ whyThisSupports: string
30
+ linkedGaps: string
31
+ selectedGap: string
32
+ noLinkedGaps: string
20
33
  claim: string
21
34
  claimId: string
22
35
  status: string
@@ -48,6 +61,11 @@ export interface NarrativeDisplayClaimCard {
48
61
  researchGapsSummary?: string
49
62
  }
50
63
 
64
+ export interface NarrativeDisplayResearchGapCard {
65
+ gapId: string
66
+ displayQuestion?: string
67
+ }
68
+
51
69
  export interface NarrativeDisplayRelation {
52
70
  fromClaimId: string
53
71
  toClaimId: string
@@ -63,6 +81,7 @@ export interface ValidatedNarrativeDisplayModel {
63
81
  summaryLine?: string
64
82
  labels: NarrativeDisplayLabels
65
83
  claimCards: Map<string, NarrativeDisplayClaimCard>
84
+ researchGapCards: Map<string, NarrativeDisplayResearchGapCard>
66
85
  relations: Map<string, NarrativeDisplayRelation>
67
86
  }
68
87
 
@@ -71,8 +90,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
71
90
  return {
72
91
  eyebrow: "只读主张流",
73
92
  claimFlow: "主张推进",
74
- flowNote: "点击主张查看证据、关系、风险、缺口和已覆盖页面。",
93
+ flowNote: "点击主张查看论据和真实存在的缺口;点击论据查看它关联的缺口。",
75
94
  selectedClaim: "当前主张",
95
+ selectedEvidence: "当前论据",
96
+ evidenceList: "论据",
97
+ gap: "缺口",
98
+ gaps: "缺口",
99
+ noEvidence: "没有绑定论据",
100
+ selectEvidencePrompt: "选择一条论据或缺口查看详情",
101
+ sourceTrace: "来源追踪",
102
+ evidenceSource: "来源",
103
+ whyThisSupports: "为什么支撑论点",
104
+ linkedGaps: "这条论据关联的缺口",
105
+ selectedGap: "当前缺口",
106
+ noLinkedGaps: "这条论据没有关联缺口",
76
107
  claim: "主张",
77
108
  claimId: "主张 ID",
78
109
  status: "状态",
@@ -93,8 +124,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
93
124
  return {
94
125
  eyebrow: "読み取り専用クレームフロー",
95
126
  claimFlow: "クレームフロー",
96
- flowNote: "クレームをクリックすると、根拠、関係、リスク、ギャップ、該当スライドを確認できます。",
127
+ flowNote: "クレームをクリックして根拠と実在するギャップを確認し、根拠をクリックして紐づくギャップを確認します。",
97
128
  selectedClaim: "選択中のクレーム",
129
+ selectedEvidence: "選択中の根拠",
130
+ evidenceList: "根拠",
131
+ gap: "ギャップ",
132
+ gaps: "ギャップ",
133
+ noEvidence: "紐づいた根拠はありません",
134
+ selectEvidencePrompt: "根拠またはギャップを選択して詳細を確認してください",
135
+ sourceTrace: "出典トレース",
136
+ evidenceSource: "出典",
137
+ whyThisSupports: "この根拠がクレームを支える理由",
138
+ linkedGaps: "この根拠に紐づくギャップ",
139
+ selectedGap: "選択中のギャップ",
140
+ noLinkedGaps: "この根拠に紐づくギャップはありません",
98
141
  claim: "クレーム",
99
142
  claimId: "クレーム ID",
100
143
  status: "ステータス",
@@ -114,8 +157,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
114
157
  return {
115
158
  eyebrow: "Read-only claim flow board",
116
159
  claimFlow: "Claim Flow",
117
- flowNote: "Click a claim to inspect support, relation context, gaps, and covered slides.",
160
+ flowNote: "Click a claim to read its evidence and real gaps; click evidence to see gaps linked to that evidence.",
118
161
  selectedClaim: "Selected claim",
162
+ selectedEvidence: "Selected evidence",
163
+ evidenceList: "Evidence",
164
+ gap: "Gap",
165
+ gaps: "Gaps",
166
+ noEvidence: "No evidence bound",
167
+ selectEvidencePrompt: "Select evidence or a gap to inspect details.",
168
+ sourceTrace: "Source trace",
169
+ evidenceSource: "Source",
170
+ whyThisSupports: "Why this supports the claim",
171
+ linkedGaps: "Gaps linked to evidence",
172
+ selectedGap: "Selected gap",
173
+ noLinkedGaps: "No gaps linked to this evidence.",
119
174
  claim: "Claim",
120
175
  claimId: "Claim ID",
121
176
  status: "Status",
@@ -140,6 +195,7 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
140
195
  if (input.language !== language) throw new Error(`Narrative display model language must be ${language}.`)
141
196
 
142
197
  const claimIds = new Set(map.claimFlow.map((claim) => claim.id))
198
+ const gapIds = new Set(map.researchGaps.map((gap) => gap.id))
143
199
  const relationByKey = new Map(map.claimRelations.map((relation) => [relationKey(relation), relation]))
144
200
  const claimCards = new Map<string, NarrativeDisplayClaimCard>()
145
201
  for (const card of input.claimCards ?? []) {
@@ -147,6 +203,12 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
147
203
  claimCards.set(card.claimId, cleanClaimCard(card))
148
204
  }
149
205
 
206
+ const researchGapCards = new Map<string, NarrativeDisplayResearchGapCard>()
207
+ for (const card of input.researchGapCards ?? []) {
208
+ if (!gapIds.has(card.gapId)) throw new Error(`Unknown display gapId: ${card.gapId}`)
209
+ researchGapCards.set(card.gapId, cleanResearchGapCard(card))
210
+ }
211
+
150
212
  const relations = new Map<string, NarrativeDisplayRelation>()
151
213
  for (const relation of input.relations ?? []) {
152
214
  const key = relationKey(relation)
@@ -162,12 +224,13 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
162
224
  summaryLine: clean(input.summaryLine),
163
225
  labels: mergeLabels(defaults, input.labels),
164
226
  claimCards,
227
+ researchGapCards,
165
228
  relations,
166
229
  }
167
230
  }
168
231
 
169
232
  export function emptyDisplayModel(language: NarrativeViewLanguage, labels = defaultNarrativeDisplayLabels(language)): ValidatedNarrativeDisplayModel {
170
- return { version: 1, language, labels, claimCards: new Map(), relations: new Map() }
233
+ return { version: 1, language, labels, claimCards: new Map(), researchGapCards: new Map(), relations: new Map() }
171
234
  }
172
235
 
173
236
  export function relationKey(relation: Pick<NarrativeDisplayRelation, "fromClaimId" | "toClaimId" | "relation">): string {
@@ -210,6 +273,13 @@ function cleanClaimCard(card: NarrativeDisplayClaimCard): NarrativeDisplayClaimC
210
273
  }
211
274
  }
212
275
 
276
+ function cleanResearchGapCard(card: NarrativeDisplayResearchGapCard): NarrativeDisplayResearchGapCard {
277
+ return {
278
+ gapId: card.gapId,
279
+ displayQuestion: clean(card.displayQuestion),
280
+ }
281
+ }
282
+
213
283
  function cleanRelation(relation: NarrativeDisplayRelation, canonical: NarrativeMapClaimRelation): NarrativeDisplayRelation {
214
284
  const displayLabel = clean(relation.displayLabel)
215
285
  const displayRationale = clean(relation.displayRationale)