@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
@@ -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 { NarrativeClaimRelation, NarrativeEvidenceBinding, NarrativeResearchGap, 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,135 @@ 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 relation of narrative.claimRelations ?? []) addClaimRelation(builder, relation)
182
+
183
+ for (const objection of narrative.objections) {
184
+ addNode(builder, {
185
+ id: objection.id,
186
+ type: "objection",
187
+ label: objection.text,
188
+ data: compactData({ text: objection.text, priority: objection.priority, response: objection.response }),
189
+ })
190
+ addEdge(builder, "contains", narrative.id, objection.id)
191
+ addEdge(builder, "challenges", objection.id, objection.claimId || narrative.id)
192
+ }
193
+
194
+ for (const risk of narrative.risks) {
195
+ addNode(builder, {
196
+ id: risk.id,
197
+ type: "risk",
198
+ label: risk.text,
199
+ data: compactData({ text: risk.text, severity: risk.severity, mitigation: risk.mitigation }),
200
+ })
201
+ addEdge(builder, "contains", narrative.id, risk.id)
202
+ addEdge(builder, "constrained_by", risk.claimId || narrative.id, risk.id)
203
+ }
204
+
205
+ for (const gap of narrative.researchGaps ?? []) addResearchGap(builder, narrative, gap)
206
+
207
+ return narrative.id
208
+ }
209
+
210
+ function addClaimRelation(builder: GraphBuilder, relation: NarrativeClaimRelation): void {
211
+ addEdge(builder, graphEdgeTypeForClaimRelation(relation.relation), relation.fromClaimId, relation.toClaimId, compactData({
212
+ relationId: relation.id,
213
+ relation: relation.relation,
214
+ rationale: relation.rationale,
215
+ source: "canonicalNarrative",
216
+ }))
217
+ }
218
+
219
+ function graphEdgeTypeForClaimRelation(relation: NarrativeClaimRelation["relation"]): GraphEdgeType {
220
+ if (relation === "constrains") return "constrained_by"
221
+ if (relation === "supports") return "supports"
222
+ return relation
223
+ }
224
+
225
+ function addResearchGap(builder: GraphBuilder, narrative: NarrativeStateV1, gap: NarrativeResearchGap): void {
226
+ addNode(builder, {
227
+ id: gap.id,
228
+ type: "researchGap",
229
+ label: gap.question,
230
+ data: compactData({
231
+ question: gap.question,
232
+ status: gap.status,
233
+ priority: gap.priority,
234
+ targetType: gap.targetType,
235
+ targetId: gap.targetId,
236
+ findingsFile: gap.findingsFile,
237
+ evidenceBindingIds: gap.evidenceBindingIds,
238
+ createdFromIssueType: gap.createdFromIssueType,
239
+ notes: gap.notes,
240
+ }),
241
+ })
242
+ addEdge(builder, "contains", narrative.id, gap.id)
243
+ addEdge(builder, "derived_from", gap.id, gapTargetNodeId(narrative, gap))
244
+ if (gap.findingsFile) addEdge(builder, "derived_from", gap.id, findingNodeId(gap.findingsFile), { status: gap.status })
245
+ for (const evidenceId of gap.evidenceBindingIds ?? []) {
246
+ const binding = narrative.evidenceBindings.find((item) => item.id === evidenceId)
247
+ if (binding) addEdge(builder, "derived_from", gap.id, binding.claimId, { evidenceBindingId: evidenceId })
248
+ }
249
+ }
250
+
251
+ function gapTargetNodeId(narrative: NarrativeStateV1, gap: NarrativeResearchGap): string {
252
+ if (gap.targetId) return gap.targetId
253
+ if (gap.targetType === "narrative" || gap.targetType === "decision") return narrative.id
254
+ return narrative.id
255
+ }
256
+
125
257
  function addSlide(builder: GraphBuilder, slide: SlideSpec): void {
126
258
  addNode(builder, {
127
259
  id: slideNodeId(slide.index),
@@ -222,6 +354,29 @@ function addEvidenceSupportNode(builder: GraphBuilder, evidence: EvidenceRef): s
222
354
  return id
223
355
  }
224
356
 
357
+ function addNarrativeEvidenceSupportNode(builder: GraphBuilder, binding: NarrativeEvidenceBinding): string {
358
+ if (binding.findingsFile?.trim()) {
359
+ const id = findingNodeId(binding.findingsFile)
360
+ addNode(builder, {
361
+ id,
362
+ type: "finding",
363
+ label: binding.findingsFile,
364
+ data: compactData({ findingsFile: binding.findingsFile, source: binding.source, quote: binding.quote, location: binding.location, caveat: binding.caveat }),
365
+ })
366
+ return id
367
+ }
368
+
369
+ const sourceKey = binding.sourcePath || binding.source || binding.url || "unknown-narrative-evidence-source"
370
+ const id = sourceNodeId(sourceKey)
371
+ addNode(builder, {
372
+ id,
373
+ type: "source",
374
+ label: sourceKey,
375
+ data: compactData({ source: binding.source, sourcePath: binding.sourcePath, url: binding.url }),
376
+ })
377
+ return id
378
+ }
379
+
225
380
  function addArtifact(builder: GraphBuilder, deck: DeckSpec, target: RenderTarget, narrativeId: string | undefined, targets: RenderTarget[]): void {
226
381
  const artifactId = artifactNodeId(target.outputPath ?? deck.outputPath)
227
382
  addNode(builder, {
@@ -248,9 +403,11 @@ function renderTargetsForDeck(state: DecksState, deck: DeckSpec): RenderTarget[]
248
403
  const deckOutputPath = normalizePath(deck.outputPath)
249
404
  const htmlTargetId = renderTargetId("html_deck", deckOutputPath)
250
405
  const htmlArtifactId = artifactNodeId(deckOutputPath)
406
+ const narrativeId = state.narrative?.id
251
407
  const targets = (state.renderTargets ?? []).filter((target) => {
252
408
  if (target.id === htmlTargetId) return true
253
409
  if (target.type === "html_deck") return normalizePath(target.outputPath ?? "") === deckOutputPath
410
+ if (narrativeId && target.sourceNodeIds.includes(narrativeId)) return true
254
411
  const data = target.data ?? {}
255
412
  return data.sourceTargetId === htmlTargetId ||
256
413
  data.sourceOutputPath === deckOutputPath ||
@@ -308,6 +465,21 @@ function hasNarrativeBrief(brief: NarrativeBrief | undefined): boolean {
308
465
  )
309
466
  }
310
467
 
468
+ function hasCanonicalNarrative(narrative: NarrativeStateV1 | undefined): narrative is NarrativeStateV1 {
469
+ return Boolean(
470
+ narrative?.audience.primary?.trim() ||
471
+ narrative?.audience.beliefBefore?.trim() ||
472
+ narrative?.audience.beliefAfter?.trim() ||
473
+ narrative?.decision.action?.trim() ||
474
+ narrative?.thesis?.statement?.trim() ||
475
+ narrative?.claims.length ||
476
+ narrative?.evidenceBindings.length ||
477
+ narrative?.objections.length ||
478
+ narrative?.risks.length ||
479
+ narrative?.researchGaps?.length,
480
+ )
481
+ }
482
+
311
483
  function hasEvidenceDetail(evidence: EvidenceRef): boolean {
312
484
  return Boolean(
313
485
  evidence.quote?.trim() ||
@@ -50,6 +50,7 @@ export type GraphNodeType =
50
50
  | "risk"
51
51
  | "slide"
52
52
  | "artifact"
53
+ | "researchGap"
53
54
 
54
55
  export interface GraphEdge {
55
56
  id: string
@@ -64,6 +65,10 @@ export type GraphEdgeType =
64
65
  | "extracted_as"
65
66
  | "produced"
66
67
  | "supports"
68
+ | "leads_to"
69
+ | "depends_on"
70
+ | "contrasts_with"
71
+ | "answers"
67
72
  | "appears_in"
68
73
  | "challenges"
69
74
  | "constrained_by"
@@ -88,14 +93,21 @@ export type WorkspaceActionType =
88
93
  | "source.extracted"
89
94
  | "research.findings_saved"
90
95
  | "research.findings_attached"
96
+ | "research.gap_created"
97
+ | "research.gap_updated"
98
+ | "research.gap_closed"
99
+ | "narrative.upserted"
100
+ | "deck.plan_compiled"
101
+ | "artifact.coverage_backfilled"
91
102
  | "evidence.candidate_generated"
92
103
  | "evidence.binding_applied"
104
+ | "narrative.approved"
93
105
  | "review.performed"
94
106
  | "artifact.rendered"
95
107
 
96
108
  export interface RenderTarget {
97
109
  id: string
98
- type: "html_deck" | "pdf" | "pptx" | "brief" | "appendix" | "qa_view" | "interactive_page"
110
+ type: "html_deck" | "pdf" | "pptx" | "brief" | "executive_brief" | "appendix" | "qa_view" | "interactive_page"
99
111
  outputPath?: string
100
112
  sourceNodeIds: string[]
101
113
  artifactVersion?: string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/plugin.ts CHANGED
@@ -59,8 +59,10 @@ import {
59
59
  buildDesignsEditPrompt,
60
60
  } from "./lib/commands/designs-new"
61
61
  import { buildInitPrompt } from "./lib/commands/init"
62
+ import { handleBrief, parseBriefArgs } from "./lib/commands/brief"
63
+ import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs } from "./lib/commands/narrative"
62
64
  import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
63
- import { buildReviewPrompt } from "./lib/commands/review"
65
+ import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
64
66
  import {
65
67
  extractDeckHtmlTargetsFromPatch,
66
68
  extractPatchTextArg,
@@ -85,6 +87,7 @@ import researchImagesListTool from "./tools/research-images-list"
85
87
  import researchSaveTool from "./tools/research-save"
86
88
  import inspectionContextTool from "./tools/inspection-context"
87
89
  import inspectionResultTool from "./tools/inspection-result"
90
+ import narrativeViewTool from "./tools/narrative-view"
88
91
  import workspaceScanTool from "./tools/workspace-scan"
89
92
  import extractDocumentMaterialsTool from "./tools/extract-document-materials"
90
93
  import qaTool from "./tools/qa"
@@ -326,6 +329,7 @@ const server: Plugin = (async (pluginCtx) => {
326
329
  throw new Error("__REVELA_DISABLE_HANDLED__")
327
330
  }
328
331
  if (sub === "init") {
332
+ buildPrompt({ mode: "narrative" })
329
333
  output.parts.length = 0
330
334
  output.parts.push({
331
335
  type: "text",
@@ -339,6 +343,7 @@ const server: Plugin = (async (pluginCtx) => {
339
343
  await send(parsed.error)
340
344
  throw new Error("__REVELA_REMEMBER_USAGE_HANDLED__")
341
345
  }
346
+ buildPrompt({ mode: "narrative" })
342
347
  output.parts.length = 0
343
348
  output.parts.push({
344
349
  type: "text",
@@ -348,9 +353,10 @@ const server: Plugin = (async (pluginCtx) => {
348
353
  }
349
354
  if (sub === "review") {
350
355
  if (param) {
351
- await send("`/revela review` no longer accepts a deck name. It reviews the current workspace deck.")
356
+ 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
357
  throw new Error("__REVELA_REVIEW_USAGE_HANDLED__")
353
358
  }
359
+ buildPrompt({ mode: "narrative" })
354
360
  output.parts.length = 0
355
361
  output.parts.push({
356
362
  type: "text",
@@ -358,6 +364,55 @@ const server: Plugin = (async (pluginCtx) => {
358
364
  } as any)
359
365
  return
360
366
  }
367
+ if (sub === "narrative") {
368
+ const parsed = parseNarrativeArgs(param)
369
+ if (!parsed.ok) {
370
+ await send(parsed.error)
371
+ throw new Error("__REVELA_NARRATIVE_USAGE_HANDLED__")
372
+ }
373
+ if (parsed.args.raw) {
374
+ await handleNarrative({ workspaceRoot, openBrowser: true, language: parsed.args.language }, send)
375
+ throw new Error("__REVELA_NARRATIVE_HANDLED__")
376
+ }
377
+ buildPrompt({ mode: "narrative" })
378
+ output.parts.length = 0
379
+ output.parts.push({
380
+ type: "text",
381
+ text: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
382
+ } as any)
383
+ return
384
+ }
385
+ if (sub === "brief") {
386
+ const parsed = parseBriefArgs(param)
387
+ if (!parsed.ok) {
388
+ await send(parsed.error)
389
+ throw new Error("__REVELA_BRIEF_USAGE_HANDLED__")
390
+ }
391
+ await handleBrief({ workspaceRoot, outputPath: parsed.args.outputPath }, send)
392
+ throw new Error("__REVELA_BRIEF_HANDLED__")
393
+ }
394
+ if (sub === "deck") {
395
+ if (param && param !== "--review") {
396
+ await send("Usage: `/revela deck` starts approved-narrative deck handoff; `/revela deck --review` reviews deck/artifact readiness.")
397
+ throw new Error("__REVELA_DECK_USAGE_HANDLED__")
398
+ }
399
+ if (!param) {
400
+ buildPrompt({ mode: "deck-render" })
401
+ output.parts.length = 0
402
+ output.parts.push({
403
+ type: "text",
404
+ text: buildDeckPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
405
+ } as any)
406
+ return
407
+ }
408
+ buildPrompt({ mode: "deck-render" })
409
+ output.parts.length = 0
410
+ output.parts.push({
411
+ type: "text",
412
+ text: buildDeckReviewPrompt({ exists: hasDecksState(workspaceRoot), workspaceRoot }),
413
+ } as any)
414
+ return
415
+ }
361
416
  if (sub === "refine") {
362
417
  if (param) {
363
418
  await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
@@ -482,6 +537,7 @@ const server: Plugin = (async (pluginCtx) => {
482
537
  "revela-research-save": researchSaveTool,
483
538
  "revela-inspection-context": inspectionContextTool,
484
539
  "revela-inspection-result": inspectionResultTool,
540
+ "revela-narrative-view": narrativeViewTool,
485
541
  "revela-workspace-scan": workspaceScanTool,
486
542
  "revela-extract-document-materials": extractDocumentMaterialsTool,
487
543
  "revela-qa": qaTool,
@@ -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.