@cyber-dash-tech/revela 0.17.6 → 0.17.7
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 +26 -46
- package/README.zh-CN.md +26 -46
- package/bin/revela.ts +98 -0
- package/lib/edit/prompt.ts +6 -2
- package/lib/edit/server.ts +2 -2
- package/lib/inspect/prompt.ts +5 -1
- package/lib/refine/comment-requests.ts +77 -0
- package/lib/refine/open.ts +5 -2
- package/lib/refine/prompt-bridge.ts +219 -0
- package/lib/refine/qa-suppression.ts +41 -0
- package/lib/refine/server.ts +122 -34
- package/lib/runtime/index.ts +225 -0
- package/lib/runtime/research.ts +175 -0
- package/lib/runtime/review.ts +270 -0
- package/lib/runtime/story.ts +53 -0
- package/package.json +6 -1
- package/plugin.ts +4 -2
- package/plugins/revela/.codex-plugin/plugin.json +37 -0
- package/plugins/revela/.mcp.json +11 -0
- package/plugins/revela/assets/README.md +2 -0
- package/plugins/revela/hooks/hooks.json +28 -0
- package/plugins/revela/hooks/revela_guard.ts +10 -0
- package/plugins/revela/hooks/revela_post_write_notice.ts +18 -0
- package/plugins/revela/mcp/revela-server.ts +504 -0
- package/plugins/revela/mcp/runtime-resolver.ts +109 -0
- package/plugins/revela/skills/revela-design/SKILL.md +20 -0
- package/plugins/revela/skills/revela-domain/SKILL.md +18 -0
- package/plugins/revela/skills/revela-export/SKILL.md +21 -0
- package/plugins/revela/skills/revela-init/SKILL.md +36 -0
- package/plugins/revela/skills/revela-make-deck/SKILL.md +37 -0
- package/plugins/revela/skills/revela-research/SKILL.md +38 -0
- package/plugins/revela/skills/revela-review-deck/SKILL.md +33 -0
- package/plugins/revela/skills/revela-story/SKILL.md +24 -0
- package/tools/decks.ts +10 -78
- 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
|
+
}
|