@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/README.md +6 -7
- package/README.zh-CN.md +6 -7
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/help.ts +1 -2
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/review.ts +49 -12
- package/lib/decks-state.ts +122 -3
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +6 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +100 -10
- package/lib/qa/index.ts +8 -6
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +127 -4
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +36 -130
- package/skill/NARRATIVE_SKILL.md +1 -1
- package/skill/SKILL.md +5 -10
- package/tools/decks.ts +29 -3
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
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 {
|
|
53
|
-
import {
|
|
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
|
|
154
|
+
const blockedPatches = new Map<string, string>()
|
|
158
155
|
|
|
159
|
-
async function
|
|
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 =
|
|
171
|
-
|
|
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("
|
|
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
|
|
205
|
+
function ensureRefineOpenAfterDeckChange(filePath: string, sessionID: string): void {
|
|
233
206
|
if (!isDeckHtmlPath(filePath) || !sessionID) return
|
|
234
207
|
|
|
235
208
|
try {
|
|
236
|
-
|
|
209
|
+
ensureRefineDeckOpenForChange("", {
|
|
237
210
|
client,
|
|
238
211
|
sessionID,
|
|
239
212
|
workspaceRoot,
|
|
240
213
|
})
|
|
241
214
|
} catch (e) {
|
|
242
|
-
childLog("
|
|
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
|
-
|
|
413
|
-
|
|
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:
|
|
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`
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1022
|
-
|
|
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" &&
|
|
1028
|
-
const [blockedPath, blockedReason] =
|
|
1029
|
-
if (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]**
|
|
940
|
+
"---\n\n**[revela prewrite gate]** Patch was blocked.\n\n" +
|
|
1033
941
|
`${blockedReason}\n\n` +
|
|
1034
|
-
"
|
|
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
|
|
1044
|
-
|
|
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
|
|
1053
|
-
|
|
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
|
},
|
package/skill/NARRATIVE_SKILL.md
CHANGED
|
@@ -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
|
-
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
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)
|
package/tools/narrative-view.ts
CHANGED
|
@@ -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 —
|
|
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 {
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
48
|
-
errors: report.
|
|
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
|
|
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
|
})
|