@cyber-dash-tech/revela 0.10.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/plugin.ts CHANGED
@@ -48,6 +48,7 @@ import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from
48
48
  import { handleEdit } from "./lib/commands/edit"
49
49
  import { handleInspect } from "./lib/commands/inspect"
50
50
  import { handleRefine } from "./lib/commands/refine"
51
+ import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "./lib/deck-html/contract"
51
52
  import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
52
53
  import { hasLiveEditorSessionForFile } from "./lib/edit/server"
53
54
  import { handleDesignsPreview } from "./lib/commands/designs-preview"
@@ -177,6 +178,27 @@ const server: Plugin = (async (pluginCtx) => {
177
178
  }
178
179
  }
179
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
+
180
202
  function extractSessionID(input: any): string {
181
203
  return input?.sessionID ?? input?.session?.id ?? input?.context?.sessionID ?? ""
182
204
  }
@@ -423,7 +445,7 @@ const server: Plugin = (async (pluginCtx) => {
423
445
  throw new Error("__REVELA_DOMAINS_RM_HANDLED__")
424
446
  }
425
447
  if (sub === "pdf") {
426
- await handlePdf(param, send)
448
+ await handlePdf(param, send, workspaceRoot)
427
449
  throw new Error("__REVELA_PDF_HANDLED__")
428
450
  }
429
451
  if (sub === "pptx") {
@@ -742,6 +764,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
742
764
  return
743
765
  }
744
766
  await appendComplianceReport(filePath, output)
767
+ await appendDeckHtmlContractReport(filePath, output)
745
768
  ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
746
769
  return
747
770
  }
@@ -763,6 +786,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
763
786
  const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
764
787
  for (const target of targets) {
765
788
  await appendComplianceReport(target, output)
789
+ await appendDeckHtmlContractReport(target, output)
766
790
  ensureEditorOpenAfterDeckChange(target, extractSessionID(input))
767
791
  }
768
792
  return
@@ -771,6 +795,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
771
795
  if (input.tool === "edit") {
772
796
  const filePath = extractEditFilePath(input.args)
773
797
  await appendComplianceReport(filePath, output)
798
+ await appendDeckHtmlContractReport(filePath, output)
774
799
  ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
775
800
  return
776
801
  }
package/tools/decks.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import {
3
- applyEvidenceCandidates,
4
3
  createDeckSpec,
5
4
  DECKS_STATE_FILE,
6
5
  normalizeWorkspaceDeckState,
@@ -18,6 +17,10 @@ import {
18
17
  type SlideSpec,
19
18
  } from "../lib/decks-state"
20
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"
21
24
 
22
25
  export default tool({
23
26
  description:
@@ -26,7 +29,7 @@ export default tool({
26
29
  "It stores active deck specs, per-slide content/layout/components, and computes write readiness.",
27
30
  args: {
28
31
  action: tool.schema
29
- .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "remember"])
32
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "review", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
30
33
  .describe("Action to perform on DECKS.json."),
31
34
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
32
35
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -118,6 +121,9 @@ export default tool({
118
121
  notes: tool.schema.string().optional().describe("Implementation notes for this slide."),
119
122
  })).optional().describe("For upsertSlides: complete or partial slide specs."),
120
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."),
121
127
  },
122
128
  async execute(args, context) {
123
129
  try {
@@ -126,8 +132,20 @@ export default tool({
126
132
  const defaultSlug = workspaceDeckSlug(workspaceRoot)
127
133
 
128
134
  if (args.action === "init") {
135
+ const discovered: SourceMaterial[] = []
129
136
  for (const material of (args.sourceMaterials ?? []) as SourceMaterial[]) {
130
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
+ })
131
149
  }
132
150
  writeDecksState(workspaceRoot, state)
133
151
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, state }, null, 2)
@@ -178,6 +196,28 @@ export default tool({
178
196
 
179
197
  if (args.action === "review") {
180
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
+ })
181
221
  writeDecksState(workspaceRoot, reviewed.state)
182
222
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: reviewed.result }, null, 2)
183
223
  }
@@ -185,9 +225,18 @@ export default tool({
185
225
  if (args.action === "applyEvidenceCandidates") {
186
226
  const candidateIds = args.candidateIds ?? []
187
227
  if (candidateIds.length === 0) return JSON.stringify({ ok: false, error: "candidateIds are required for applyEvidenceCandidates" })
188
- const applied = applyEvidenceCandidates(state, candidateIds, { workspaceRoot })
189
- writeDecksState(workspaceRoot, applied.state)
190
- return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: applied.result }, null, 2)
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)
191
240
  }
192
241
 
