@cyber-dash-tech/revela 0.9.0 → 0.11.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 +54 -9
- package/README.zh-CN.md +54 -9
- package/designs/monet/DESIGN.md +9 -9
- package/designs/starter/DESIGN.md +8 -8
- package/designs/summit/DESIGN.md +9 -9
- package/lib/commands/help.ts +2 -0
- package/lib/commands/inspect.ts +23 -0
- package/lib/commands/pdf.ts +33 -5
- package/lib/commands/pptx.ts +14 -9
- package/lib/commands/refine.ts +26 -0
- package/lib/commands/review.ts +8 -2
- package/lib/deck-html/contract.ts +252 -0
- package/lib/decks-state.ts +574 -31
- package/lib/document-materials/extract.ts +20 -0
- package/lib/edit/resolve-deck.ts +13 -2
- package/lib/inspect/open.ts +63 -0
- package/lib/inspect/prompt.ts +32 -0
- package/lib/inspect/request.ts +70 -0
- package/lib/inspect/requests.ts +86 -0
- package/lib/inspect/server.ts +1063 -0
- package/lib/inspect/slide-index.ts +12 -0
- package/lib/inspection-context/compile.ts +346 -0
- package/lib/inspection-context/match.ts +169 -0
- package/lib/inspection-context/project.ts +263 -0
- package/lib/inspection-context/result.ts +160 -0
- package/lib/qa/export-gate.ts +8 -1
- package/lib/refine/open.ts +70 -0
- package/lib/refine/server.ts +1581 -0
- package/lib/workspace-state/actions.ts +71 -0
- package/lib/workspace-state/compat.ts +10 -0
- package/lib/workspace-state/evidence-status.ts +267 -0
- package/lib/workspace-state/graph.ts +426 -0
- package/lib/workspace-state/render-targets.ts +182 -0
- package/lib/workspace-state/rendered-artifacts.ts +43 -0
- package/lib/workspace-state/repository.ts +43 -0
- package/lib/workspace-state/research-attachments.ts +130 -0
- package/lib/workspace-state/review-snapshots.ts +127 -0
- package/lib/workspace-state/types.ts +119 -0
- package/package.json +1 -1
- package/plugin.ts +48 -1
- package/skill/SKILL.md +10 -5
- package/tools/decks.ts +61 -2
- package/tools/inspection-context.ts +22 -0
- package/tools/inspection-result.ts +63 -0
- package/tools/pdf.ts +9 -1
- package/tools/pptx.ts +10 -0
- package/tools/research-save.ts +15 -0
- package/tools/workspace-scan.ts +15 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { existsSync } from "fs"
|
|
2
|
+
import { basename, resolve, sep } from "path"
|
|
3
|
+
import {
|
|
4
|
+
readDecksState,
|
|
5
|
+
writeDecksState,
|
|
6
|
+
type DecksState,
|
|
7
|
+
type ResearchAxis,
|
|
8
|
+
} from "../decks-state"
|
|
9
|
+
import { recordWorkspaceAction } from "./actions"
|
|
10
|
+
|
|
11
|
+
export interface AttachResearchFindingsInput {
|
|
12
|
+
findingsFile: string
|
|
13
|
+
researchAxis?: string
|
|
14
|
+
status?: "done" | "read"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AttachResearchFindingsResult {
|
|
18
|
+
attached: boolean
|
|
19
|
+
skipped: boolean
|
|
20
|
+
reason?: string
|
|
21
|
+
slug?: string
|
|
22
|
+
axis?: string
|
|
23
|
+
findingsFile?: string
|
|
24
|
+
status?: ResearchAxis["status"]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function attachResearchFindings(workspaceRoot: string, input: AttachResearchFindingsInput): AttachResearchFindingsResult {
|
|
28
|
+
const state = readDecksState(workspaceRoot)
|
|
29
|
+
const result = attachResearchFindingsToState(state, workspaceRoot, input)
|
|
30
|
+
writeDecksState(workspaceRoot, state)
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function attachResearchFindingsToState(state: DecksState, workspaceRoot: string, input: AttachResearchFindingsInput): AttachResearchFindingsResult {
|
|
35
|
+
const normalizedFile = normalizeResearchFindingsPath(input.findingsFile)
|
|
36
|
+
if (!normalizedFile) {
|
|
37
|
+
return recordSkipped(state, input, "findingsFile must be a workspace-relative researches/*.md path")
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const absoluteFile = safeWorkspacePath(workspaceRoot, normalizedFile)
|
|
41
|
+
if (!absoluteFile || !existsSync(absoluteFile)) {
|
|
42
|
+
return recordSkipped(state, { ...input, findingsFile: normalizedFile }, "findingsFile does not exist inside the workspace")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const slug = state.activeDeck ?? singleDeckKey(state)
|
|
46
|
+
const deck = slug ? state.decks[slug] : undefined
|
|
47
|
+
if (!slug || !deck) return recordSkipped(state, { ...input, findingsFile: normalizedFile }, "no active deck is available")
|
|
48
|
+
|
|
49
|
+
const matches = matchingAxes(deck.researchPlan ?? [], input.researchAxis, normalizedFile)
|
|
50
|
+
if (matches.length === 0) return recordSkipped(state, { ...input, findingsFile: normalizedFile }, "no matching researchPlan axis found")
|
|
51
|
+
if (matches.length > 1) return recordSkipped(state, { ...input, findingsFile: normalizedFile }, "researchPlan axis match is ambiguous")
|
|
52
|
+
|
|
53
|
+
const index = matches[0]!
|
|
54
|
+
const existing = deck.researchPlan[index]!
|
|
55
|
+
const nextStatus = input.status ?? existing.status
|
|
56
|
+
deck.researchPlan[index] = {
|
|
57
|
+
...existing,
|
|
58
|
+
status: nextStatus,
|
|
59
|
+
findingsFile: normalizedFile,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
recordWorkspaceAction(state, {
|
|
63
|
+
type: "research.findings_attached",
|
|
64
|
+
actor: "revela-decks",
|
|
65
|
+
inputs: { activeDeck: slug, axis: existing.axis, findingsFile: normalizedFile, requestedStatus: input.status },
|
|
66
|
+
outputs: { slug, axis: existing.axis, findingsFile: normalizedFile, status: nextStatus },
|
|
67
|
+
summary: `Attached research findings ${normalizedFile} to axis ${existing.axis}.`,
|
|
68
|
+
nodeIds: [`finding:${normalizedFile}`],
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
attached: true,
|
|
73
|
+
skipped: false,
|
|
74
|
+
slug,
|
|
75
|
+
axis: existing.axis,
|
|
76
|
+
findingsFile: normalizedFile,
|
|
77
|
+
status: nextStatus,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function matchingAxes(researchPlan: ResearchAxis[], researchAxis: string | undefined, findingsFile: string): number[] {
|
|
82
|
+
if (researchAxis?.trim()) {
|
|
83
|
+
const requested = normalizeKey(researchAxis)
|
|
84
|
+
return researchPlan.flatMap((axis, index) => normalizeKey(axis.axis) === requested ? [index] : [])
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const fileKey = normalizeKey(basename(findingsFile, ".md"))
|
|
88
|
+
return researchPlan.flatMap((axis, index) => normalizeKey(axis.axis) === fileKey ? [index] : [])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function recordSkipped(state: DecksState, input: AttachResearchFindingsInput, reason: string): AttachResearchFindingsResult {
|
|
92
|
+
const normalizedFile = normalizeResearchFindingsPath(input.findingsFile) ?? input.findingsFile
|
|
93
|
+
recordWorkspaceAction(state, {
|
|
94
|
+
type: "research.findings_attached",
|
|
95
|
+
actor: "revela-decks",
|
|
96
|
+
inputs: { axis: input.researchAxis, findingsFile: normalizedFile, requestedStatus: input.status },
|
|
97
|
+
outputs: { reason },
|
|
98
|
+
status: "skipped",
|
|
99
|
+
summary: `Skipped research findings attachment: ${reason}.`,
|
|
100
|
+
nodeIds: normalizedFile ? [`finding:${normalizedFile}`] : [],
|
|
101
|
+
})
|
|
102
|
+
return { attached: false, skipped: true, reason }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeResearchFindingsPath(filePath: string | undefined): string | undefined {
|
|
106
|
+
const normalized = normalizePath(filePath ?? "").replace(/^\.\//, "")
|
|
107
|
+
if (!normalized || normalized.startsWith("../") || normalized.startsWith("/")) return undefined
|
|
108
|
+
if (!normalized.startsWith("researches/") || !normalized.endsWith(".md")) return undefined
|
|
109
|
+
return normalized
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function safeWorkspacePath(workspaceRoot: string, relativePath: string): string | undefined {
|
|
113
|
+
const root = resolve(workspaceRoot)
|
|
114
|
+
const target = resolve(root, relativePath)
|
|
115
|
+
if (target !== root && !target.startsWith(root + sep)) return undefined
|
|
116
|
+
return target
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function singleDeckKey(state: DecksState): string | undefined {
|
|
120
|
+
const keys = Object.keys(state.decks)
|
|
121
|
+
return keys.length === 1 ? keys[0] : undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeKey(value: string): string {
|
|
125
|
+
return value.toLowerCase().replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "-").replace(/^-+|-+$/g, "")
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function normalizePath(filePath: string): string {
|
|
129
|
+
return filePath.replace(/\\/g, "/")
|
|
130
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import type { DeckSpec, DecksState, DeckStateReadinessResult } from "../decks-state"
|
|
3
|
+
import { projectWorkspaceGraph } from "./graph"
|
|
4
|
+
import { activeHtmlDeckRenderTarget, ensureActiveHtmlDeckRenderTarget } from "./render-targets"
|
|
5
|
+
import type { ReviewSnapshot } from "./types"
|
|
6
|
+
|
|
7
|
+
export const MAX_REVIEW_SNAPSHOTS = 50
|
|
8
|
+
|
|
9
|
+
export interface ReviewSnapshotInput {
|
|
10
|
+
slug: string
|
|
11
|
+
result: DeckStateReadinessResult
|
|
12
|
+
reviewedAt?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function currentReviewInputHash(state: DecksState, slug?: string): string {
|
|
16
|
+
return stableHash(stableStringify(reviewInputProjection(state, slug)))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function activeReviewTargetId(state: DecksState): string | undefined {
|
|
20
|
+
return activeHtmlDeckRenderTarget(state)?.id ?? ensureActiveHtmlDeckRenderTarget(state)?.id
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createReviewSnapshot(state: DecksState, input: ReviewSnapshotInput): ReviewSnapshot {
|
|
24
|
+
const reviewedAt = input.reviewedAt ?? new Date().toISOString()
|
|
25
|
+
const targetId = activeReviewTargetId(state)
|
|
26
|
+
const inputHash = currentReviewInputHash(state, input.slug)
|
|
27
|
+
return {
|
|
28
|
+
id: reviewSnapshotId(targetId, inputHash, reviewedAt),
|
|
29
|
+
...(targetId ? { targetId } : {}),
|
|
30
|
+
inputHash,
|
|
31
|
+
status: input.result.status ?? (input.result.ready ? "ready" : "blocked"),
|
|
32
|
+
blockers: input.result.blockers,
|
|
33
|
+
warnings: input.result.warnings,
|
|
34
|
+
issues: input.result.issues,
|
|
35
|
+
...(input.result.evidenceCandidates ? { evidenceCandidates: input.result.evidenceCandidates } : {}),
|
|
36
|
+
reviewedAt,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function appendReviewSnapshot(state: DecksState, snapshot: ReviewSnapshot): DecksState {
|
|
41
|
+
const next = (state.reviews ?? []).filter((item) => item.id !== snapshot.id)
|
|
42
|
+
next.push(snapshot)
|
|
43
|
+
state.reviews = next
|
|
44
|
+
.sort((a, b) => a.reviewedAt.localeCompare(b.reviewedAt))
|
|
45
|
+
.slice(-MAX_REVIEW_SNAPSHOTS)
|
|
46
|
+
return state
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function latestReviewSnapshotForTarget(state: DecksState, targetId?: string): ReviewSnapshot | undefined {
|
|
50
|
+
const reviews = state.reviews ?? []
|
|
51
|
+
const candidates = targetId ? reviews.filter((item) => item.targetId === targetId) : reviews
|
|
52
|
+
return candidates.reduce<ReviewSnapshot | undefined>((latest, item) => {
|
|
53
|
+
if (!latest) return item
|
|
54
|
+
return item.reviewedAt.localeCompare(latest.reviewedAt) >= 0 ? item : latest
|
|
55
|
+
}, undefined)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function isReviewSnapshotCurrent(state: DecksState, snapshot: ReviewSnapshot, slug?: string): boolean {
|
|
59
|
+
return snapshot.inputHash === currentReviewInputHash(state, slug)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reviewInputProjection(state: DecksState, slug?: string): unknown {
|
|
63
|
+
const key = slug || state.activeDeck || singleDeckKey(state.decks)
|
|
64
|
+
const deck = key ? state.decks[key] : undefined
|
|
65
|
+
const stableState = cloneForGraphProjection(state, key)
|
|
66
|
+
return {
|
|
67
|
+
version: state.version,
|
|
68
|
+
activeDeck: key,
|
|
69
|
+
workspace: {
|
|
70
|
+
brief: state.workspace.brief,
|
|
71
|
+
sourceMaterials: state.workspace.sourceMaterials,
|
|
72
|
+
openQuestions: state.workspace.openQuestions,
|
|
73
|
+
},
|
|
74
|
+
deck: deck ? stableDeckProjection(deck) : undefined,
|
|
75
|
+
renderTarget: activeHtmlDeckRenderTarget(state) ?? ensureActiveHtmlDeckRenderTarget(state),
|
|
76
|
+
graph: deck ? projectWorkspaceGraph(stableState, { slug: key }) : undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function stableDeckProjection(deck: DeckSpec): unknown {
|
|
81
|
+
return {
|
|
82
|
+
slug: deck.slug,
|
|
83
|
+
goal: deck.goal,
|
|
84
|
+
audience: deck.audience,
|
|
85
|
+
language: deck.language,
|
|
86
|
+
outputPath: deck.outputPath,
|
|
87
|
+
narrativeBrief: deck.narrativeBrief,
|
|
88
|
+
theme: deck.theme,
|
|
89
|
+
requiredInputs: deck.requiredInputs,
|
|
90
|
+
researchPlan: deck.researchPlan,
|
|
91
|
+
slides: deck.slides,
|
|
92
|
+
assets: deck.assets,
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function cloneForGraphProjection(state: DecksState, slug?: string): DecksState {
|
|
97
|
+
const clone = structuredClone(state) as DecksState
|
|
98
|
+
const key = slug || clone.activeDeck || singleDeckKey(clone.decks)
|
|
99
|
+
const deck = key ? clone.decks[key] : undefined
|
|
100
|
+
if (deck) {
|
|
101
|
+
deck.status = "planning"
|
|
102
|
+
deck.writeReadiness = { status: "blocked", blockers: [] }
|
|
103
|
+
}
|
|
104
|
+
clone.actions = []
|
|
105
|
+
clone.reviews = []
|
|
106
|
+
return clone
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function reviewSnapshotId(targetId: string | undefined, inputHash: string, reviewedAt: string): string {
|
|
110
|
+
return `review:${targetId ?? "workspace"}:${inputHash.slice(0, 12)}:${stableHash(reviewedAt).slice(0, 8)}`
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function stableHash(value: string): string {
|
|
114
|
+
return createHash("sha1").update(value).digest("hex")
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stableStringify(value: unknown): string {
|
|
118
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value)
|
|
119
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`
|
|
120
|
+
const object = value as Record<string, unknown>
|
|
121
|
+
return `{${Object.keys(object).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(object[key])}`).join(",")}}`
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function singleDeckKey(decks: Record<string, DeckSpec>): string | undefined {
|
|
125
|
+
const keys = Object.keys(decks)
|
|
126
|
+
return keys.length === 1 ? keys[0] : undefined
|
|
127
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
|
|
3
|
+
export const WORKSPACE_STATE_FILE = "DECKS.json"
|
|
4
|
+
|
|
5
|
+
export type WorkspaceStateVersion = 1 | 2
|
|
6
|
+
|
|
7
|
+
export interface WorkspaceStateRepositoryOptions<TState> {
|
|
8
|
+
fileName?: string
|
|
9
|
+
normalize?: (state: TState) => TState
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface WorkspaceStateV2 {
|
|
13
|
+
version: 2
|
|
14
|
+
workspace: WorkspaceMeta
|
|
15
|
+
graph: WorkspaceGraph
|
|
16
|
+
actions: WorkspaceAction[]
|
|
17
|
+
renderTargets: RenderTarget[]
|
|
18
|
+
reviews: ReviewSnapshot[]
|
|
19
|
+
compatibility?: DecksStateV1Projection
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkspaceMeta {
|
|
23
|
+
brief?: string
|
|
24
|
+
preferences?: {
|
|
25
|
+
user: string[]
|
|
26
|
+
workflow: string[]
|
|
27
|
+
}
|
|
28
|
+
openQuestions?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkspaceGraph {
|
|
32
|
+
nodes: Record<string, GraphNode>
|
|
33
|
+
edges: GraphEdge[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GraphNode {
|
|
37
|
+
id: string
|
|
38
|
+
type: GraphNodeType
|
|
39
|
+
label?: string
|
|
40
|
+
data?: Record<string, unknown>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type GraphNodeType =
|
|
44
|
+
| "source"
|
|
45
|
+
| "extraction"
|
|
46
|
+
| "finding"
|
|
47
|
+
| "claim"
|
|
48
|
+
| "narrativeIntent"
|
|
49
|
+
| "objection"
|
|
50
|
+
| "risk"
|
|
51
|
+
| "slide"
|
|
52
|
+
| "artifact"
|
|
53
|
+
|
|
54
|
+
export interface GraphEdge {
|
|
55
|
+
id: string
|
|
56
|
+
type: GraphEdgeType
|
|
57
|
+
from: string
|
|
58
|
+
to: string
|
|
59
|
+
data?: Record<string, unknown>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type GraphEdgeType =
|
|
63
|
+
| "contains"
|
|
64
|
+
| "extracted_as"
|
|
65
|
+
| "produced"
|
|
66
|
+
| "supports"
|
|
67
|
+
| "appears_in"
|
|
68
|
+
| "challenges"
|
|
69
|
+
| "constrained_by"
|
|
70
|
+
| "renders_from"
|
|
71
|
+
| "derived_from"
|
|
72
|
+
|
|
73
|
+
export interface WorkspaceAction {
|
|
74
|
+
id: string
|
|
75
|
+
type: WorkspaceActionType
|
|
76
|
+
timestamp: string
|
|
77
|
+
actor?: string
|
|
78
|
+
inputs?: Record<string, unknown>
|
|
79
|
+
outputs?: Record<string, unknown>
|
|
80
|
+
status: "success" | "failed" | "skipped"
|
|
81
|
+
summary?: string
|
|
82
|
+
nodeIds?: string[]
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type WorkspaceActionType =
|
|
86
|
+
| "workspace.scanned"
|
|
87
|
+
| "source.discovered"
|
|
88
|
+
| "source.extracted"
|
|
89
|
+
| "research.findings_saved"
|
|
90
|
+
| "research.findings_attached"
|
|
91
|
+
| "evidence.candidate_generated"
|
|
92
|
+
| "evidence.binding_applied"
|
|
93
|
+
| "review.performed"
|
|
94
|
+
| "artifact.rendered"
|
|
95
|
+
|
|
96
|
+
export interface RenderTarget {
|
|
97
|
+
id: string
|
|
98
|
+
type: "html_deck" | "pdf" | "pptx" | "brief" | "appendix" | "qa_view" | "interactive_page"
|
|
99
|
+
outputPath?: string
|
|
100
|
+
sourceNodeIds: string[]
|
|
101
|
+
artifactVersion?: string
|
|
102
|
+
contractStatus?: "unknown" | "valid" | "invalid" | "stale"
|
|
103
|
+
data?: Record<string, unknown>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ReviewSnapshot {
|
|
107
|
+
id: string
|
|
108
|
+
targetId?: string
|
|
109
|
+
inputHash: string
|
|
110
|
+
status: "blocked" | "ready" | "written"
|
|
111
|
+
blockers: string[]
|
|
112
|
+
warnings: string[]
|
|
113
|
+
issues: unknown[]
|
|
114
|
+
evidenceCandidates?: unknown[]
|
|
115
|
+
reviewedAt: string
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type DecksStateV1Projection = DecksState
|
|
119
|
+
export type WorkspaceState = DecksState | WorkspaceStateV2
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -46,6 +46,9 @@ import {
|
|
|
46
46
|
import { handlePdf } from "./lib/commands/pdf"
|
|
47
47
|
import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from "./lib/commands/pptx"
|
|
48
48
|
import { handleEdit } from "./lib/commands/edit"
|
|
49
|
+
import { handleInspect } from "./lib/commands/inspect"
|
|
50
|
+
import { handleRefine } from "./lib/commands/refine"
|
|
51
|
+
import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "./lib/deck-html/contract"
|
|
49
52
|
import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
|
|
50
53
|
import { hasLiveEditorSessionForFile } from "./lib/edit/server"
|
|
51
54
|
import { handleDesignsPreview } from "./lib/commands/designs-preview"
|
|
@@ -80,6 +83,8 @@ import mediaBatchSaveTool from "./tools/media-batch-save"
|
|
|
80
83
|
import mediaSaveTool from "./tools/media-save"
|
|
81
84
|
import researchImagesListTool from "./tools/research-images-list"
|
|
82
85
|
import researchSaveTool from "./tools/research-save"
|
|
86
|
+
import inspectionContextTool from "./tools/inspection-context"
|
|
87
|
+
import inspectionResultTool from "./tools/inspection-result"
|
|
83
88
|
import workspaceScanTool from "./tools/workspace-scan"
|
|
84
89
|
import extractDocumentMaterialsTool from "./tools/extract-document-materials"
|
|
85
90
|
import qaTool from "./tools/qa"
|
|
@@ -173,6 +178,27 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
async function appendDeckHtmlContractReport(filePath: string, output: any): Promise<void> {
|
|
182
|
+
if (!isDeckHtmlPath(filePath)) return
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const report = validateDeckHtmlContract(workspaceRoot, filePath)
|
|
186
|
+
if (report.status === "valid" || report.status === "skipped") return
|
|
187
|
+
|
|
188
|
+
appendToolResult(
|
|
189
|
+
output,
|
|
190
|
+
"---\n\n**[revela deck HTML contract]** Slide identity check failed:\n\n" +
|
|
191
|
+
formatDeckHtmlContractReport(report) +
|
|
192
|
+
"\n\nFix every `<section class=\"slide\">` to use the matching 1-based `data-slide-index` from DECKS.json before inspection or export."
|
|
193
|
+
)
|
|
194
|
+
} catch (e) {
|
|
195
|
+
childLog("deck-contract").warn("deck HTML contract report failed", {
|
|
196
|
+
filePath,
|
|
197
|
+
error: e instanceof Error ? e.message : String(e),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
176
202
|
function extractSessionID(input: any): string {
|
|
177
203
|
return input?.sessionID ?? input?.session?.id ?? input?.context?.sessionID ?? ""
|
|
178
204
|
}
|
|
@@ -332,6 +358,14 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
332
358
|
} as any)
|
|
333
359
|
return
|
|
334
360
|
}
|
|
361
|
+
if (sub === "refine") {
|
|
362
|
+
if (param) {
|
|
363
|
+
await send("`/revela refine` does not accept a target. It opens the only HTML deck in `decks/`.")
|
|
364
|
+
throw new Error("__REVELA_REFINE_USAGE_HANDLED__")
|
|
365
|
+
}
|
|
366
|
+
await handleRefine({ client, sessionID, workspaceRoot }, send)
|
|
367
|
+
throw new Error("__REVELA_REFINE_HANDLED__")
|
|
368
|
+
}
|
|
335
369
|
if (sub === "edit") {
|
|
336
370
|
if (param) {
|
|
337
371
|
await send("`/revela edit` no longer accepts a target. It opens the only HTML deck in `decks/`.")
|
|
@@ -340,6 +374,14 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
340
374
|
await handleEdit({ client, sessionID, workspaceRoot }, send)
|
|
341
375
|
throw new Error("__REVELA_EDIT_HANDLED__")
|
|
342
376
|
}
|
|
377
|
+
if (sub === "inspect") {
|
|
378
|
+
if (param) {
|
|
379
|
+
await send("`/revela inspect` does not accept a target. It opens the only HTML deck in `decks/`.")
|
|
380
|
+
throw new Error("__REVELA_INSPECT_USAGE_HANDLED__")
|
|
381
|
+
}
|
|
382
|
+
await handleInspect({ client, sessionID, workspaceRoot }, send)
|
|
383
|
+
throw new Error("__REVELA_INSPECT_HANDLED__")
|
|
384
|
+
}
|
|
343
385
|
if (sub === "designs" && !param) {
|
|
344
386
|
await handleDesignsList(send)
|
|
345
387
|
throw new Error("__REVELA_DESIGNS_LIST_HANDLED__")
|
|
@@ -403,7 +445,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
403
445
|
throw new Error("__REVELA_DOMAINS_RM_HANDLED__")
|
|
404
446
|
}
|
|
405
447
|
if (sub === "pdf") {
|
|
406
|
-
await handlePdf(param, send)
|
|
448
|
+
await handlePdf(param, send, workspaceRoot)
|
|
407
449
|
throw new Error("__REVELA_PDF_HANDLED__")
|
|
408
450
|
}
|
|
409
451
|
if (sub === "pptx") {
|
|
@@ -438,6 +480,8 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
438
480
|
"revela-media-save": mediaSaveTool,
|
|
439
481
|
"revela-research-images-list": researchImagesListTool,
|
|
440
482
|
"revela-research-save": researchSaveTool,
|
|
483
|
+
"revela-inspection-context": inspectionContextTool,
|
|
484
|
+
"revela-inspection-result": inspectionResultTool,
|
|
441
485
|
"revela-workspace-scan": workspaceScanTool,
|
|
442
486
|
"revela-extract-document-materials": extractDocumentMaterialsTool,
|
|
443
487
|
"revela-qa": qaTool,
|
|
@@ -720,6 +764,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
720
764
|
return
|
|
721
765
|
}
|
|
722
766
|
await appendComplianceReport(filePath, output)
|
|
767
|
+
await appendDeckHtmlContractReport(filePath, output)
|
|
723
768
|
ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
|
|
724
769
|
return
|
|
725
770
|
}
|
|
@@ -741,6 +786,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
741
786
|
const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
|
|
742
787
|
for (const target of targets) {
|
|
743
788
|
await appendComplianceReport(target, output)
|
|
789
|
+
await appendDeckHtmlContractReport(target, output)
|
|
744
790
|
ensureEditorOpenAfterDeckChange(target, extractSessionID(input))
|
|
745
791
|
}
|
|
746
792
|
return
|
|
@@ -749,6 +795,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
|
|
|
749
795
|
if (input.tool === "edit") {
|
|
750
796
|
const filePath = extractEditFilePath(input.args)
|
|
751
797
|
await appendComplianceReport(filePath, output)
|
|
798
|
+
await appendDeckHtmlContractReport(filePath, output)
|
|
752
799
|
ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
|
|
753
800
|
return
|
|
754
801
|
}
|
package/skill/SKILL.md
CHANGED
|
@@ -261,12 +261,17 @@ A 6-slide deck might be: Cover → Background → Content × 3 → Closing.
|
|
|
261
261
|
An 8-slide deck might be: Cover → TOC → Background → Content × 3 → Summary → Closing.
|
|
262
262
|
Never skip Cover, Background, or Closing regardless of deck length.
|
|
263
263
|
|
|
264
|
-
**Every `<section class="slide">` must include
|
|
265
|
-
`slide-qa="true"` for content-heavy layouts
|
|
266
|
-
QA column of the active design). Set
|
|
267
|
-
layouts (cover, TOC, closing, quote,
|
|
264
|
+
**Every `<section class="slide">` must include `slide-qa` and
|
|
265
|
+
`data-slide-index` attributes.** Set `slide-qa="true"` for content-heavy layouts
|
|
266
|
+
(those marked ✓ in the Layout Index QA column of the active design). Set
|
|
267
|
+
`slide-qa="false"` for structural or sparse layouts (cover, TOC, closing, quote,
|
|
268
|
+
summary, etc.). When unsure, use `"false"`.
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
`data-slide-index` is the canonical 1-based slide identity. It must match the
|
|
271
|
+
corresponding `DECKS.json` `slides[].index` value. Do not use 0-based
|
|
272
|
+
`data-index` as slide identity.
|
|
273
|
+
|
|
274
|
+
Example: `<section class="slide" slide-qa="true" data-slide-index="1">`
|
|
270
275
|
|
|
271
276
|
The export QA path treats this as deck metadata. It is consumed when PDF/PPTX
|
|
272
277
|
export runs preflight checks.
|
package/tools/decks.ts
CHANGED
|
@@ -17,6 +17,10 @@ import {
|
|
|
17
17
|
type SlideSpec,
|
|
18
18
|
} from "../lib/decks-state"
|
|
19
19
|
import { upsertSourceMaterial } from "../lib/source-materials"
|
|
20
|
+
import { recordWorkspaceAction } from "../lib/workspace-state/actions"
|
|
21
|
+
import { applyEvidenceBindings } from "../lib/workspace-state/evidence-status"
|
|
22
|
+
import { attachResearchFindings } from "../lib/workspace-state/research-attachments"
|
|
23
|
+
import { activeReviewTargetId, latestReviewSnapshotForTarget } from "../lib/workspace-state/review-snapshots"
|
|
20
24
|
|
|
21
25
|
export default tool({
|
|
22
26
|
description:
|
|
@@ -25,7 +29,7 @@ export default tool({
|
|
|
25
29
|
"It stores active deck specs, per-slide content/layout/components, and computes write readiness.",
|
|
26
30
|
args: {
|
|
27
31
|
action: tool.schema
|
|
28
|
-
.enum(["read", "init", "upsertDeck", "upsertSlides", "review", "remember"])
|
|
32
|
+
.enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
|
|
29
33
|
.describe("Action to perform on DECKS.json."),
|
|
30
34
|
summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
|
|
31
35
|
goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
|
|
@@ -116,6 +120,10 @@ export default tool({
|
|
|
116
120
|
status: tool.schema.enum(["planned", "ready", "written", "qa_passed", "qa_failed"]).optional().describe("Slide production status."),
|
|
117
121
|
notes: tool.schema.string().optional().describe("Implementation notes for this slide."),
|
|
118
122
|
})).optional().describe("For upsertSlides: complete or partial slide specs."),
|
|
123
|
+
candidateIds: tool.schema.array(tool.schema.string()).optional().describe("For applyEvidenceCandidates: candidate IDs returned by revela-decks review to explicitly bind proposed evidenceDraft records into slide evidence."),
|
|
124
|
+
findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
|
|
125
|
+
researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
|
|
126
|
+
researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
|
|
119
127
|
},
|
|
120
128
|
async execute(args, context) {
|
|
121
129
|
try {
|
|
@@ -124,8 +132,20 @@ export default tool({
|
|
|
124
132
|
const defaultSlug = workspaceDeckSlug(workspaceRoot)
|
|
125
133
|
|
|
126
134
|
if (args.action === "init") {
|
|
135
|
+
const discovered: SourceMaterial[] = []
|
|
127
136
|
for (const material of (args.sourceMaterials ?? []) as SourceMaterial[]) {
|
|
128
137
|
upsertSourceMaterial(state, material, material.status ?? "discovered")
|
|
138
|
+
discovered.push(material)
|
|
139
|
+
}
|
|
140
|
+
if (discovered.length > 0) {
|
|
141
|
+
recordWorkspaceAction(state, {
|
|
142
|
+
type: "source.discovered",
|
|
143
|
+
actor: "revela-decks",
|
|
144
|
+
inputs: { count: discovered.length },
|
|
145
|
+
outputs: { paths: discovered.map((material) => material.path), statuses: discovered.map((material) => material.status ?? "discovered") },
|
|
146
|
+
summary: `Registered ${discovered.length} discovered source material${discovered.length === 1 ? "" : "s"}.`,
|
|
147
|
+
nodeIds: discovered.map((material) => `source:${material.path}`),
|
|
148
|
+
})
|
|
129
149
|
}
|
|
130
150
|
writeDecksState(workspaceRoot, state)
|
|
131
151
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, state }, null, 2)
|
|
@@ -175,11 +195,50 @@ export default tool({
|
|
|
175
195
|
}
|
|
176
196
|
|
|
177
197
|
if (args.action === "review") {
|
|
178
|
-
const reviewed = reviewDeckState(state)
|
|
198
|
+
const reviewed = reviewDeckState(state, undefined, { workspaceRoot })
|
|
199
|
+
const targetId = activeReviewTargetId(reviewed.state)
|
|
200
|
+
const snapshot = latestReviewSnapshotForTarget(reviewed.state, targetId)
|
|
201
|
+
recordWorkspaceAction(reviewed.state, {
|
|
202
|
+
type: "review.performed",
|
|
203
|
+
actor: "revela-decks",
|
|
204
|
+
inputs: { activeDeck: state.activeDeck },
|
|
205
|
+
outputs: {
|
|
206
|
+
slug: reviewed.result.slug,
|
|
207
|
+
status: reviewed.result.status,
|
|
208
|
+
ready: reviewed.result.ready,
|
|
209
|
+
blockerCount: reviewed.result.blockers.length,
|
|
210
|
+
warningCount: reviewed.result.warnings.length,
|
|
211
|
+
issueCount: reviewed.result.issues.length,
|
|
212
|
+
evidenceCandidateCount: reviewed.result.evidenceCandidates?.length ?? 0,
|
|
213
|
+
snapshotId: snapshot?.id,
|
|
214
|
+
inputHash: snapshot?.inputHash,
|
|
215
|
+
targetId: snapshot?.targetId,
|
|
216
|
+
},
|
|
217
|
+
status: "success",
|
|
218
|
+
summary: `Reviewed deck readiness: ${reviewed.result.ready ? "ready" : "blocked"}.`,
|
|
219
|
+
nodeIds: [`artifact:${reviewed.state.decks[reviewed.result.slug]?.outputPath ?? reviewed.result.slug}`, ...(snapshot ? [snapshot.id] : [])],
|
|
220
|
+
})
|
|
179
221
|
writeDecksState(workspaceRoot, reviewed.state)
|
|
180
222
|
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
|
|
181
223
|
}
|
|
182
224
|
|
|
225
|
+
if (args.action === "applyEvidenceCandidates") {
|
|
226
|
+
const candidateIds = args.candidateIds ?? []
|
|
227
|
+
if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
|
|
228
|
+
const result = applyEvidenceBindings(workspaceRoot, candidateIds)
|
|
229
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (args.action === "attachResearchFindings") {
|
|
233
|
+
if (!args.findingsFile?.trim()) return JSON.stringify({ ok: false, error: "findingsFile is required for attachResearchFindings" })
|
|
234
|
+
const result = attachResearchFindings(workspaceRoot, {
|
|
235
|
+
findingsFile: args.findingsFile,
|
|
236
|
+
researchAxis: args.researchAxis,
|
|
237
|
+
status: args.researchStatus,
|
|
238
|
+
})
|
|
239
|
+
return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result }, null, 2)
|
|
240
|
+
}
|
|
241
|
+
|
|
183
242
|
if (args.action === "remember") {
|
|
184
243
|
const memory = args.memory?.trim()
|
|
185
244
|
if (!memory) return JSON.stringify({ ok: false, error: "memory is required for remember" })
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { compileInspectionContext } from "../lib/inspection-context/compile"
|
|
3
|
+
import { normalizeWorkspaceDeckState, readOrCreateDecksState } from "../lib/decks-state"
|
|
4
|
+
|
|
5
|
+
export default tool({
|
|
6
|
+
description:
|
|
7
|
+
"Compile Revela's current DECKS.json into structured inspection context for debugging and future Evidence Inspector flows. " +
|
|
8
|
+
"This is read-only: it does not write artifacts, mutate DECKS.json, or generate user-facing files.",
|
|
9
|
+
args: {
|
|
10
|
+
slug: tool.schema.string().optional().describe("Optional deck slug to compile. Defaults to the active workspace deck."),
|
|
11
|
+
},
|
|
12
|
+
async execute(args, context) {
|
|
13
|
+
try {
|
|
14
|
+
const workspaceRoot = context.directory ?? process.cwd()
|
|
15
|
+
const state = normalizeWorkspaceDeckState(readOrCreateDecksState(workspaceRoot), workspaceRoot)
|
|
16
|
+
const inspectionContext = compileInspectionContext(state, args.slug)
|
|
17
|
+
return JSON.stringify({ ok: true, inspectionContext }, null, 2)
|
|
18
|
+
} catch (e: any) {
|
|
19
|
+
return JSON.stringify({ ok: false, error: e.message || String(e) })
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|