@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.
- package/README.md +35 -29
- package/README.zh-CN.md +35 -29
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +7 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/narrative.ts +160 -0
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +46 -3
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspection-context/compile.ts +159 -5
- package/lib/inspection-context/project.ts +20 -0
- package/lib/narrative-state/coverage.ts +100 -0
- package/lib/narrative-state/display.ts +219 -0
- package/lib/narrative-state/executive-brief.ts +246 -0
- package/lib/narrative-state/hash.ts +61 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +361 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/queries.ts +433 -0
- package/lib/narrative-state/readiness.ts +359 -0
- package/lib/narrative-state/render-plan.ts +250 -0
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +172 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +174 -2
- package/lib/workspace-state/types.ts +13 -1
- package/package.json +1 -1
- package/plugin.ts +58 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +265 -2
- package/tools/narrative-view.ts +84 -0
- 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
|
|
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
|
+
})
|
package/tools/workspace-scan.ts
CHANGED
|
@@ -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,
|
|
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
|