@cyber-dash-tech/revela 0.17.6 → 0.17.8

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 (35) hide show
  1. package/README.md +53 -23
  2. package/README.zh-CN.md +53 -23
  3. package/bin/revela.ts +98 -0
  4. package/lib/edit/prompt.ts +6 -2
  5. package/lib/edit/server.ts +2 -2
  6. package/lib/inspect/prompt.ts +5 -1
  7. package/lib/refine/comment-requests.ts +77 -0
  8. package/lib/refine/open.ts +5 -2
  9. package/lib/refine/prompt-bridge.ts +219 -0
  10. package/lib/refine/qa-suppression.ts +41 -0
  11. package/lib/refine/server.ts +122 -34
  12. package/lib/runtime/index.ts +225 -0
  13. package/lib/runtime/research.ts +175 -0
  14. package/lib/runtime/review.ts +270 -0
  15. package/lib/runtime/story.ts +53 -0
  16. package/package.json +6 -1
  17. package/plugin.ts +4 -2
  18. package/plugins/revela/.codex-plugin/plugin.json +37 -0
  19. package/plugins/revela/.mcp.json +11 -0
  20. package/plugins/revela/assets/README.md +2 -0
  21. package/plugins/revela/hooks/hooks.json +28 -0
  22. package/plugins/revela/hooks/revela_guard.ts +10 -0
  23. package/plugins/revela/hooks/revela_post_write_notice.ts +18 -0
  24. package/plugins/revela/mcp/revela-server.ts +504 -0
  25. package/plugins/revela/mcp/runtime-resolver.ts +109 -0
  26. package/plugins/revela/skills/revela-design/SKILL.md +20 -0
  27. package/plugins/revela/skills/revela-domain/SKILL.md +18 -0
  28. package/plugins/revela/skills/revela-export/SKILL.md +21 -0
  29. package/plugins/revela/skills/revela-init/SKILL.md +36 -0
  30. package/plugins/revela/skills/revela-make-deck/SKILL.md +37 -0
  31. package/plugins/revela/skills/revela-research/SKILL.md +38 -0
  32. package/plugins/revela/skills/revela-review-deck/SKILL.md +33 -0
  33. package/plugins/revela/skills/revela-story/SKILL.md +24 -0
  34. package/tools/decks.ts +10 -78
  35. package/tools/research-save.ts +8 -72
