@cyber-dash-tech/revela 0.15.0 → 0.15.1

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
@@ -49,9 +49,8 @@ import { buildPptxNotesPrompt, handlePptx, parsePptxArgs, resolvePptxDeck } from
49
49
  import { handleEdit } from "./lib/commands/edit"
50
50
  import { handleInspect } from "./lib/commands/inspect"
51
51
  import { handleRefine } from "./lib/commands/refine"
52
- import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "./lib/deck-html/contract"
53
- import { ensureEditableDeckOpenForChange } from "./lib/edit/open"
54
- import { hasLiveEditorSessionForFile } from "./lib/edit/server"
52
+ import { formatArtifactQAReport, runArtifactQA } from "./lib/qa/artifact"
53
+ import { ensureRefineDeckOpenForChange } from "./lib/refine/open"
55
54
  import { handleDesignsPreview } from "./lib/commands/designs-preview"
56
55
  import {
57
56
  parseDesignsNewArgs,
@@ -62,7 +61,7 @@ import {
62
61
  import { buildInitPrompt } from "./lib/commands/init"
63
62
  import { buildResearchPrompt } from "./lib/commands/research"
64
63
  import { handleBrief, parseBriefArgs } from "./lib/commands/brief"
65
- import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs } from "./lib/commands/narrative"
64
+ import { buildNarrativeViewPrompt, handleNarrative, parseNarrativeArgs, parseStoryArgs } from "./lib/commands/narrative"
66
65
  import { parseRememberArgs, buildRememberPrompt } from "./lib/commands/remember"
67
66
  import { buildDeckPrompt, buildDeckReviewPrompt, buildReviewPrompt } from "./lib/commands/review"
68
67
  import {
@@ -73,7 +72,6 @@ import {
73
72
  } from "./lib/decks-memory"
74
73
  import {
75
74
  buildDecksStatePromptLayer,
76
- checkDeckStateWriteReadiness,
77
75
  DECKS_STATE_FILE,
78
76
  extractDecksStateTargetsFromPatch,
79
77
  hasDecksState,
@@ -98,7 +96,6 @@ import pptxTool from "./tools/pptx"
98
96
  import createEditTool from "./tools/edit"
99
97
  import { RESEARCH_PROMPT, RESEARCH_AGENT_SIGNATURE } from "./lib/agents/research-prompt"
100
98
  import { NARRATIVE_REVIEWER_PROMPT, NARRATIVE_REVIEWER_SIGNATURE } from "./lib/agents/narrative-reviewer-prompt"
101
- import { formatReport, runComplianceQA } from "./lib/qa"
102
99
  import { extractDesignClasses } from "./lib/design/designs"
103
100
  import { log, childLog } from "./lib/log"
104
101
 
@@ -154,53 +151,29 @@ const server: Plugin = (async (pluginCtx) => {
154
151
  const client = pluginCtx.client
155
152
  const workspaceRoot = pluginCtx.directory
156
153
  const blockedDeckWrites = new Map<string, string>()
157
- const blockedDeckPatches = new Map<string, string>()
154
+ const blockedPatches = new Map<string, string>()
158
155
 
159
- async function appendComplianceReport(filePath: string, output: any): Promise<void> {
160
- if (!isDeckHtmlPath(filePath)) return
156
+ async function runPostWriteArtifactQA(filePath: string, output: any): Promise<boolean> {
157
+ if (!isDeckHtmlPath(filePath)) return true
161
158
 
162
159
  try {
163
160
  let vocabulary
164
161
  try {
165
162
  vocabulary = extractDesignClasses()
166
163
  } catch {
167
- // Design may not be installed or may have no markers — skip compliance.
164
+ // Design may not be installed or may have no markers — skip compliance vocabulary.
168
165
  }
169
166
 
170
- const report = runComplianceQA(filePath, vocabulary)
171
- if (report.totalIssues === 0) return
172
-
173
- appendToolResult(
174
- output,
175
- "---\n\n**[revela design compliance]** Static check completed:\n\n" +
176
- formatReport(report)
177
- )
167
+ const report = await runArtifactQA({ workspaceRoot, filePath, vocabulary })
168
+ appendToolResult(output, "---\n\n" + formatArtifactQAReport(report))
169
+ return report.passed
178
170
  } catch (e) {
179
- childLog("compliance").warn("static compliance failed", {
180
- filePath,
181
- error: e instanceof Error ? e.message : String(e),
182
- })
183
- }
184
- }
185
-
186
- async function appendDeckHtmlContractReport(filePath: string, output: any): Promise<void> {
187
- if (!isDeckHtmlPath(filePath)) return
188
-
189
- try {
190
- const report = validateDeckHtmlContract(workspaceRoot, filePath)
191
- if (report.status === "valid" || report.status === "skipped") return
192
-
193
- appendToolResult(
194
- output,
195
- "---\n\n**[revela deck HTML contract]** Slide identity check failed:\n\n" +
196
- formatDeckHtmlContractReport(report) +
197
- "\n\nFix every `<section class=\"slide\">` to use the matching 1-based `data-slide-index` from DECKS.json before inspection or export."
198
- )
199
- } catch (e) {
200
- childLog("deck-contract").warn("deck HTML contract report failed", {
171
+ childLog("artifact-qa").warn("post-write artifact QA failed", {
201
172
  filePath,
202
173
  error: e instanceof Error ? e.message : String(e),
203
174
  })
175
+ appendToolResult(output, "---\n\n## Artifact QA: FAILED\n\nError running artifact QA: " + (e instanceof Error ? e.message : String(e)))
176
+ return false
204
177
  }
205
178
  }
206
179
 
@@ -229,17 +202,17 @@ const server: Plugin = (async (pluginCtx) => {
229
202
  input.output.parts.push({ type: "text", text: input.visibleText } as any)
230
203
  }
231
204
 
232
- function ensureEditorOpenAfterDeckChange(filePath: string, sessionID: string): void {
205
+ function ensureRefineOpenAfterDeckChange(filePath: string, sessionID: string): void {
233
206
  if (!isDeckHtmlPath(filePath) || !sessionID) return
234
207
 
235
208
  try {
236
- ensureEditableDeckOpenForChange("", {
209
+ ensureRefineDeckOpenForChange("", {
237
210
  client,
238
211
  sessionID,
239
212
  workspaceRoot,
240
213
  })
241
214
  } catch (e) {
242
- childLog("edit").warn("failed to ensure visual editor after deck change", {
215
+ childLog("refine").warn("failed to ensure Refine after deck change", {
243
216
  filePath,
244
217
  error: e instanceof Error ? e.message : String(e),
245
218
  })
@@ -409,8 +382,9 @@ const server: Plugin = (async (pluginCtx) => {
409
382
  return
410
383
  }
411
384
  if (sub === "story") {
412
- if (param) {
413
- await send("`/revela story` does not accept arguments yet. It opens the current workspace story UI. Use `/revela review` for a readiness report.")
385
+ const parsed = parseStoryArgs(param)
386
+ if (!parsed.ok) {
387
+ await send(parsed.error)
414
388
  throw new Error("__REVELA_STORY_USAGE_HANDLED__")
415
389
  }
416
390
  queueWorkflowCommand({
@@ -418,7 +392,7 @@ const server: Plugin = (async (pluginCtx) => {
418
392
  name: "story",
419
393
  mode: "narrative",
420
394
  visibleText: "Open Revela story workspace.",
421
- hiddenPrompt: buildNarrativeViewPrompt({ workspaceRoot, language: "en" }),
395
+ hiddenPrompt: buildNarrativeViewPrompt({ workspaceRoot, language: parsed.args.language }),
422
396
  output,
423
397
  })
424
398
  return
@@ -531,7 +505,7 @@ const server: Plugin = (async (pluginCtx) => {
531
505
  }
532
506
  if (sub === "edit") {
533
507
  if (param) {
534
- await send("`/revela edit` is deprecated and does not accept a target. Use `/revela refine` for the unified refinement workspace.")
508
+ await send("`/revela edit` has been removed. Use `/revela refine` for the unified reading, inspection, and editing workspace.")
535
509
  throw new Error("__REVELA_EDIT_USAGE_HANDLED__")
536
510
  }
537
511
  await handleEdit({ client, sessionID, workspaceRoot }, send)
@@ -850,7 +824,7 @@ const server: Plugin = (async (pluginCtx) => {
850
824
 
851
825
  // ── Pre-tool processing ────────────────────────────────────────────────
852
826
  // - read: intercept DOCX/PPTX/XLSX before read executes.
853
- // - write/apply_patch: gate decks/*.html on DECKS.json readiness.
827
+ // - write/apply_patch: protect DECKS.json, but do not block deck HTML edits.
854
828
  "tool.execute.before": async (input, output) => {
855
829
  log.info("[hook] tool.execute.before fired", { tool: input.tool, enabled: ctx.enabled, isResearch: ctx.isResearchAgent })
856
830
  if (!ctx.enabled) return
@@ -875,32 +849,6 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
875
849
  childLog("decks-state").warn("blocked direct DECKS.json write", { filePath, blockedPath })
876
850
  return
877
851
  }
878
- if (!isDeckHtmlPath(filePath)) return
879
- if (hasLiveEditorSessionForFile(workspaceRoot, filePath)) return
880
-
881
- const readiness = checkDeckStateWriteReadiness(workspaceRoot, filePath) ?? {
882
- ready: false,
883
- slug: basename(filePath, ".html") || "deck",
884
- blocker: `No ${DECKS_STATE_FILE} exists. Use revela-decks init/upsertDeck/upsertSlides/review before writing deck HTML.`,
885
- blockers: [`No ${DECKS_STATE_FILE} exists.`],
886
- }
887
- if (readiness.ready) return
888
-
889
- const blockedDir = join(workspaceRoot, ".opencode", "revela", "blocked-writes")
890
- mkdirSync(blockedDir, { recursive: true })
891
- const blockedPath = join(blockedDir, `${readiness.slug}.blocked.md`)
892
- ;(output.args as any).filePath = blockedPath
893
- ;(output.args as any).content = `# Revela Blocked Deck Write
894
-
895
- The attempted write to \`${filePath}\` was blocked.
896
-
897
- Reason: ${readiness.blocker}
898
-
899
- Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FILE}, then write only after the matching deck has \`writeReadiness.status\` set to \`ready\` and no blockers.
900
- `
901
- blockedDeckWrites.set(filePath, readiness.blocker)
902
- childLog("decks-memory").warn("blocked deck write", { filePath, blockedPath, blocker: readiness.blocker })
903
- return
904
852
  }
905
853
 
906
854
  if (input.tool === "apply_patch") {
@@ -925,49 +873,10 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
925
873
  +Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSlides\`, or \`review\`.
926
874
  *** End Patch`
927
875
  setPatchTextArg(args, blockedPatch)
928
- blockedDeckPatches.set(blockedRelativePath, blocker)
876
+ blockedPatches.set(blockedRelativePath, blocker)
929
877
  childLog("decks-state").warn("blocked direct DECKS.json patch", { targets: stateTargets, blockedPath: blockedRelativePath })
930
878
  return
931
879
  }
932
-
933
- const targets = extractDeckHtmlTargetsFromPatch(patchText)
934
- if (targets.length === 0) return
935
- if (targets.every((target) => hasLiveEditorSessionForFile(workspaceRoot, target))) return
936
-
937
- const blocked = targets
938
- .map((target) => ({
939
- target,
940
- readiness: checkDeckStateWriteReadiness(workspaceRoot, target) ?? {
941
- ready: false,
942
- slug: basename(target, ".html") || "deck",
943
- blocker: `No ${DECKS_STATE_FILE} exists. Use revela-decks init/upsertDeck/upsertSlides/review before patching deck HTML.`,
944
- blockers: [`No ${DECKS_STATE_FILE} exists.`],
945
- },
946
- }))
947
- .find((item) => !item.readiness.ready)
948
- if (!blocked) return
949
-
950
- const blockedDir = join(workspaceRoot, ".opencode", "revela", "blocked-writes")
951
- mkdirSync(blockedDir, { recursive: true })
952
- const blockedRelativePath = `.opencode/revela/blocked-writes/${blocked.readiness.slug}-${Date.now()}.blocked.md`
953
- const blockedPatch = `*** Begin Patch
954
- *** Add File: ${blockedRelativePath}
955
- +# Revela Blocked Deck Patch
956
- +
957
- +The attempted patch touching \`${blocked.target}\` was blocked.
958
- +
959
- +Reason: ${blocked.readiness.blocker}
960
- +
961
- +Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FILE}, then patch only after the matching deck has \`writeReadiness.status\` set to \`ready\` and no blockers.
962
- *** End Patch`
963
- setPatchTextArg(args, blockedPatch)
964
- blockedDeckPatches.set(blockedRelativePath, blocked.readiness.blocker)
965
- childLog("decks-memory").warn("blocked deck patch", {
966
- target: blocked.target,
967
- blockedPath: blockedRelativePath,
968
- blocker: blocked.readiness.blocker,
969
- })
970
- return
971
880
  }
972
881
 
973
882
  if (input.tool === "read") {
@@ -987,7 +896,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
987
896
  // PDF: extract text, remove base64. Images: jimp compress.
988
897
  //
989
898
  // Also reports writes/patches blocked by the DECKS.json prewrite gate and
990
- // runs lightweight static design compliance after successful deck changes.
899
+ // runs artifact QA before opening Refine after successful deck changes.
991
900
  "tool.execute.after": async (input, output) => {
992
901
  if (!ctx.enabled) return
993
902
 
@@ -1004,7 +913,7 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
1004
913
  return
1005
914
  }
1006
915
 
1007
- // ── Report blocked deck writes and run static compliance ──────────
916
+ // ── Report blocked state writes and run artifact QA ───────────────
1008
917
  if (input.tool === "write") {
1009
918
  const filePath: string = input.args?.filePath ?? ""
1010
919
  const blockedReason = blockedDeckWrites.get(filePath)
@@ -1018,20 +927,19 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
1018
927
  )
1019
928
  return
1020
929
  }
1021
- await appendComplianceReport(filePath, output)
1022
- await appendDeckHtmlContractReport(filePath, output)
1023
- ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
930
+ const qaPassed = await runPostWriteArtifactQA(filePath, output)
931
+ if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
1024
932
  return
1025
933
  }
1026
934
 
1027
- if (input.tool === "apply_patch" && blockedDeckPatches.size > 0) {
1028
- const [blockedPath, blockedReason] = blockedDeckPatches.entries().next().value ?? []
1029
- if (blockedPath) blockedDeckPatches.delete(blockedPath)
935
+ if (input.tool === "apply_patch" && blockedPatches.size > 0) {
936
+ const [blockedPath, blockedReason] = blockedPatches.entries().next().value ?? []
937
+ if (blockedPath) blockedPatches.delete(blockedPath)
1030
938
  appendToolResult(
1031
939
  output,
1032
- "---\n\n**[revela prewrite gate]** Deck HTML patch was blocked.\n\n" +
940
+ "---\n\n**[revela prewrite gate]** Patch was blocked.\n\n" +
1033
941
  `${blockedReason}\n\n` +
1034
- "Run `/revela review` or complete the same DECKS.json review workflow before patching the deck."
942
+ "Use the `revela-decks` tool for controlled workspace state changes."
1035
943
  )
1036
944
  return
1037
945
  }
@@ -1040,18 +948,16 @@ Next step: use \`revela-decks\` or \`/revela review\` to update ${DECKS_STATE_FI
1040
948
  const patchText = extractPatchTextArg(input.args as Record<string, unknown>)
1041
949
  const targets = patchText ? extractDeckHtmlTargetsFromPatch(patchText) : []
1042
950
  for (const target of targets) {
1043
- await appendComplianceReport(target, output)
1044
- await appendDeckHtmlContractReport(target, output)
1045
- ensureEditorOpenAfterDeckChange(target, extractSessionID(input))
951
+ const qaPassed = await runPostWriteArtifactQA(target, output)
952
+ if (qaPassed) ensureRefineOpenAfterDeckChange(target, extractSessionID(input))
1046
953
  }
1047
954
  return
1048
955
  }
1049
956
 
1050
957
  if (input.tool === "edit") {
1051
958
  const filePath = extractEditFilePath(input.args)
1052
- await appendComplianceReport(filePath, output)
1053
- await appendDeckHtmlContractReport(filePath, output)
1054
- ensureEditorOpenAfterDeckChange(filePath, extractSessionID(input))
959
+ const qaPassed = await runPostWriteArtifactQA(filePath, output)
960
+ if (qaPassed) ensureRefineOpenAfterDeckChange(filePath, extractSessionID(input))
1055
961
  return
1056
962
  }
1057
963
  },
@@ -124,7 +124,7 @@ Use `/revela refine` for post-artifact reading, inspection, and editing.
124
124
  - Reading should explain source, support strength, caveat, unsupported scope, narrative purpose, related risks/objections, research gaps, and artifact coverage.
125
125
  - Pure artifact polish may stay artifact-level: layout, typography, spacing, crop, visual hierarchy, export mechanics, and deck contract fixes.
126
126
  - Meaning-changing edits must update canonical narrative first, then run story readiness/approval or explicit override, then remake affected artifacts.
127
- - Deprecated `/revela edit` and `/revela inspect` route to Refine.
127
+ - `/revela edit` has been removed; use `/revela refine`. Deprecated `/revela inspect` routes to Refine.
128
128
 
129
129
  ## Design Surface
130
130
 
package/skill/SKILL.md CHANGED
@@ -460,16 +460,11 @@ deck HTML writes or patches. If the tool result reports compliance issues, fix
460
460
  them immediately by removing the offending classes and replacing them with the
461
461
  closest component from the Component Index.
462
462
 
463
- Do not run `revela-qa` after writing or editing HTML unless the user explicitly
464
- asks for diagnostics. PDF/PPTX export commands run hard-error pre-export QA
465
- automatically and will report overflow issues that must be fixed before exporting.
466
-
467
- ### Inline Editing
468
-
469
- **Always include inline editing** in every generated presentation. The complete
470
- reference implementation is provided in the active design's `@design:foundation`
471
- section. Follow it exactly — pay attention to the hover-delay pattern, editable
472
- element selector list, and `window.getEditedHTML()` definition.
463
+ Deck HTML writes and patches automatically run Artifact QA. If hard errors are
464
+ reported, fix them immediately with the smallest patch; Refine opens only after
465
+ hard errors pass. Do not add deck-local inline editing JavaScript, `contenteditable`
466
+ handlers, `editable` classes, or `window.getEditedHTML()` implementations. Post-
467
+ artifact editing belongs in `/revela refine`, not inside generated deck HTML.
473
468
 
474
469
  ### Image Rules
475
470
 
package/tools/decks.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { tool } from "@opencode-ai/plugin"
2
2
  import {
3
3
  createDeckSpec,
4
+ confirmDeckPlan,
4
5
  DECKS_STATE_FILE,
5
6
  normalizeWorkspaceDeckState,
6
7
  readOrCreateDecksState,
@@ -67,7 +68,7 @@ export default tool({
67
68
  "It stores workspace narrative state, active deck specs, per-slide content/layout/components, and computes narrative or deck readiness.",
68
69
  args: {
69
70
  action: tool.schema
70
- .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
71
+ .enum(["read", "init", "upsertDeck", "upsertSlides", "upsertNarrative", "compileDeckPlan", "confirmDeckPlan", "backfillClaimRefs", "review", "reviewNarrative", "approveNarrative", "deriveResearchGaps", "upsertResearchGaps", "updateResearchGap", "closeResearchGap", "applyEvidenceCandidates", "attachResearchFindings", "remember"])
71
72
  .describe("Action to perform on DECKS.json."),
72
73
  summary: tool.schema.boolean().optional().describe("For read: return a compact summary instead of full state."),
73
74
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
@@ -251,8 +252,8 @@ export default tool({
251
252
  findingsFile: tool.schema.string().optional().describe("For attachResearchFindings: workspace-relative researches/{topic}/{axis}.md file to attach to researchPlan."),
252
253
  researchAxis: tool.schema.string().optional().describe("For attachResearchFindings: researchPlan axis to attach the findings file to. Required when filename matching would be ambiguous."),
253
254
  researchStatus: tool.schema.enum(["done", "read"]).optional().describe("For attachResearchFindings: optional explicit status to set on the matched research axis."),
254
- approvalNote: tool.schema.string().optional().describe("For approveNarrative: optional note explaining the approval or override."),
255
- approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative: use override only for explicit render overrides, not normal strategic approval."),
255
+ approvalNote: tool.schema.string().optional().describe("For approveNarrative or confirmDeckPlan: optional note explaining the approval, override, or deck plan confirmation."),
256
+ approvalBy: tool.schema.enum(["user", "override"]).optional().describe("For approveNarrative or confirmDeckPlan: use override only for explicit render overrides, not normal strategic approval or deck plan confirmation."),
256
257
  approvalScope: tool.schema.enum(["narrative", "render_override"]).optional().describe("For approveNarrative: narrative approval or explicit render override scope."),
257
258
  gapId: tool.schema.string().optional().describe("For updateResearchGap/closeResearchGap: canonical research gap id."),
258
259
  researchGaps: tool.schema.array(tool.schema.object({
@@ -428,6 +429,31 @@ export default tool({
428
429
  return JSON.stringify({ ok: true, path: DECKS_STATE_FILE, result: compiled.result, deck: compiled.state.activeDeck ? compiled.state.decks[compiled.state.activeDeck] : undefined, narrative: compiled.state.narrative }, null, 2)
429
430
  }
430
431
 
432
+ if (args.action === "confirmDeckPlan") {
433
+ if (args.approvalBy && args.approvalBy !== "user") return JSON.stringify({ ok: false, error: "confirmDeckPlan requires approvalBy=user" })
434
+ const confirmed = confirmDeckPlan(state, {
435
+ approvedBy: "user",
436
+ note: args.approvalNote,
437
+ })
438
+ if (confirmed.result.confirmed) {
439
+ recordWorkspaceAction(confirmed.state, {
440
+ type: "deck.plan_confirmed",
441
+ actor: "revela-decks",
442
+ inputs: { activeDeck: state.activeDeck, approvalBy: "user" },
443
+ outputs: {
444
+ slug: confirmed.result.slug,
445
+ narrativeHash: confirmed.result.narrativeHash,
446
+ planHash: confirmed.result.planHash,
447
+ },
448
+ status: "success",
449
+ summary: args.approvalNote?.trim() || "User confirmed the compiled deck plan.",
450
+ nodeIds: [confirmed.state.narrative?.id, confirmed.result.slug ? `deck:${confirmed.result.slug}` : undefined].filter((item): item is string => Boolean(item)),
451
+ })
452
+ }
453
+ writeDecksState(workspaceRoot, confirmed.state)
454
+ return JSON.stringify({ ok: confirmed.result.confirmed, path: DECKS_STATE_FILE, result: confirmed.result, deck: confirmed.state.activeDeck ? confirmed.state.decks[confirmed.state.activeDeck] : undefined }, null, 2)
455
+ }
456
+
431
457
  if (args.action === "backfillClaimRefs") {
432
458
  const backfilled = backfillSlideClaimRefsFromCoverage(state)
433
459
  writeDecksState(workspaceRoot, backfilled.state)
@@ -10,7 +10,7 @@ export default tool({
10
10
  "Render Revela's read-only narrative claim-flow UI from the current deterministic narrative map plus an optional localized display model. " +
11
11
  "This tool validates display IDs against DECKS.json, opens a local HTML view, and never mutates workspace state.",
12
12
  args: {
13
- language: tool.schema.string().describe("UI language request from /revela narrative. May be any language tag or language name, such as en, zh-CN, fr, de, Korean, Arabic, or Portuguese-BR."),
13
+ language: tool.schema.string().describe("UI language request from /revela story or /revela narrative. May be any language tag or language name, such as en, zh-CN, fr, de, Korean, Arabic, or Portuguese-BR."),
14
14
  narrativeHash: tool.schema.string().optional().describe("Narrative hash from the prompt projection. Used to detect stale display prompts."),
15
15
  displayModel: tool.schema.object({
16
16
  version: tool.schema.number().describe("Must be 1."),
package/tools/qa.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * tools/qa.ts
3
3
  *
4
- * revela-qa — Hard-error quality assurance for generated slide HTML files.
4
+ * revela-qa — Artifact quality assurance for generated slide HTML files.
5
5
  *
6
6
  * Exposed as a manual diagnostic tool. Export commands run pre-export QA automatically.
7
7
  */
@@ -9,15 +9,16 @@
9
9
  import { tool } from "@opencode-ai/plugin"
10
10
  import { resolve } from "path"
11
11
  import { existsSync } from "fs"
12
- import { runQA, formatReport } from "../lib/qa"
12
+ import { extractDesignClasses } from "../lib/design/designs"
13
+ import { formatArtifactQAReport, runArtifactQA } from "../lib/qa/artifact"
13
14
 
14
15
  export default tool({
15
16
  description:
16
- "Run hard-error checks on a generated slide HTML file. " +
17
+ "Run artifact QA on a generated slide HTML file. " +
17
18
  "Opens the file in a headless browser and measures actual rendered geometry. " +
18
- "Checks for element overflow. " +
19
+ "Checks deck contract, component compliance, exact 1920x1080 canvas, scrollbars, element overflow, text overflow, and content/evidence density warnings. " +
19
20
  "Returns a structured report with specific issues and fix instructions. " +
20
- "Normally PDF/PPTX export commands run this automatically; call it directly only for explicit diagnostics.",
21
+ "Deck writes and PDF/PPTX export commands run QA automatically; call it directly for explicit diagnostics.",
21
22
  args: {
22
23
  file: tool.schema
23
24
  .string()
@@ -39,20 +40,25 @@ export default tool({
39
40
  }
40
41
 
41
42
  try {
42
- const report = await runQA(filePath)
43
- const formatted = formatReport(report)
43
+ let vocabulary
44
+ try {
45
+ vocabulary = extractDesignClasses()
46
+ } catch {
47
+ // Design may not be installed or may have no markers.
48
+ }
49
+ const report = await runArtifactQA({ workspaceRoot: directory || process.cwd(), filePath, vocabulary })
50
+ const formatted = formatArtifactQAReport(report)
44
51
 
45
52
  // Prepend a compact JSON summary for programmatic use if needed
46
53
  const jsonSummary = JSON.stringify({
47
- totalIssues: report.totalIssues,
48
- errors: report.errorCount,
54
+ passed: report.passed,
55
+ errors: report.hardErrorCount,
49
56
  warnings: report.warningCount,
50
- slidesWithIssues: report.slides.filter((s) => s.issues.length > 0).map((s) => s.index + 1),
51
57
  })
52
58
 
53
59
  return `<!-- QA Summary: ${jsonSummary} -->\n\n${formatted}`
54
60
  } catch (err: any) {
55
- return `Error running hard-error QA: ${err?.message ?? String(err)}\n\nMake sure Chrome is installed at /Applications/Google Chrome.app`
61
+ return `Error running artifact QA: ${err?.message ?? String(err)}\n\nMake sure Chrome is installed at /Applications/Google Chrome.app`
56
62
  }
57
63
  },
58
64
  })