@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,71 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import type { DecksState } from "../decks-state"
|
|
3
|
+
import type { WorkspaceAction, WorkspaceActionType } from "./types"
|
|
4
|
+
|
|
5
|
+
export const MAX_WORKSPACE_ACTIONS = 500
|
|
6
|
+
|
|
7
|
+
export interface WorkspaceActionInput {
|
|
8
|
+
type: WorkspaceActionType
|
|
9
|
+
actor?: string
|
|
10
|
+
inputs?: Record<string, unknown>
|
|
11
|
+
outputs?: Record<string, unknown>
|
|
12
|
+
status?: WorkspaceAction["status"]
|
|
13
|
+
summary?: string
|
|
14
|
+
nodeIds?: string[]
|
|
15
|
+
timestamp?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function recordWorkspaceAction(state: DecksState, input: WorkspaceActionInput): DecksState {
|
|
19
|
+
const actions = state.actions ?? []
|
|
20
|
+
const timestamp = input.timestamp ?? new Date().toISOString()
|
|
21
|
+
const action: WorkspaceAction = {
|
|
22
|
+
id: workspaceActionId(input.type, timestamp, actions.length, input),
|
|
23
|
+
type: input.type,
|
|
24
|
+
timestamp,
|
|
25
|
+
status: input.status ?? "success",
|
|
26
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
27
|
+
...(input.inputs ? { inputs: compactActionPayload(input.inputs) } : {}),
|
|
28
|
+
...(input.outputs ? { outputs: compactActionPayload(input.outputs) } : {}),
|
|
29
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
30
|
+
...(input.nodeIds && input.nodeIds.length > 0 ? { nodeIds: [...new Set(input.nodeIds)].sort() } : {}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
state.actions = [...actions, action].slice(-MAX_WORKSPACE_ACTIONS)
|
|
34
|
+
return state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactActionPayload(input: Record<string, unknown>): Record<string, unknown> {
|
|
38
|
+
const output: Record<string, unknown> = {}
|
|
39
|
+
for (const [key, value] of Object.entries(input)) {
|
|
40
|
+
const compacted = compactActionValue(value)
|
|
41
|
+
if (compacted !== undefined) output[key] = compacted
|
|
42
|
+
}
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workspaceActionId(type: WorkspaceActionType, timestamp: string, sequence: number, input: Omit<WorkspaceActionInput, "timestamp">): string {
|
|
47
|
+
return `action:${timestamp}:${type}:${stableHash(JSON.stringify({ sequence, input: compactActionPayload(input as Record<string, unknown>) }))}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compactActionValue(value: unknown): unknown {
|
|
51
|
+
if (value === undefined || value === null) return undefined
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
if (!trimmed) return undefined
|
|
55
|
+
return trimmed.length > 500 ? `${trimmed.slice(0, 500).trimEnd()}... [truncated]` : trimmed
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === "number" || typeof value === "boolean") return value
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const items = value.map(compactActionValue).filter((item) => item !== undefined)
|
|
60
|
+
return items.length > 0 ? items.slice(0, 50) : undefined
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "object") {
|
|
63
|
+
const compacted = compactActionPayload(value as Record<string, unknown>)
|
|
64
|
+
return Object.keys(compacted).length > 0 ? compacted : undefined
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stableHash(value: string): string {
|
|
70
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 10)
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
import type { DecksStateV1Projection, WorkspaceState } from "./types"
|
|
3
|
+
|
|
4
|
+
export function isDecksStateV1(state: WorkspaceState): state is DecksState {
|
|
5
|
+
return state.version === 1
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function asDecksStateV1Projection(state: DecksState): DecksStateV1Projection {
|
|
9
|
+
return state
|
|
10
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyEvidenceCandidates,
|
|
3
|
+
hasDecksState,
|
|
4
|
+
readDecksState,
|
|
5
|
+
reviewDeckState,
|
|
6
|
+
writeDecksState,
|
|
7
|
+
type AppliedEvidenceCandidate,
|
|
8
|
+
type DecksState,
|
|
9
|
+
type EvidenceBindingCandidate,
|
|
10
|
+
type EvidenceCandidateSearchDiagnostic,
|
|
11
|
+
type EvidenceRef,
|
|
12
|
+
type ReadinessIssue,
|
|
13
|
+
type SkippedEvidenceCandidate,
|
|
14
|
+
} from "../decks-state"
|
|
15
|
+
import { compileInspectionContext, type InspectionEvidenceTrace, type InspectionGap } from "../inspection-context/compile"
|
|
16
|
+
import { matchInspectionElement, type InspectionElementMatch, type InspectionElementSnapshot, type InspectionMatchConfidence } from "../inspection-context/match"
|
|
17
|
+
import { recordWorkspaceAction } from "./actions"
|
|
18
|
+
import type { WorkspaceAction } from "./types"
|
|
19
|
+
|
|
20
|
+
export interface EvidenceStatusForSelection {
|
|
21
|
+
version: 1
|
|
22
|
+
selection: EvidenceStatusSelection
|
|
23
|
+
match: EvidenceStatusMatch
|
|
24
|
+
boundEvidence: EvidenceStatusEvidence[]
|
|
25
|
+
gaps: EvidenceStatusGap[]
|
|
26
|
+
candidateEvidence: EvidenceStatusCandidate[]
|
|
27
|
+
searchDiagnostics: EvidenceCandidateSearchDiagnostic[]
|
|
28
|
+
actionTrace: EvidenceStatusActionTrace[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface EvidenceStatusSelection {
|
|
32
|
+
slideIndex?: number
|
|
33
|
+
selectedText?: string
|
|
34
|
+
scope?: InspectionElementSnapshot["scope"]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface EvidenceStatusMatch {
|
|
38
|
+
confidence: InspectionMatchConfidence
|
|
39
|
+
reason: string
|
|
40
|
+
slideIndex?: number
|
|
41
|
+
slideTitle?: string
|
|
42
|
+
claimId?: string
|
|
43
|
+
claimText?: string
|
|
44
|
+
claimEvidenceSensitive?: boolean
|
|
45
|
+
claimEvidenceSupport?: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface EvidenceStatusEvidence extends EvidenceRef {
|
|
49
|
+
slideIndex: number
|
|
50
|
+
slideTitle: string
|
|
51
|
+
hasDetail: boolean
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface EvidenceStatusGap {
|
|
55
|
+
type: InspectionGap["type"]
|
|
56
|
+
slideIndex: number
|
|
57
|
+
slideTitle: string
|
|
58
|
+
claimText: string
|
|
59
|
+
message: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface EvidenceStatusCandidate extends EvidenceBindingCandidate {
|
|
63
|
+
relevant: boolean
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface EvidenceStatusActionTrace {
|
|
67
|
+
id: string
|
|
68
|
+
type: WorkspaceAction["type"]
|
|
69
|
+
timestamp: string
|
|
70
|
+
actor?: string
|
|
71
|
+
status: WorkspaceAction["status"]
|
|
72
|
+
summary?: string
|
|
73
|
+
inputs?: Record<string, unknown>
|
|
74
|
+
outputs?: Record<string, unknown>
|
|
75
|
+
nodeIds?: string[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ApplyEvidenceBindingsResult {
|
|
79
|
+
applied: AppliedEvidenceCandidate[]
|
|
80
|
+
skipped: SkippedEvidenceCandidate[]
|
|
81
|
+
nextReviewNeeded: boolean
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getEvidenceStatusForSelection(workspaceRoot: string, snapshot: InspectionElementSnapshot, options: { slug?: string } = {}): EvidenceStatusForSelection {
|
|
85
|
+
if (!hasDecksState(workspaceRoot)) throw new Error("DECKS.json is required before checking evidence status. Run /revela init first.")
|
|
86
|
+
const state = readDecksState(workspaceRoot)
|
|
87
|
+
return getEvidenceStatusInState(state, snapshot, { ...options, workspaceRoot })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function getEvidenceStatusInState(
|
|
91
|
+
state: DecksState,
|
|
92
|
+
snapshot: InspectionElementSnapshot,
|
|
93
|
+
options: { workspaceRoot?: string; slug?: string } = {},
|
|
94
|
+
): EvidenceStatusForSelection {
|
|
95
|
+
const normalizedSnapshot = normalizeEvidenceSnapshot(snapshot)
|
|
96
|
+
const context = compileInspectionContext(state, options.slug)
|
|
97
|
+
const match = matchInspectionElement(context, normalizedSnapshot)
|
|
98
|
+
const reviewed = reviewDeckState(state, context.slug, { workspaceRoot: options.workspaceRoot })
|
|
99
|
+
const candidates = relevantCandidates(reviewed.result.evidenceCandidates ?? [], match)
|
|
100
|
+
const diagnostics = relevantSearchDiagnostics(reviewed.result.issues, match)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
version: 1,
|
|
104
|
+
selection: {
|
|
105
|
+
slideIndex: normalizedSnapshot.slideIndex,
|
|
106
|
+
selectedText: normalizedSnapshot.selectedText || normalizedSnapshot.text,
|
|
107
|
+
scope: normalizedSnapshot.scope,
|
|
108
|
+
},
|
|
109
|
+
match: projectMatch(match),
|
|
110
|
+
boundEvidence: match.evidence.map(projectEvidence),
|
|
111
|
+
gaps: evidenceStatusGaps(match, reviewed.result.issues),
|
|
112
|
+
candidateEvidence: candidates,
|
|
113
|
+
searchDiagnostics: diagnostics,
|
|
114
|
+
actionTrace: actionTraceForMatch(state.actions ?? [], match),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function applyEvidenceBindings(workspaceRoot: string, candidateIds: string[]): ApplyEvidenceBindingsResult {
|
|
119
|
+
if (!hasDecksState(workspaceRoot)) throw new Error("DECKS.json is required before applying evidence bindings. Run /revela init first.")
|
|
120
|
+
const ids = [...new Set(candidateIds.map((id) => id.trim()).filter(Boolean))]
|
|
121
|
+
if (ids.length === 0) throw new Error("candidateIds are required for evidence binding application.")
|
|
122
|
+
|
|
123
|
+
const state = readDecksState(workspaceRoot)
|
|
124
|
+
const applied = applyEvidenceCandidates(state, ids, { workspaceRoot })
|
|
125
|
+
recordEvidenceBindingAction(applied.state, ids, applied.result)
|
|
126
|
+
writeDecksState(workspaceRoot, applied.state)
|
|
127
|
+
return applied.result
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function recordEvidenceBindingAction(state: DecksState, candidateIds: string[], result: ApplyEvidenceBindingsResult): DecksState {
|
|
131
|
+
return recordWorkspaceAction(state, {
|
|
132
|
+
type: "evidence.binding_applied",
|
|
133
|
+
actor: "revela-decks",
|
|
134
|
+
inputs: { candidateIds },
|
|
135
|
+
outputs: {
|
|
136
|
+
applied: result.applied.map((item) => ({ candidateId: item.candidateId, slideIndex: item.slideIndex, evidence: item.evidence })),
|
|
137
|
+
skipped: result.skipped,
|
|
138
|
+
nextReviewNeeded: result.nextReviewNeeded,
|
|
139
|
+
},
|
|
140
|
+
status: result.applied.length > 0 ? "success" : "skipped",
|
|
141
|
+
summary: `Applied ${result.applied.length} evidence candidate${result.applied.length === 1 ? "" : "s"}.`,
|
|
142
|
+
nodeIds: result.applied.map((item) => `slide:${item.slideIndex}`),
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function normalizeEvidenceSnapshot(snapshot: InspectionElementSnapshot): InspectionElementSnapshot {
|
|
147
|
+
const selectedText = snapshot.selectedText || snapshot.text
|
|
148
|
+
return {
|
|
149
|
+
...snapshot,
|
|
150
|
+
text: snapshot.text || selectedText,
|
|
151
|
+
selectedText,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function projectMatch(match: InspectionElementMatch): EvidenceStatusMatch {
|
|
156
|
+
return {
|
|
157
|
+
confidence: match.confidence,
|
|
158
|
+
reason: match.reason,
|
|
159
|
+
slideIndex: match.slide?.index,
|
|
160
|
+
slideTitle: match.slide?.title,
|
|
161
|
+
claimId: match.claim?.id,
|
|
162
|
+
claimText: match.claim?.text,
|
|
163
|
+
claimEvidenceSensitive: match.claim?.evidenceSensitive,
|
|
164
|
+
claimEvidenceSupport: match.claim?.evidenceSupport,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function projectEvidence(trace: InspectionEvidenceTrace): EvidenceStatusEvidence {
|
|
169
|
+
return {
|
|
170
|
+
...trace,
|
|
171
|
+
slideIndex: trace.slideIndex,
|
|
172
|
+
slideTitle: trace.slideTitle,
|
|
173
|
+
hasDetail: trace.hasDetail,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function projectGap(gap: InspectionGap): EvidenceStatusGap {
|
|
178
|
+
return {
|
|
179
|
+
type: gap.type,
|
|
180
|
+
slideIndex: gap.slideIndex,
|
|
181
|
+
slideTitle: gap.slideTitle,
|
|
182
|
+
claimText: gap.claimText,
|
|
183
|
+
message: gap.message,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function evidenceStatusGaps(match: InspectionElementMatch, issues: ReadinessIssue[]): EvidenceStatusGap[] {
|
|
188
|
+
const gaps = match.gaps.map(projectGap)
|
|
189
|
+
const slideIndex = match.slide?.index
|
|
190
|
+
for (const issue of issues) {
|
|
191
|
+
if (issue.slideIndex !== slideIndex) continue
|
|
192
|
+
if (typeof issue.slideIndex !== "number") continue
|
|
193
|
+
if (issue.type !== "missing_evidence" && issue.type !== "weak_evidence") continue
|
|
194
|
+
if (gaps.some((gap) => gap.type === issue.type && gap.claimText === issue.claimText)) continue
|
|
195
|
+
gaps.push({
|
|
196
|
+
type: issue.type,
|
|
197
|
+
slideIndex: issue.slideIndex,
|
|
198
|
+
slideTitle: issue.slideTitle ?? match.slide?.title ?? "",
|
|
199
|
+
claimText: issue.claimText ?? match.claim?.text ?? "",
|
|
200
|
+
message: issue.message,
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
return gaps
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function relevantCandidates(candidates: EvidenceBindingCandidate[], match: InspectionElementMatch): EvidenceStatusCandidate[] {
|
|
207
|
+
const slideIndex = match.slide?.index
|
|
208
|
+
return candidates
|
|
209
|
+
.filter((candidate) => candidate.slideIndex === slideIndex)
|
|
210
|
+
.map((candidate) => ({
|
|
211
|
+
...candidate,
|
|
212
|
+
relevant: candidateRelevantToMatch(candidate, match),
|
|
213
|
+
}))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function candidateRelevantToMatch(candidate: EvidenceBindingCandidate, match: InspectionElementMatch): boolean {
|
|
217
|
+
const claimText = normalizeText(match.claim?.text)
|
|
218
|
+
if (!claimText || !candidate.claimText) return true
|
|
219
|
+
const candidateClaim = normalizeText(candidate.claimText)
|
|
220
|
+
if (candidateClaim === claimText || candidateClaim.includes(claimText) || claimText.includes(candidateClaim)) return true
|
|
221
|
+
const quote = normalizeText(candidate.quote)
|
|
222
|
+
if (quote.includes(claimText)) return true
|
|
223
|
+
return (candidate.supportScope ?? []).some((scope) => {
|
|
224
|
+
const normalizedScope = normalizeText(scope)
|
|
225
|
+
return normalizedScope === claimText || normalizedScope.includes(claimText) || claimText.includes(normalizedScope)
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function relevantSearchDiagnostics(issues: ReadinessIssue[], match: InspectionElementMatch): EvidenceCandidateSearchDiagnostic[] {
|
|
230
|
+
const slideIndex = match.slide?.index
|
|
231
|
+
return issues
|
|
232
|
+
.filter((issue) => issue.slideIndex === slideIndex && issue.evidenceCandidateSearch)
|
|
233
|
+
.map((issue) => issue.evidenceCandidateSearch!)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function actionTraceForMatch(actions: WorkspaceAction[], match: InspectionElementMatch): EvidenceStatusActionTrace[] {
|
|
237
|
+
const slideNodeId = match.slide ? `slide:${match.slide.index}` : undefined
|
|
238
|
+
const evidenceKeys = new Set(match.evidence.flatMap((item) => [item.source, item.sourcePath, item.findingsFile].filter((value): value is string => Boolean(value))))
|
|
239
|
+
return actions
|
|
240
|
+
.filter((action) => actionRelevantToMatch(action, slideNodeId, evidenceKeys))
|
|
241
|
+
.slice(-12)
|
|
242
|
+
.map((action) => ({
|
|
243
|
+
id: action.id,
|
|
244
|
+
type: action.type,
|
|
245
|
+
timestamp: action.timestamp,
|
|
246
|
+
actor: action.actor,
|
|
247
|
+
status: action.status,
|
|
248
|
+
summary: action.summary,
|
|
249
|
+
inputs: action.inputs,
|
|
250
|
+
outputs: action.outputs,
|
|
251
|
+
nodeIds: action.nodeIds,
|
|
252
|
+
}))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function actionRelevantToMatch(action: WorkspaceAction, slideNodeId: string | undefined, evidenceKeys: Set<string>): boolean {
|
|
256
|
+
if (slideNodeId && action.nodeIds?.includes(slideNodeId)) return true
|
|
257
|
+
if (action.type === "evidence.binding_applied" || action.type === "evidence.candidate_generated") return true
|
|
258
|
+
const text = JSON.stringify({ inputs: action.inputs, outputs: action.outputs, nodeIds: action.nodeIds })
|
|
259
|
+
for (const key of evidenceKeys) {
|
|
260
|
+
if (key && text.includes(key)) return true
|
|
261
|
+
}
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function normalizeText(value: string | undefined): string {
|
|
266
|
+
return String(value ?? "").replace(/\s+/g, " ").trim().toLowerCase()
|
|
267
|
+
}
|