@@ -0,0 +1,225 @@
1
+ import { existsSync } from "fs"
2
+ import { resolve } from "path"
3
+ import { activeDesign, activateDesign, getDesignSkillMd, listDesigns, seedBuiltinDesigns } from "../design/designs"
4
+ import { createDeckFoundation as createDeckFoundationShell } from "../deck-html/foundation"
5
+ import { activeDomain, activateDomain, getDomainSkillMd, listDomains, seedBuiltinDomains } from "../domain/domains"
6
+ import { computeNarrativeHash } from "../narrative-state/hash"
7
+ import { compileNarrativeVault } from "../narrative-vault/compile"
8
+ import { runNarrativeMarkdownQa, type MarkdownQaOptions } from "../narrative-vault/markdown-qa"
9
+ import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
10
+ import { exportToPdf } from "../pdf/export"
11
+ import { exportToPptx } from "../pptx/export"
12
+ import { assertExportQAPassed } from "../qa/export-gate"
13
+ import { formatArtifactQAReport, runArtifactQA } from "../qa/artifact"
14
+ import { extractDesignClasses } from "../design/designs"
15
+ import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
16
+ export { bindResearchFindings, evaluateResearchFindings, researchSave, researchTargets } from "./research"
17
+ export { reviewDeckOpen, reviewDeckRead } from "./review"
18
+ export { storyRead } from "./story"
19
+
20
+ export interface RuntimeWorkspaceInput {
21
+ workspaceRoot?: string
22
+ }
23
+
24
+ export interface RuntimeFileInput extends RuntimeWorkspaceInput {
25
+ file: string
26
+ }
27
+
28
+ export interface RuntimeDeckFoundationInput extends RuntimeWorkspaceInput {
29
+ outputPath: string
30
+ title: string
31
+ language: string
32
+ designName?: string
33
+ mode?: "create" | "repair"
34
+ overwrite?: boolean
35
+ }
36
+
37
+ export interface RuntimeDesignReadInput {
38
+ name?: string
39
+ }
40
+
41
+ export interface RuntimeNameInput {
42
+ name: string
43
+ }
44
+
45
+ export function doctor(input: RuntimeWorkspaceInput = {}) {
46
+ const workspaceRoot = root(input.workspaceRoot)
47
+ return {
48
+ ok: true,
49
+ workspaceRoot,
50
+ hasNarrativeVault: existsSync(resolve(workspaceRoot, "revela-narrative")),
51
+ hasDeckPlan: existsSync(resolve(workspaceRoot, "deck-plan")),
52
+ hasDecksJson: existsSync(resolve(workspaceRoot, "DECKS.json")),
53
+ activeDesign: safe(activeDesign),
54
+ }
55
+ }
56
+
57
+ export function compileNarrative(input: RuntimeWorkspaceInput = {}) {
58
+ const workspaceRoot = root(input.workspaceRoot)
59
+ return compileNarrativeVault(workspaceRoot)
60
+ }
61
+
62
+ export function markdownQa(input: RuntimeWorkspaceInput & MarkdownQaOptions = {}) {
63
+ const workspaceRoot = root(input.workspaceRoot)
64
+ return runNarrativeMarkdownQa(workspaceRoot, {
65
+ scope: input.scope,
66
+ strictness: input.strictness,
67
+ touched: input.touched,
68
+ })
69
+ }
70
+
71
+ export function readDeckPlan(input: RuntimeWorkspaceInput = {}) {
72
+ const workspaceRoot = root(input.workspaceRoot)
73
+ const compiled = compileNarrativeVault(workspaceRoot)
74
+ const knownNodeIds = compiled.graph ? new Set(compiled.graph.nodes.map((node) => node.id)) : undefined
75
+ return readDeckPlanArtifact(workspaceRoot, {
76
+ narrativeHash: compiled.narrative ? computeNarrativeHash(compiled.narrative) : undefined,
77
+ knownNodeIds,
78
+ })
79
+ }
80
+
81
+ export function createDeckFoundation(input: RuntimeDeckFoundationInput) {
82
+ return createDeckFoundationShell({
83
+ workspaceRoot: root(input.workspaceRoot),
84
+ outputPath: input.outputPath,
85
+ title: input.title,
86
+ language: input.language,
87
+ designName: input.designName,
88
+ mode: input.mode,
89
+ overwrite: input.overwrite ?? false,
90
+ })
91
+ }
92
+
93
+ export async function runDeckQa(input: RuntimeFileInput) {
94
+ const workspaceRoot = root(input.workspaceRoot)
95
+ const filePath = resolve(workspaceRoot, input.file)
96
+ let vocabulary
97
+ try {
98
+ vocabulary = extractDesignClasses()
99
+ } catch {
100
+ // Design vocabulary is optional.
101
+ }
102
+ const report = await runArtifactQA({ workspaceRoot, filePath, vocabulary })
103
+ return {
104
+ ok: report.passed,
105
+ summary: {
106
+ passed: report.passed,
107
+ errors: report.hardErrorCount,
108
+ warnings: report.warningCount,
109
+ },
110
+ report,
111
+ markdown: formatArtifactQAReport(report),
112
+ }
113
+ }
114
+
115
+ export async function exportPdf(input: RuntimeFileInput) {
116
+ const workspaceRoot = root(input.workspaceRoot)
117
+ const filePath = resolve(workspaceRoot, input.file)
118
+ await assertExportQAPassed(filePath, { workspaceRoot })
119
+ const result = await exportToPdf(filePath)
120
+ recordRenderedArtifact(workspaceRoot, {
121
+ sourceHtmlPath: workspaceRelative(resolve(workspaceRoot), filePath),
122
+ outputPath: result.outputPath,
123
+ type: "pdf",
124
+ actor: "revela-codex-mcp",
125
+ })
126
+ return { ok: true, ...result }
127
+ }
128
+
129
+ export async function exportPptx(input: RuntimeFileInput & { speakerNotes?: Array<string | null | undefined> }) {
130
+ const workspaceRoot = root(input.workspaceRoot)
131
+ const filePath = resolve(workspaceRoot, input.file)
132
+ await assertExportQAPassed(filePath, { workspaceRoot })
133
+ const result = await exportToPptx(filePath, { speakerNotes: input.speakerNotes })
134
+ recordRenderedArtifact(workspaceRoot, {
135
+ sourceHtmlPath: workspaceRelative(resolve(workspaceRoot), filePath),
136
+ outputPath: result.outputPath,
137
+ type: "pptx",
138
+ actor: "revela-codex-mcp",
139
+ })
140
+ return { ok: true, ...result }
141
+ }
142
+
143
+ export function designList() {
144
+ seedBuiltinDesigns()
145
+ return {
146
+ ok: true,
147
+ activeDesign: activeDesign(),
148
+ designs: listDesigns({ includeInternal: false }).map((design) => ({
149
+ name: design.name,
150
+ description: design.description,
151
+ author: design.author,
152
+ version: design.version,
153
+ })),
154
+ }
155
+ }
156
+
157
+ export function designRead(input: RuntimeDesignReadInput = {}) {
158
+ seedBuiltinDesigns()
159
+ const name = input.name || activeDesign()
160
+ return {
161
+ ok: true,
162
+ name,
163
+ markdown: getDesignSkillMd(name),
164
+ }
165
+ }
166
+
167
+ export function designActivate(input: RuntimeNameInput) {
168
+ seedBuiltinDesigns()
169
+ activateDesign(requiredName(input, "design"))
170
+ return {
171
+ ok: true,
172
+ activeDesign: activeDesign(),
173
+ }
174
+ }
175
+
176
+ export function domainList() {
177
+ seedBuiltinDomains()
178
+ return {
179
+ ok: true,
180
+ activeDomain: activeDomain(),
181
+ domains: listDomains().map((domain) => ({
182
+ name: domain.name,
183
+ description: domain.description,
184
+ author: domain.author,
185
+ version: domain.version,
186
+ })),
187
+ }
188
+ }
189
+
190
+ export function domainRead(input: RuntimeDesignReadInput = {}) {
191
+ seedBuiltinDomains()
192
+ const name = input.name || activeDomain()
193
+ return {
194
+ ok: true,
195
+ name,
196
+ markdown: getDomainSkillMd(name),
197
+ }
198
+ }
199
+
200
+ export function domainActivate(input: RuntimeNameInput) {
201
+ seedBuiltinDomains()
202
+ activateDomain(requiredName(input, "domain"))
203
+ return {
204
+ ok: true,
205
+ activeDomain: activeDomain(),
206
+ }
207
+ }
208
+
209
+ function root(workspaceRoot: string | undefined): string {
210
+ return resolve(workspaceRoot || process.cwd())
211
+ }
212
+
213
+ function safe<T>(fn: () => T): T | undefined {
214
+ try {
215
+ return fn()
216
+ } catch {
217
+ return undefined
218
+ }
219
+ }
220
+
221
+ function requiredName(input: RuntimeNameInput, label: string): string {
222
+ const name = input?.name?.trim()
223
+ if (!name) throw new Error(`${label} name is required`)
224
+ return name
225
+ }
@@ -0,0 +1,175 @@
1
+ import { mkdirSync, writeFileSync } from "fs"
2
+ import { join, resolve } from "path"
3
+ import { DECKS_STATE_FILE, hasDecksState, readDecksState, readOrCreateDecksState, writeDecksState, type DecksState } from "../decks-state"
4
+ import { stableEvidenceId } from "../narrative-state/hash"
5
+ import { evaluateResearchFindingsBinding } from "../narrative-state/research-binding-eval"
6
+ import { deriveResearchTargets } from "../narrative-state/research-gaps"
7
+ import { compileCacheMirrorNarrativeVault } from "../narrative-vault/compile-mirror"
8
+ import { compileNarrativeVault, formatVaultDiagnosticReport, hasNarrativeVault, updateVaultResearchGapNode, upsertVaultEvidenceNode } from "../narrative-vault"
9
+ import { recordWorkspaceAction } from "../workspace-state/actions"
10
+
11
+ export interface ResearchSaveInput {
12
+ topic: string
13
+ filename: string
14
+ content: string
15
+ sources?: string[]
16
+ workspaceRoot?: string
17
+ }
18
+
19
+ export interface ResearchFindingsInput {
20
+ findingsFile: string
21
+ workspaceRoot?: string
22
+ }
23
+
24
+ export interface BindResearchFindingsInput extends ResearchFindingsInput {
25
+ evidenceId?: string
26
+ }
27
+
28
+ export function researchTargets(input: { workspaceRoot?: string } = {}) {
29
+ const workspaceRoot = root(input.workspaceRoot)
30
+ const state = readOrCreateDecksState(workspaceRoot)
31
+ return { ok: true, path: DECKS_STATE_FILE, result: deriveResearchTargets(state, { workspaceRoot }) }
32
+ }
33
+
34
+ export function researchSave(input: ResearchSaveInput) {
35
+ const workspaceRoot = root(input.workspaceRoot)
36
+ const topicKey = keyify(input.topic || "research")
37
+ const fileKey = keyify(input.filename || "findings")
38
+ const topicDir = join(workspaceRoot, "researches", topicKey)
39
+ const sources = input.sources ?? []
40
+
41
+ mkdirSync(topicDir, { recursive: true })
42
+ const relPath = `researches/${topicKey}/${fileKey}.md`
43
+ writeFileSync(join(topicDir, `${fileKey}.md`), `${buildFrontmatter(input.topic, fileKey, sources)}\n\n${input.content ?? ""}\n`, "utf-8")
44
+
45
+ if (!hasDecksState(workspaceRoot)) return { ok: true, path: relPath }
46
+
47
+ const state = readDecksState(workspaceRoot)
48
+ recordWorkspaceAction(state, {
49
+ type: "research.findings_saved",
50
+ actor: "revela-research-save",
51
+ inputs: { topic: topicKey, axis: fileKey, sourceCount: sources.length },
52
+ outputs: { path: relPath, sources },
53
+ summary: `Saved research findings for ${topicKey}/${fileKey}.`,
54
+ nodeIds: [`finding:${relPath}`],
55
+ })
56
+ const bindingEval = evaluateResearchFindingsBinding(state, workspaceRoot, relPath)
57
+ writeDecksState(workspaceRoot, state)
58
+ return { ok: true, path: relPath, bindingEval }
59
+ }
60
+
61
+ export function evaluateResearchFindings(input: ResearchFindingsInput) {
62
+ const workspaceRoot = root(input.workspaceRoot)
63
+ if (!input.findingsFile?.trim()) return { ok: false, error: "findingsFile is required for evaluateResearchFindings" }
64
+ const state = readOrCreateDecksState(workspaceRoot)
65
+ const bindingEval = evaluateResearchFindingsBinding(state, workspaceRoot, input.findingsFile)
66
+ const targets = deriveResearchTargets(state, { workspaceRoot })
67
+ const vaultDiagnostics = hasNarrativeVault(workspaceRoot)
68
+ ? formatVaultDiagnosticReport(compileNarrativeVault(workspaceRoot, { fallbackApprovals: state.narrative?.approvals ?? [] }).diagnostics)
69
+ : undefined
70
+ return { ok: true, path: DECKS_STATE_FILE, result: { bindingEval, selected: targets.selected, vaultDiagnostics } }
71
+ }
72
+
73
+ export function bindResearchFindings(input: BindResearchFindingsInput) {
74
+ const workspaceRoot = root(input.workspaceRoot)
75
+ if (!hasNarrativeVault(workspaceRoot)) return { ok: false, error: "bindResearchFindings requires revela-narrative/ to exist. Use initNarrativeVault first, then evaluateResearchFindings." }
76
+ if (!input.findingsFile?.trim()) return { ok: false, error: "findingsFile is required for bindResearchFindings" }
77
+
78
+ const state = readOrCreateDecksState(workspaceRoot)
79
+ const bindingEval = evaluateResearchFindingsBinding(state, workspaceRoot, input.findingsFile)
80
+ if (bindingEval.status !== "bindable" || !bindingEval.claimId || !bindingEval.recommendedEvidenceDraft) {
81
+ return { ok: false, skipped: true, reason: "findings are not safely bindable", bindingEval }
82
+ }
83
+
84
+ const draft = bindingEval.recommendedEvidenceDraft
85
+ const evidence = {
86
+ id: input.evidenceId?.trim() || stableEvidenceId(bindingEval.claimId, `${bindingEval.findingsFile}:${draft.quote ?? ""}`),
87
+ claimId: bindingEval.claimId,
88
+ source: draft.source,
89
+ sourcePath: draft.sourcePath,
90
+ findingsFile: draft.findingsFile ?? bindingEval.findingsFile,
91
+ quote: draft.quote,
92
+ location: draft.location,
93
+ url: draft.url,
94
+ caveat: draft.caveat,
95
+ supportScope: draft.supportScope,
96
+ unsupportedScope: draft.unsupportedScope,
97
+ strength: draft.strength,
98
+ }
99
+ const missing = missingBindableEvidenceFields(evidence)
100
+ if (missing.length > 0) return { ok: false, skipped: true, reason: "recommended evidence draft is incomplete", missingFields: missing, bindingEval }
101
+
102
+ const mutation = upsertVaultEvidenceNode(workspaceRoot, evidence as any)
103
+ if (!mutation.ok) return { ok: false, mutation, bindingEval }
104
+
105
+ const gap = exactResearchGapForBinding(state, bindingEval.findingsFile, bindingEval.claimId)
106
+ const gapMutation = gap
107
+ ? updateVaultResearchGapNode(workspaceRoot, {
108
+ id: gap.id,
109
+ status: "evidence_bound",
110
+ findingsFile: bindingEval.findingsFile,
111
+ evidenceBindingIds: [...new Set([...(gap.evidenceBindingIds ?? []), evidence.id])],
112
+ notes: gap.notes,
113
+ })
114
+ : undefined
115
+ const compiled = compileCacheMirrorNarrativeVault(workspaceRoot)
116
+ return {
117
+ ok: compiled.result.ok,
118
+ path: mutation.file,
119
+ bindingEval,
120
+ mutation,
121
+ gapMutation: gapMutation ?? { ok: true, skipped: true, reason: "no exact single research gap matched this findings file and claim" },
122
+ evidence,
123
+ diagnostics: compiled.result.diagnostics,
124
+ diagnosticReport: compiled.diagnosticReport,
125
+ narrative: compiled.result.narrative,
126
+ }
127
+ }
128
+
129
+ function today(): string {
130
+ return new Date().toISOString().slice(0, 10)
131
+ }
132
+
133
+ function keyify(s: string): string {
134
+ return s
135
+ .toLowerCase()
136
+ .replace(/[^a-z0-9]+/g, "-")
137
+ .replace(/^-+|-+$/g, "")
138
+ }
139
+
140
+ function buildFrontmatter(topic: string, axis: string, sources: string[]): string {
141
+ const lines = [
142
+ "---",
143
+ `topic: ${topic}`,
144
+ `axis: ${axis}`,
145
+ `date: ${today()}`,
146
+ ]
147
+ if (sources.length > 0) {
148
+ lines.push("sources:")
149
+ for (const source of sources) lines.push(` - "${source.replace(/"/g, '\\"')}"`)
150
+ }
151
+ lines.push("---")
152
+ return lines.join("\n")
153
+ }
154
+
155
+ function missingBindableEvidenceFields(input: Record<string, unknown>): string[] {
156
+ const missing: string[] = []
157
+ for (const key of ["id", "claimId", "source", "quote", "supportScope", "unsupportedScope", "caveat", "strength"] as const) {
158
+ if (!String(input[key] ?? "").trim()) missing.push(key)
159
+ }
160
+ if (!String(input.sourcePath ?? "").trim() && !String(input.url ?? "").trim() && !String(input.findingsFile ?? "").trim()) missing.push("sourcePath|url|findingsFile")
161
+ return missing
162
+ }
163
+
164
+ function exactResearchGapForBinding(state: DecksState, findingsFile: string, claimId: string) {
165
+ const gaps = state.narrative?.researchGaps ?? []
166
+ const exact = gaps.filter((gap) => gap.targetType === "claim" && gap.targetId === claimId && gap.findingsFile === findingsFile)
167
+ if (exact.length === 1) return exact[0]
168
+ if (exact.length > 1) return undefined
169
+ const byClaim = gaps.filter((gap) => gap.targetType === "claim" && gap.targetId === claimId && !gap.findingsFile)
170
+ return byClaim.length === 1 ? byClaim[0] : undefined
171
+ }
172
+
173
+ function root(workspaceRoot: string | undefined): string {
174
+ return resolve(workspaceRoot || process.cwd())
175
+ }
@@ -0,0 +1,270 @@
1
+ import { existsSync } from "fs"
2
+ import { resolve } from "path"
3
+ import { DECKS_STATE_FILE, hasDecksState, normalizeWorkspaceDeckState, readDecksState } from "../decks-state"
4
+ import { extractDesignClasses } from "../design/designs"
5
+ import { compileInspectionContext, type InspectionContext } from "../inspection-context/compile"
6
+ import { computeNarrativeHash } from "../narrative-state/hash"
7
+ import { readDeckPlanArtifact } from "../narrative-state/deck-plan-artifact"
8
+ import { compileNarrativeVault } from "../narrative-vault/compile"
9
+ import { formatVaultDiagnosticMarkdown, formatVaultDiagnosticReport } from "../narrative-vault/diagnostic-report"
10
+ import { formatArtifactQAReport, runArtifactQA } from "../qa/artifact"
11
+ import { openRefineDeck } from "../refine/open"
12
+ import { createCodexExecReviewPromptBridge } from "../refine/prompt-bridge"
13
+ import { workspaceRelative } from "../workspace-state/rendered-artifacts"
14
+
15
+ export interface ReviewDeckReadInput {
16
+ workspaceRoot?: string
17
+ file: string
18
+ format?: "json" | "markdown"
19
+ }
20
+
21
+ export interface ReviewDeckOpenInput extends ReviewDeckReadInput {
22
+ bridge?: "codex-exec"
23
+ openBrowser?: boolean
24
+ openUrl?: (url: string) => void
25
+ }
26
+
27
+ export interface ReviewDeckInspectionContextResult {
28
+ ok: boolean
29
+ skipped: boolean
30
+ reason?: string
31
+ context?: InspectionContext
32
+ }
33
+
34
+ export async function reviewDeckRead(input: ReviewDeckReadInput): Promise<any> {
35
+ const workspaceRoot = root(input.workspaceRoot)
36
+ const requestedFile = input.file?.trim()
37
+ if (!requestedFile) {
38
+ return {
39
+ ok: false,
40
+ file: "",
41
+ error: "Missing required file.",
42
+ diagnostics: [{ severity: "error", code: "missing_file", message: "Provide a workspace-relative or absolute deck HTML file." }],
43
+ }
44
+ }
45
+
46
+ const filePath = resolve(workspaceRoot, requestedFile)
47
+ const file = workspaceRelative(workspaceRoot, filePath)
48
+ if (!existsSync(filePath)) {
49
+ return {
50
+ ok: false,
51
+ file,
52
+ error: `Deck HTML file not found: ${file}`,
53
+ diagnostics: [{ severity: "error", code: "file_not_found", message: `Deck HTML file not found: ${file}` }],
54
+ }
55
+ }
56
+
57
+ const artifactQa = await readArtifactQa(workspaceRoot, filePath)
58
+ const narrativeRead = readNarrative(workspaceRoot)
59
+ const deckPlan = readDeckPlan(workspaceRoot, narrativeRead.knownNodeIds, narrativeRead.narrativeHash)
60
+ const { knownNodeIds: _knownNodeIds, ...narrative } = narrativeRead
61
+ const inspectionContext = readInspectionContext(workspaceRoot, file)
62
+ const diagnostics = {
63
+ artifactQa: artifactQa.summary,
64
+ deckPlan: summarizeDeckPlan(deckPlan),
65
+ narrative: narrative.summary,
66
+ inspectionContext: inspectionContext.ok
67
+ ? { ok: true, skipped: false }
68
+ : { ok: false, skipped: true, reason: inspectionContext.reason },
69
+ }
70
+ const markdown = input.format === "markdown"
71
+ ? formatReviewDeckReadMarkdown({ file, artifactQa, deckPlan, narrative, inspectionContext })
72
+ : undefined
73
+
74
+ return {
75
+ ok: artifactQa.ok,
76
+ file,
77
+ artifactQa,
78
+ deckPlan,
79
+ narrative,
80
+ diagnostics,
81
+ inspectionContext,
82
+ artifactCoverage: inspectionContext.context?.artifactCoverage ?? [],
83
+ evidenceTrace: inspectionContext.context?.slides.flatMap((slide) => slide.evidence) ?? narrative.evidenceTrace,
84
+ markdown,
85
+ }
86
+ }
87
+
88
+ export async function reviewDeckOpen(input: ReviewDeckOpenInput): Promise<any> {
89
+ const workspaceRoot = root(input.workspaceRoot)
90
+ const requestedFile = input.file?.trim()
91
+ if (!requestedFile) {
92
+ return {
93
+ ok: false,
94
+ file: "",
95
+ error: "Missing required file.",
96
+ diagnostics: [{ severity: "error", code: "missing_file", message: "Provide a workspace-relative or absolute deck HTML file." }],
97
+ }
98
+ }
99
+
100
+ try {
101
+ const opened = openRefineDeck(requestedFile, {
102
+ workspaceRoot,
103
+ mode: "edit",
104
+ openBrowser: input.openBrowser,
105
+ openUrl: input.openUrl,
106
+ sessionID: `codex-review:${requestedFile}`,
107
+ promptBridge: createCodexExecReviewPromptBridge(),
108
+ })
109
+ return {
110
+ ok: true,
111
+ file: opened.deck.file,
112
+ deck: {
113
+ slug: opened.deck.slug,
114
+ file: opened.deck.file,
115
+ source: opened.deck.source,
116
+ },
117
+ bridge: input.bridge ?? "codex-exec",
118
+ url: opened.url,
119
+ token: new URL(opened.url).searchParams.get("token"),
120
+ mode: opened.mode,
121
+ openedBrowser: opened.openedBrowser,
122
+ reusedSession: opened.reusedSession,
123
+ liveSession: opened.liveSession,
124
+ source: opened.source,
125
+ stateNote: opened.stateNote,
126
+ preflightChanged: opened.preflightChanged,
127
+ }
128
+ } catch (e) {
129
+ const message = e instanceof Error ? e.message : String(e)
130
+ return {
131
+ ok: false,
132
+ file: requestedFile,
133
+ bridge: input.bridge ?? "codex-exec",
134
+ error: message,
135
+ diagnostics: [{ severity: "error", code: "review_open_failed", message }],
136
+ }
137
+ }
138
+ }
139
+
140
+ async function readArtifactQa(workspaceRoot: string, filePath: string) {
141
+ let vocabulary
142
+ try {
143
+ vocabulary = extractDesignClasses()
144
+ } catch {
145
+ // Design vocabulary is optional for standalone artifacts.
146
+ }
147
+ const report = await runArtifactQA({ workspaceRoot, filePath, vocabulary })
148
+ return {
149
+ ok: report.passed,
150
+ summary: {
151
+ passed: report.passed,
152
+ errors: report.hardErrorCount,
153
+ warnings: report.warningCount,
154
+ },
155
+ report,
156
+ markdown: formatArtifactQAReport(report),
157
+ }
158
+ }
159
+
160
+ function readNarrative(workspaceRoot: string): any {
161
+ if (!existsSync(resolve(workspaceRoot, "revela-narrative"))) {
162
+ return {
163
+ ok: false,
164
+ skipped: true,
165
+ reason: "No revela-narrative/ vault exists; narrative diagnostics skipped.",
166
+ summary: { ok: false, skipped: true, reason: "No revela-narrative/ vault exists; narrative diagnostics skipped." },
167
+ evidenceTrace: [],
168
+ }
169
+ }
170
+
171
+ const compiled = compileNarrativeVault(workspaceRoot)
172
+ const report = formatVaultDiagnosticReport(compiled.diagnostics)
173
+ const narrative = compiled.narrative
174
+ return {
175
+ ok: compiled.ok,
176
+ skipped: false,
177
+ narrativeHash: narrative ? computeNarrativeHash(narrative) : undefined,
178
+ summary: report,
179
+ diagnostics: compiled.diagnostics,
180
+ diagnosticsMarkdown: formatVaultDiagnosticMarkdown(report),
181
+ evidenceTrace: narrative?.evidenceBindings.map((binding) => ({
182
+ id: binding.id,
183
+ claimId: binding.claimId,
184
+ source: binding.source,
185
+ sourcePath: binding.sourcePath,
186
+ findingsFile: binding.findingsFile,
187
+ quote: binding.quote,
188
+ location: binding.location,
189
+ url: binding.url,
190
+ supportScope: binding.supportScope,
191
+ unsupportedScope: binding.unsupportedScope,
192
+ caveat: binding.caveat,
193
+ strength: binding.strength,
194
+ })) ?? [],
195
+ knownNodeIds: compiled.graph ? new Set(compiled.graph.nodes.map((node) => node.id)) : undefined,
196
+ }
197
+ }
198
+
199
+ function readDeckPlan(workspaceRoot: string, knownNodeIds: Set<string> | undefined, narrativeHash: string | undefined) {
200
+ return readDeckPlanArtifact(workspaceRoot, { knownNodeIds, narrativeHash })
201
+ }
202
+
203
+ function readInspectionContext(workspaceRoot: string, file: string): ReviewDeckInspectionContextResult {
204
+ if (!hasDecksState(workspaceRoot)) {
205
+ return {
206
+ ok: false,
207
+ skipped: true,
208
+ reason: `No ${DECKS_STATE_FILE} exists; legacy inspection context skipped for file-native deck.`,
209
+ }
210
+ }
211
+
212
+ try {
213
+ const state = normalizeWorkspaceDeckState(readDecksState(workspaceRoot), workspaceRoot)
214
+ const slug = Object.entries(state.decks).find(([, deck]) => normalizePath(deck.outputPath) === normalizePath(file))?.[0]
215
+ if (!slug) {
216
+ return {
217
+ ok: false,
218
+ skipped: true,
219
+ reason: `No ${DECKS_STATE_FILE} deck outputPath matches ${file}; legacy inspection context skipped.`,
220
+ }
221
+ }
222
+ return { ok: true, skipped: false, context: compileInspectionContext(state, slug) }
223
+ } catch (e) {
224
+ return {
225
+ ok: false,
226
+ skipped: true,
227
+ reason: `Could not compile legacy inspection context: ${e instanceof Error ? e.message : String(e)}`,
228
+ }
229
+ }
230
+ }
231
+
232
+ function summarizeDeckPlan(deckPlan: ReturnType<typeof readDeckPlanArtifact>) {
233
+ return {
234
+ ok: deckPlan.ok,
235
+ skipped: !deckPlan.ok && Boolean(deckPlan.reason?.includes("missing")),
236
+ warnings: deckPlan.warnings?.length ?? 0,
237
+ reason: deckPlan.reason,
238
+ }
239
+ }
240
+
241
+ function formatReviewDeckReadMarkdown(input: {
242
+ file: string
243
+ artifactQa: Awaited<ReturnType<typeof readArtifactQa>>
244
+ deckPlan: ReturnType<typeof readDeckPlanArtifact>
245
+ narrative: ReturnType<typeof readNarrative>
246
+ inspectionContext: ReviewDeckInspectionContextResult
247
+ }): string {
248
+ const lines = [
249
+ "# Review Deck Read",
250
+ "",
251
+ `File: \`${input.file}\``,
252
+ "",
253
+ `Artifact QA: ${input.artifactQa.summary.passed ? "passed" : "failed"} (${input.artifactQa.summary.errors} hard error(s), ${input.artifactQa.summary.warnings} warning(s))`,
254
+ `Deck-plan: ${input.deckPlan.ok ? "read" : `skipped/diagnostic - ${input.deckPlan.reason ?? "not available"}`}`,
255
+ `Narrative: ${input.narrative.skipped ? input.narrative.reason : input.narrative.summary.summary}`,
256
+ `Inspection context: ${input.inspectionContext.ok ? "read" : `skipped - ${input.inspectionContext.reason}`}`,
257
+ "",
258
+ input.artifactQa.markdown,
259
+ ]
260
+ if (!input.narrative.skipped && input.narrative.diagnosticsMarkdown) lines.push("", input.narrative.diagnosticsMarkdown)
261
+ return lines.join("\n")
262
+ }
263
+
264
+ function normalizePath(value: string): string {
265
+ return value.replace(/\\/g, "/").replace(/^\.\/+/, "")
266
+ }
267
+
268
+ function root(workspaceRoot: string | undefined): string {
269
+ return resolve(workspaceRoot || process.cwd())
270
+ }