193
242
  if (args.action === "remember") {
package/tools/pdf.ts CHANGED
@@ -9,6 +9,7 @@ import { existsSync } from "fs"
9
9
  import { resolve } from "path"
10
10
  import { exportToPdf } from "../lib/pdf/export"
11
11
  import { assertExportQAPassed } from "../lib/qa/export-gate"
12
+ import { recordRenderedArtifact, workspaceRelative } from "../lib/workspace-state/rendered-artifacts"
12
13
 
13
14
  export default tool({
14
15
  description:
@@ -35,8 +36,15 @@ export default tool({
35
36
  }
36
37
 
37
38
  try {
38
- await assertExportQAPassed(filePath)
39
+ const root = directory || process.cwd()
40
+ await assertExportQAPassed(filePath, { workspaceRoot: root })
39
41
  const result = await exportToPdf(filePath)
42
+ recordRenderedArtifact(root, {
43
+ sourceHtmlPath: workspaceRelative(resolve(root), filePath),
44
+ outputPath: result.outputPath,
45
+ type: "pdf",
46
+ actor: "revela-pdf",
47
+ })
40
48
  return JSON.stringify({ ok: true, ...result }, null, 2)
41
49
  } catch (e: any) {
42
50
  return JSON.stringify({ ok: false, error: e?.message ?? String(e) })
package/tools/pptx.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  import { tool } from "@opencode-ai/plugin"
8
8
  import { existsSync } from "fs"
9
9
  import { resolve } from "path"
10
+ import { assertDeckHtmlContractValid } from "../lib/deck-html/contract"
10
11
  import { exportToPptx } from "../lib/pptx/export"
12
+ import { recordRenderedArtifact, workspaceRelative } from "../lib/workspace-state/rendered-artifacts"
11
13
 
12
14
  export default tool({
13
15
  description:
@@ -42,12 +44,20 @@ export default tool({
42
44
  const progress: string[] = []
43
45
 
44
46
  try {
47
+ const root = directory || process.cwd()
48
+ assertDeckHtmlContractValid(root, filePath)
45
49
  const result = await exportToPptx(filePath, {
46
50
  speakerNotes: normalizeSpeakerNotes(speakerNotes),
47
51
  onProgress: (event) => {
48
52
  progress.push(event.message)
49
53
  },
50
54
  })
55
+ recordRenderedArtifact(root, {
56
+ sourceHtmlPath: workspaceRelative(resolve(root), filePath),
57
+ outputPath: result.outputPath,
58
+ type: "pptx",
59
+ actor: "revela-pptx",
60
+ })
51
61
  return JSON.stringify({ ok: true, ...result, progress }, null, 2)
52
62
  } catch (e: any) {
53
63
  return JSON.stringify({ ok: false, error: e?.message ?? String(e), progress })
@@ -1,6 +1,8 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import { mkdirSync, writeFileSync } from "fs"
3
3
  import { join } from "path"
4
+ import { hasDecksState, readDecksState, writeDecksState } from "../lib/decks-state"
5
+ import { recordWorkspaceAction } from "../lib/workspace-state/actions"
4
6
 
5
7
  /**
6
8
  * Format today's date as YYYY-MM-DD
@@ -88,6 +90,19 @@ export default tool({
88
90
 
89
91
  writeFileSync(filePath, fileContent, "utf-8")
90
92
 
93
+ if (hasDecksState(workspaceDir)) {
94
+ const state = readDecksState(workspaceDir)
95
+ recordWorkspaceAction(state, {
96
+ type: "research.findings_saved",
97
+ actor: "revela-research-save",
98
+ inputs: { topic: topicKey, axis: fileKey, sourceCount: args.sources?.length ?? 0 },
99
+ outputs: { path: relPath, sources: args.sources ?? [] },
100
+ summary: `Saved research findings for ${topicKey}/${fileKey}.`,
101
+ nodeIds: [`finding:${relPath}`],
102
+ })
103
+ writeDecksState(workspaceDir, state)
104
+ }
105
+
91
106
  return JSON.stringify({ ok: true, path: relPath })
92
107
  } catch (e: any) {
93
108
  return JSON.stringify({ error: e.message || String(e) })
@@ -3,6 +3,8 @@ import { readdirSync, statSync, existsSync } from "fs"
3
3
  import { join, relative, extname, resolve, sep, isAbsolute } from "path"
4
4
  import { sourceMaterialMetadata } from "../lib/source-materials"
5
5
  import type { SourceMaterial } from "../lib/decks-state"
6
+ import { hasDecksState, readDecksState, writeDecksState } from "../lib/decks-state"
7
+ import { recordWorkspaceAction } from "../lib/workspace-state/actions"
6
8
 
7
9
  const DOC_EXTENSIONS = new Set([
8
10
  ".pdf", ".docx", ".doc", ".xlsx", ".xls",
@@ -145,6 +147,19 @@ export default tool({
145
147
  const results: FileEntry[] = []
146
148
  scanDir(scanRoot, workspaceDir, results, maxDepth, 0)
147
149
 
150
+ if (hasDecksState(workspaceDir)) {
151
+ const state = readDecksState(workspaceDir)
152
+ recordWorkspaceAction(state, {
153
+ type: "workspace.scanned",
154
+ actor: "revela-workspace-scan",
155
+ inputs: { path: args.path, maxDepth },
156
+ outputs: { found: results.length, paths: results.map((file) => file.path) },
157
+ summary: `Scanned workspace documents and found ${results.length} file${results.length === 1 ? "" : "s"}.`,
158
+ nodeIds: results.map((file) => `source:${file.sourceMaterial.path}`),
159
+ })
160
+ writeDecksState(workspaceDir, state)
161
+ }
162
+
148
163
  if (results.length === 0) {
149
164
  return JSON.stringify({
150
165
  found: 0,