@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
package/tools/decks.ts CHANGED
@@ -21,15 +21,53 @@ import { recordWorkspaceAction } from "../lib/workspace-state/actions"
21
21
  import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
22
22
  import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
23
23
  import { activeReviewTargetId, latestReviewSnapshotForTarget } from "../lib/workspace-state/review-snapshots"
24
+ import {
25
+ approveNarrativeState,
26
+ recordNarrativeApprovalAction,
27
+ recordNarrativeReviewAction,
28
+ reviewNarrativeState,
29
+ } from "../lib/narrative-state/readiness"
30
+ import { compileDeckPlanFromNarrative } from "../lib/narrative-state/render-plan"
31
+ import { backfillSlideClaimRefsFromCoverage } from "../lib/narrative-state/coverage"
32
+ import { closeResearchGapInState, deriveResearchGapsFromReadiness, updateResearchGapInState, upsertResearchGapsInState } from "../lib/narrative-state/research-gaps"
33
+ import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "../lib/narrative-state/normalize"
34
+ import { narrativeToBrief } from "../lib/narrative-state/project-compat"
35
+ import type { NarrativeStateV1 } from "../lib/narrative-state/types"
36
+
37
+ function mergeNarrativeInput(current: NarrativeStateV1, input: Partial<NarrativeStateV1>): Partial<NarrativeStateV1> {
38
+ return {
39
+ ...current,
40
+ ...input,
41
+ id: current.id,
42
+ version: 1,
43
+ audience: {
44
+ ...current.audience,
45
+ ...(input.audience ?? {}),
46
+ },
47
+ decision: {
48
+ ...current.decision,
49
+ ...(input.decision ?? {}),
50
+ },
51
+ thesis: input.thesis ? { ...current.thesis, ...input.thesis } as NarrativeStateV1["thesis"] : current.thesis,
52
+ claims: input.claims ?? current.claims,
53
+ claimRelations: input.claimRelations ?? current.claimRelations,
54
+ evidenceBindings: input.evidenceBindings ?? current.evidenceBindings,
55
+ objections: input.objections ?? current.objections,
56
+ risks: input.risks ?? current.risks,
57
+ researchGaps: input.researchGaps ?? current.researchGaps,
58
+ approvals: current.approvals,
59
+ updatedAt: new Date().toISOString(),
60
+ }
61
+ }
24
62
 
25
63
  export default tool({
26
64
  description:
27
65
  `Read and update ${DECKS_STATE_FILE}, Revela's workspace deck state file. ` +
28
66
  "Use this tool instead of writing or patching the state file directly. " +
29
- "It stores active deck specs, per-slide content/layout/components, and computes write readiness.",
67
+ "It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
30
68
  args: {
31
69
  action: tool.schema
32
- .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
70
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
33
71
  .describe("Action to perform on DECKS.json."),
34
72
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
35
73
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -45,6 +83,88 @@ export default tool({
45
83
  objections: tool.schema.array(tool.schema.string()).optional().describe("Likely stakeholder objections or questions the narrative should handle."),
46
84
  risks: tool.schema.array(tool.schema.string()).optional().describe("Risks, assumptions, caveats, or tradeoffs that should travel with the narrative."),
47
85
  }).optional().describe("For upsertDeck: 0.9 Narrative Compiler brief used to review story intent before writing."),
86
+ narrative: tool.schema.object({
87
+ status: tool.schema.enum(["draft", "needs_research", "needs_user_confirmation", "ready_for_approval", "approved"]).optional(),
88
+ audience: tool.schema.object({
89
+ primary: tool.schema.string().optional(),
90
+ secondary: tool.schema.array(tool.schema.string()).optional(),
91
+ beliefBefore: tool.schema.string().optional(),
92
+ beliefAfter: tool.schema.string().optional(),
93
+ decisionContext: tool.schema.string().optional(),
94
+ successCriteria: tool.schema.array(tool.schema.string()).optional(),
95
+ }).optional(),
96
+ decision: tool.schema.object({
97
+ action: tool.schema.string().optional(),
98
+ owner: tool.schema.string().optional(),
99
+ deadline: tool.schema.string().optional(),
100
+ decisionType: tool.schema.enum(["approve", "invest", "prioritize", "align", "choose", "understand", "other"]).optional(),
101
+ consequenceOfNoDecision: tool.schema.string().optional(),
102
+ }).optional(),
103
+ thesis: tool.schema.object({
104
+ id: tool.schema.string().optional(),
105
+ statement: tool.schema.string().optional(),
106
+ confidence: tool.schema.enum(["high", "medium", "low"]).optional(),
107
+ caveat: tool.schema.string().optional(),
108
+ }).optional(),
109
+ claims: tool.schema.array(tool.schema.object({
110
+ id: tool.schema.string().optional(),
111
+ kind: tool.schema.enum(["context", "problem", "opportunity", "evidence", "recommendation", "risk", "assumption", "ask"]).optional(),
112
+ text: tool.schema.string().describe("Claim text."),
113
+ importance: tool.schema.enum(["central", "supporting", "background"]).optional(),
114
+ evidenceRequired: tool.schema.boolean().optional(),
115
+ evidenceStatus: tool.schema.enum(["supported", "partial", "weak", "missing", "not_required"]).optional(),
116
+ supportedScope: tool.schema.string().optional(),
117
+ unsupportedScope: tool.schema.string().optional(),
118
+ caveats: tool.schema.array(tool.schema.string()).optional(),
119
+ })).optional(),
120
+ claimRelations: tool.schema.array(tool.schema.object({
121
+ id: tool.schema.string().optional(),
122
+ fromClaimId: tool.schema.string().describe("Canonical claim id the relation starts from."),
123
+ toClaimId: tool.schema.string().describe("Canonical claim id the relation points to."),
124
+ relation: tool.schema.enum(["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"]).optional(),
125
+ rationale: tool.schema.string().optional().describe("Short explanation of why this narrative relation exists."),
126
+ })).optional().describe("Canonical claim-to-claim narrative progression relationships. These affect narrative approval hash."),
127
+ evidenceBindings: tool.schema.array(tool.schema.object({
128
+ id: tool.schema.string().optional(),
129
+ claimId: tool.schema.string().describe("Canonical claim id this evidence supports."),
130
+ source: tool.schema.string().describe("Source file, URL, research finding, or material name."),
131
+ sourcePath: tool.schema.string().optional(),
132
+ findingsFile: tool.schema.string().optional(),
133
+ quote: tool.schema.string().optional(),
134
+ location: tool.schema.string().optional(),
135
+ url: tool.schema.string().optional(),
136
+ caveat: tool.schema.string().optional(),
137
+ supportScope: tool.schema.string().optional(),
138
+ unsupportedScope: tool.schema.string().optional(),
139
+ strength: tool.schema.enum(["strong", "partial", "weak"]).optional(),
140
+ })).optional(),
141
+ objections: tool.schema.array(tool.schema.object({
142
+ id: tool.schema.string().optional(),
143
+ text: tool.schema.string().describe("Objection text."),
144
+ claimId: tool.schema.string().optional(),
145
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
146
+ response: tool.schema.string().optional(),
147
+ })).optional(),
148
+ risks: tool.schema.array(tool.schema.object({
149
+ id: tool.schema.string().optional(),
150
+ text: tool.schema.string().describe("Risk, assumption, caveat, or tradeoff text."),
151
+ claimId: tool.schema.string().optional(),
152
+ severity: tool.schema.enum(["high", "medium", "low"]).optional(),
153
+ mitigation: tool.schema.string().optional(),
154
+ })).optional(),
155
+ researchGaps: tool.schema.array(tool.schema.object({
156
+ id: tool.schema.string().optional(),
157
+ targetType: tool.schema.enum(["claim", "objection", "risk", "decision", "narrative"]).optional(),
158
+ targetId: tool.schema.string().optional(),
159
+ question: tool.schema.string().describe("Research question or gap to resolve."),
160
+ status: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional(),
161
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
162
+ findingsFile: tool.schema.string().optional(),
163
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional(),
164
+ createdFromIssueType: tool.schema.string().optional(),
165
+ notes: tool.schema.string().optional(),
166
+ })).optional(),
167
+ }).optional().describe("For upsertNarrative: canonical narrative state fields to merge into DECKS.json. Replaces provided arrays, preserves approvals."),
48
168
  design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
49
169
  domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
50
170
  memory: tool.schema.string().optional().describe("For remember: explicit user or workflow preference to store."),
@@ -93,6 +213,13 @@ export default tool({
93
213
  layout: tool.schema.string().describe("Design layout name."),
94
214
  qa: tool.schema.boolean().optional().describe("Whether the slide is marked QA-relevant deck metadata."),
95
215
  components: tool.schema.array(tool.schema.string()).describe("Design components used by this slide."),
216
+ claimIds: tool.schema.array(tool.schema.string()).optional().describe("Canonical narrative claim ids directly expressed by this slide."),
217
+ claimRefs: tool.schema.array(tool.schema.object({
218
+ claimId: tool.schema.string().describe("Canonical narrative claim id referenced by this slide."),
219
+ role: tool.schema.enum(["primary", "supporting", "evidence", "risk", "objection"]).describe("How the slide uses this claim."),
220
+ note: tool.schema.string().optional().describe("Optional short rationale for this claim-slide relationship."),
221
+ })).optional().describe("Structured canonical claim references for this slide; preferred over flat claimIds when available."),
222
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional().describe("Canonical narrative evidence binding ids used by this slide."),
96
223
  content: tool.schema.object({
97
224
  headline: tool.schema.string().optional(),
98
225
  body: tool.schema.array(tool.schema.string()).optional(),
@@ -124,6 +251,25 @@ export default tool({
124
251
  findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
125
252
  researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
126
253
  researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
254
+ approvalNote: tool.schema.string().optional().describe("For approveNarrative: optional note explaining the approval or override."),
255
+ approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative: use override only for explicit render overrides, not normal strategic approval."),
256
+ approvalScope: tool.schema.enum(["narrative", "render_override"]).optional().describe("For approveNarrative: narrative approval or explicit render override scope."),
257
+ gapId: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: canonical research gap id."),
258
+ researchGaps: tool.schema.array(tool.schema.object({
259
+ id: tool.schema.string().optional(),
260
+ targetType: tool.schema.enum(["claim", "objection", "risk", "decision", "narrative"]).optional(),
261
+ targetId: tool.schema.string().optional(),
262
+ question: tool.schema.string().describe("Research question or gap to resolve."),
263
+ status: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional(),
264
+ priority: tool.schema.enum(["high", "medium", "low"]).optional(),
265
+ findingsFile: tool.schema.string().optional(),
266
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional(),
267
+ createdFromIssueType: tool.schema.string().optional(),
268
+ notes: tool.schema.string().optional(),
269
+ })).optional().describe("For upsertResearchGaps: explicit canonical research gaps to create or update."),
270
+ gapStatus: tool.schema.enum(["open", "in_progress", "findings_saved", "attached", "evidence_bound", "closed"]).optional().describe("For updateResearchGap: lifecycle status."),
271
+ gapNotes: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: notes or close reason."),
272
+ evidenceBindingIds: tool.schema.array(tool.schema.string()).optional().describe("For updateResearchGap: canonical narrative evidence binding ids associated with the gap."),
127
273
  },
128
274
  async execute(args, context) {
129
275
  try {
@@ -194,6 +340,45 @@ export default tool({
194
340
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, deck: next.activeDeck ? next.decks[next.activeDeck] : undefined }, null, 2)
195
341
  }
196
342
 
343
+ if (args.action === "upsertNarrative") {
344
+ if (!args.narrative) return JSON.stringify({ ok: false, error: "narrative is required for upsertNarrative" })
345
+ const current = state.narrative ?? normalizeNarrativeState(state)
346
+ const merged = mergeNarrativeInput(current, args.narrative as Partial<NarrativeStateV1>)
347
+ const normalized = normalizeCanonicalNarrativeState(merged, state.activeDeck ?? defaultSlug)
348
+ if (!normalized) return JSON.stringify({ ok: false, error: "narrative could not be normalized" })
349
+ state.narrative = normalized
350
+
351
+ const deckKey = state.activeDeck
352
+ if (deckKey && state.decks[deckKey]) {
353
+ state = upsertDeck(state, {
354
+ ...state.decks[deckKey],
355
+ slug: deckKey,
356
+ audience: normalized.audience.primary || state.decks[deckKey].audience,
357
+ narrativeBrief: narrativeToBrief(normalized),
358
+ })
359
+ }
360
+
361
+ recordWorkspaceAction(state, {
362
+ type: "narrative.upserted",
363
+ actor: "revela-decks",
364
+ inputs: { hadExistingNarrative: Boolean(current), providedFields: Object.keys(args.narrative as object) },
365
+ outputs: {
366
+ narrativeId: normalized.id,
367
+ status: normalized.status,
368
+ claimCount: normalized.claims.length,
369
+ evidenceBindingCount: normalized.evidenceBindings.length,
370
+ objectionCount: normalized.objections.length,
371
+ riskCount: normalized.risks.length,
372
+ },
373
+ status: "success",
374
+ summary: `Updated canonical narrative state with ${normalized.claims.length} claim${normalized.claims.length === 1 ? "" : "s"}.`,
375
+ nodeIds: [normalized.id],
376
+ })
377
+
378
+ writeDecksState(workspaceRoot, state)
379
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, narrative: state.narrative, deck: state.activeDeck ? state.decks[state.activeDeck] : undefined }, null, 2)
380
+ }
381
+
197
382
  if (args.action === "review") {
198
383
  const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
199
384
  const targetId = activeReviewTargetId(reviewed.state)
@@ -222,6 +407,84 @@ export default tool({
222
407
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
223
408
  }
224
409
 
410
+ if (args.action === "compileDeckPlan") {
411
+ const compiled = compileDeckPlanFromNarrative(state)
412
+ if (compiled.result.compiled) {
413
+ recordWorkspaceAction(compiled.state, {
414
+ type: "deck.plan_compiled",
415
+ actor: "revela-decks",
416
+ inputs: { narrativeId: compiled.state.narrative?.id, activeDeck: compiled.state.activeDeck },
417
+ outputs: {
418
+ narrativeHash: compiled.result.narrativeHash,
419
+ slideCount: compiled.result.slideCount,
420
+ outputPath: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck]?.outputPath : undefined,
421
+ },
422
+ status: "success",
423
+ summary: `Compiled deck plan from canonical narrative with ${compiled.result.slideCount} slide${compiled.result.slideCount === 1 ? "" : "s"}.`,
424
+ nodeIds: [compiled.state.narrative?.id, compiled.state.activeDeck ? `artifact:${compiled.state.decks[compiled.state.activeDeck]?.outputPath ?? compiled.state.activeDeck}` : undefined].filter((item): item is string => Boolean(item)),
425
+ })
426
+ }
427
+ writeDecksState(workspaceRoot, compiled.state)
428
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: compiled.result, deck: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck] : undefined, narrative: compiled.state.narrative }, null, 2)
429
+ }
430
+
431
+ if (args.action === "backfillClaimRefs") {
432
+ const backfilled = backfillSlideClaimRefsFromCoverage(state)
433
+ writeDecksState(workspaceRoot, backfilled.state)
434
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: backfilled.result, deck: backfilled.state.activeDeck ? backfilled.state.decks[backfilled.state.activeDeck] : undefined, narrative: backfilled.state.narrative }, null, 2)
435
+ }
436
+
437
+ if (args.action === "reviewNarrative") {
438
+ const reviewed = reviewNarrativeState(state)
439
+ recordNarrativeReviewAction(reviewed.state, reviewed.result)
440
+ writeDecksState(workspaceRoot, reviewed.state)
441
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result, narrative: reviewed.state.narrative }, null, 2)
442
+ }
443
+
444
+ if (args.action === "approveNarrative") {
445
+ const approved = approveNarrativeState(state, {
446
+ approvedBy: args.approvalBy,
447
+ scope: args.approvalScope,
448
+ note: args.approvalNote,
449
+ })
450
+ recordNarrativeApprovalAction(approved.state, approved.result)
451
+ writeDecksState(workspaceRoot, approved.state)
452
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: approved.result, narrative: approved.state.narrative }, null, 2)
453
+ }
454
+
455
+ if (args.action === "deriveResearchGaps") {
456
+ const derived = deriveResearchGapsFromReadiness(state)
457
+ writeDecksState(workspaceRoot, derived.state)
458
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: derived.result, narrative: derived.state.narrative }, null, 2)
459
+ }
460
+
461
+ if (args.action === "upsertResearchGaps") {
462
+ if (!args.researchGaps?.length) return JSON.stringify({ ok: false, error: "researchGaps are required for upsertResearchGaps" })
463
+ const upserted = upsertResearchGapsInState(state, args.researchGaps as any[])
464
+ writeDecksState(workspaceRoot, upserted.state)
465
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: upserted.result, narrative: upserted.state.narrative }, null, 2)
466
+ }
467
+
468
+ if (args.action === "updateResearchGap") {
469
+ if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for updateResearchGap" })
470
+ const updated = updateResearchGapInState(state, {
471
+ id: args.gapId,
472
+ status: args.gapStatus as any,
473
+ findingsFile: args.findingsFile,
474
+ evidenceBindingIds: args.evidenceBindingIds,
475
+ notes: args.gapNotes,
476
+ })
477
+ writeDecksState(workspaceRoot, updated.state)
478
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: updated.result, narrative: updated.state.narrative }, null, 2)
479
+ }
480
+
481
+ if (args.action === "closeResearchGap") {
482
+ if (!args.gapId?.trim()) return JSON.stringify({ ok: false, error: "gapId is required for closeResearchGap" })
483
+ const closed = closeResearchGapInState(state, args.gapId, args.gapNotes)
484
+ writeDecksState(workspaceRoot, closed.state)
485
+ return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: closed.result, narrative: closed.state.narrative }, null, 2)
486
+ }
487
+
225
488
  if (args.action === "applyEvidenceCandidates") {
226
489
  const candidateIds = args.candidateIds ?? []
227
490
  if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
@@ -0,0 +1,84 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { hasDecksState, readDecksState } from "../lib/decks-state"
3
+ import { buildNarrativeMap } from "../lib/narrative-state/map"
4
+ import { validateNarrativeDisplayModel, type NarrativeDisplayModel, type NarrativeViewLanguage } from "../lib/narrative-state/display"
5
+ import { writeNarrativeMapHtml } from "../lib/commands/narrative"
6
+ import { openUrl } from "../lib/edit/open"
7
+
8
+ export default tool({
9
+ description:
10
+ "Render Revela's read-only narrative claim-flow UI from the current deterministic narrative map plus an optional localized display model. " +
11
+ "This tool validates display IDs against DECKS.json, opens a local HTML view, and never mutates workspace state.",
12
+ args: {
13
+ language: tool.schema.string().describe("UI language request from /revela narrative. May be any language tag or language name, such as en, zh-CN, fr, de, Korean, Arabic, or Portuguese-BR."),
14
+ narrativeHash: tool.schema.string().optional().describe("Narrative hash from the prompt projection. Used to detect stale display prompts."),
15
+ displayModel: tool.schema.object({
16
+ version: tool.schema.number().describe("Must be 1."),
17
+ language: tool.schema.string().describe("Must exactly match the top-level language request."),
18
+ pageTitle: tool.schema.string().optional(),
19
+ summaryLine: tool.schema.string().optional(),
20
+ labels: tool.schema.object({
21
+ eyebrow: tool.schema.string().optional(),
22
+ claimFlow: tool.schema.string().optional(),
23
+ flowNote: tool.schema.string().optional(),
24
+ selectedClaim: tool.schema.string().optional(),
25
+ claim: tool.schema.string().optional(),
26
+ claimId: tool.schema.string().optional(),
27
+ status: tool.schema.string().optional(),
28
+ supportedScope: tool.schema.string().optional(),
29
+ unsupportedScope: tool.schema.string().optional(),
30
+ incomingRelations: tool.schema.string().optional(),
31
+ outgoingRelations: tool.schema.string().optional(),
32
+ evidence: tool.schema.string().optional(),
33
+ objections: tool.schema.string().optional(),
34
+ risks: tool.schema.string().optional(),
35
+ researchGaps: tool.schema.string().optional(),
36
+ coveredSlides: tool.schema.string().optional(),
37
+ noClaims: tool.schema.string().optional(),
38
+ none: tool.schema.string().optional(),
39
+ }).optional(),
40
+ claimCards: tool.schema.array(tool.schema.object({
41
+ claimId: tool.schema.string().describe("Existing canonical claim id. Must match the deterministic map."),
42
+ displayTitle: tool.schema.string().optional().describe("Display-only localized claim title in the requested language. For Chinese manufacturing/industrial AI context, translate autonomy as 自主化/自主能力, not 自治; convert slug-like claim text into a readable title."),
43
+ roleLabel: tool.schema.string().optional(),
44
+ narrativeJob: tool.schema.string().optional(),
45
+ evidenceSummary: tool.schema.string().optional(),
46
+ riskOrGapSummary: tool.schema.string().optional(),
47
+ })).optional(),
48
+ relations: tool.schema.array(tool.schema.object({
49
+ fromClaimId: tool.schema.string(),
50
+ toClaimId: tool.schema.string(),
51
+ relation: tool.schema.enum(["leads_to", "supports", "depends_on", "contrasts_with", "constrains", "answers"]),
52
+ displayLabel: tool.schema.string().optional().describe("Display-only localization of an existing canonical relation label. Omit for inferred relations."),
53
+ displayRationale: tool.schema.string().optional().describe("Display-only localization of an existing canonical rationale. Omit when canonical rationale is missing or the relation is inferred."),
54
+ })).optional(),
55
+ }).optional().describe("Localized and organized display-only projection. It must not add facts or alter IDs."),
56
+ },
57
+ async execute(args, context) {
58
+ const workspaceRoot = context.directory ?? process.cwd()
59
+ try {
60
+ if (!hasDecksState(workspaceRoot)) {
61
+ return JSON.stringify({ ok: false, error: "No DECKS.json found. Run /revela init first." })
62
+ }
63
+ const language = args.language as NarrativeViewLanguage
64
+ const map = buildNarrativeMap(readDecksState(workspaceRoot))
65
+ const stalePrompt = Boolean(args.narrativeHash && args.narrativeHash !== map.snapshot.narrativeHash)
66
+ const display = validateNarrativeDisplayModel(map, args.displayModel as NarrativeDisplayModel | undefined, language)
67
+ const htmlPath = writeNarrativeMapHtml(map, display)
68
+ const url = `file://${htmlPath}`
69
+ openUrl(url)
70
+ return JSON.stringify({ ok: true, url, path: htmlPath, narrativeHash: map.snapshot.narrativeHash, stalePrompt, fallback: false }, null, 2)
71
+ } catch (e: any) {
72
+ try {
73
+ const language = (args.language ?? "en") as NarrativeViewLanguage
74
+ const map = buildNarrativeMap(readDecksState(workspaceRoot))
75
+ const htmlPath = writeNarrativeMapHtml(map)
76
+ const url = `file://${htmlPath}`
77
+ openUrl(url)
78
+ return JSON.stringify({ ok: false, fallback: true, url, path: htmlPath, error: e.message || String(e) }, null, 2)
79
+ } catch (fallbackError: any) {
80
+ return JSON.stringify({ ok: false, fallback: false, error: e.message || String(e), fallbackError: fallbackError.message || String(fallbackError) })
81
+ }
82
+ }
83
+ },
84
+ })
@@ -18,6 +18,17 @@ const EXCLUDE_DIRS = new Set([
18
18
  "designs", "domains", // Exclude revela plugin assets
19
19
  ])
20
20
 
21
+ const EXCLUDE_FILENAMES = new Set([
22
+ "AGENTS.md",
23
+ "DECKS.md",
24
+ "README.md",
25
+ "README.zh-CN.md",
26
+ ])
27
+
28
+ function isExcludedFile(entry: string): boolean {
29
+ return entry.startsWith("~$") || EXCLUDE_FILENAMES.has(entry)
30
+ }
31
+
21
32
  type FileEntry = {
22
33
  path: string
23
34
  type: string
@@ -84,6 +95,8 @@ function scanDir(dir: string, rootDir: string, results: FileEntry[], maxDepth: n
84
95
  if (stat.isDirectory()) {
85
96
  scanDir(fullPath, rootDir, results, maxDepth, depth + 1)
86
97
  } else if (stat.isFile()) {
98
+ if (isExcludedFile(entry)) continue
99
+
87
100
  const ext = extname(entry).toLowerCase()
88
101
  if (DOC_EXTENSIONS.has(ext)) {
89
102
  const sourceMaterial = sourceMaterialMetadata(fullPath, rootDir)
@@ -104,7 +117,7 @@ export default tool({
104
117
  "Scan the current workspace for document and data files that can be used as research input. " +
105
118
  "Returns a structured list of all found files with their type and size. " +
106
119
  "Searches for: PDF, Word (docx/doc), Excel (xlsx/xls), PowerPoint (pptx/ppt), CSV, Markdown, and text files. " +
107
- "Excludes node_modules, .git, dist, and the researches/ output directory. " +
120
+ "Excludes node_modules, .git, dist, the researches/ output directory, project docs, and Office lock files. " +
108
121
  "Use this as the first step before reading workspace documents.",
109
122
  args: {
110
123
  path: tool.schema