@cyber-dash-tech/revela 0.17.5 → 0.17.6

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.
@@ -93,11 +93,12 @@ Workflow:
93
93
  6. If target slide count, audience, language, output purpose, or visual style is unclear, ask the user for the smallest needed confirmation before writing the plan.
94
94
  7. Write \`deck-plan/index.md\` and one file per planned slide under \`deck-plan/slides/*.md\` from the planning packet and requirements. The index must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. Each slide file must include frontmatter with positive 1-based \`slideIndex\` and \`## Narrative Links\` using plain wikilinks to canonical claim/evidence/risk/objection/gap ids. Include a low-fidelity ASCII/text layout sketch for every slide; do not generate visual images or HTML mockups.
95
95
  8. Stop after presenting the plan unless the user already asked to proceed. Ask whether to continue, revise the plan, or run more research. Do not require an Approval block or \`confirmDeckPlan\` gate; \`confirmDeckPlan\` is compatibility/provenance only.
96
- 9. Ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
97
- 10. Do not update cached \`DECKS.json\` slide specs for plan authoring. Use \`deck-plan/\` files and artifact files as the execution surface.
98
- 11. Call \`revela-decks\` action \`readDeckPlan\` before artifact review or HTML writing; use it to inspect the current deck-plan projection without regenerating it. Treat stale hashes, missing links, or incomplete coverage as advisory diagnostics unless the user asks to stop.
99
- 12. Run artifact diagnostics when useful, but do not treat \`writeReadiness\`, cached slide specs, unconfirmed plans, missing research, or stale coverage as workflow blockers.
100
- 13. Write \`decks/*.html\` when the user chooses to proceed and all deck HTML contract requirements can be satisfied. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Partial decks are allowed during chapter-by-chapter authoring when written slide sections have unique, increasing 1-based \`data-slide-index\` values and valid canvases; do not pad missing planned chapters with filler to match cached \`DECKS.json.slides[]\` length. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
96
+ 9. Ask for or confirm visual design only after the narrative deck plan exists. For a new deck HTML file, call \`revela-deck-foundation\` first to create the active-design foundation shell; it must not create narrative slide content, choose layouts/components, or read/write \`${DECKS_STATE_FILE}\`.
97
+ 10. Fetch active design rules plus required layouts/components with \`revela-designs read\` as needed before patching slides into the foundation shell. Fetch chart rules before ECharts.
98
+ 11. Do not update cached \`DECKS.json\` slide specs for plan authoring. Use \`deck-plan/\` files and artifact files as the execution surface.
99
+ 12. Call \`revela-decks\` action \`readDeckPlan\` before artifact review or HTML writing; use it to inspect the current deck-plan projection without regenerating it. Treat stale hashes, missing links, or incomplete coverage as advisory diagnostics unless the user asks to stop.
100
+ 13. Run artifact diagnostics when useful, but do not treat \`writeReadiness\`, cached slide specs, unconfirmed plans, missing research, or stale coverage as workflow blockers.
101
+ 14. Write \`decks/*.html\` when the user chooses to proceed and all deck HTML contract requirements can be satisfied. For new files, patch slide sections between the \`revela-slides\` markers created by \`revela-deck-foundation\`. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Partial decks are allowed during chapter-by-chapter authoring when written slide sections have unique, increasing 1-based \`data-slide-index\` values and valid canvases; do not pad missing planned chapters with filler to match cached \`DECKS.json.slides[]\` length. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
101
102
  15. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
102
103
  16. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
103
104
 
@@ -142,6 +143,7 @@ Report format before any HTML write:
142
143
  - Start with \`Deck handoff: <status>\`.
143
144
  - Include which deck-plan projection and narrative hash are guiding artifact work.
144
145
  - State that \`revela-decks readDeckPlan\` was called and the current \`deck-plan/\` Chapter Writing Batches are being followed.
146
+ - For new HTML files, state that \`revela-deck-foundation\` created the foundation shell and identify the output path/design before slide content is patched.
145
147
  - Include the chapter currently being generated and confirm already-written slides are being preserved.
146
148
  - If technical artifact checks cannot be satisfied, list those blockers separately from narrative/deck-plan diagnostics.
147
149
  - After writing HTML, read the appended \`Artifact QA\` report from the tool output. If it failed, fix hard errors before considering the deck ready for Review.
@@ -149,6 +151,7 @@ Report format before any HTML write:
149
151
  Rules:
150
152
  - \`compileDeckPlan\` prepares the canonical narrative claim/evidence packet and deck-plan requirements. The LLM authors \`deck-plan/index.md\` and \`deck-plan/slides/*.md\` from that packet and asks the user for page count, audience, language, output purpose, or visual style when unclear.
151
153
  - \`deck-plan/\` is the execution blueprint for HTML generation when present. It must be read before writing HTML and followed chapter by chapter; \`DECKS.json.slides[]\` is compatibility/cache data, not the HTML slide-count authority.
154
+ - \`revela-deck-foundation\` is the file-native foundation helper for new deck shells. Use it before adding slides to a new \`decks/*.html\` file; do not use \`DECKS.json\` or \`revela-decks\` to create the shell.
152
155
  - Visual intent is part of the deck-plan projection. During HTML generation, satisfy the planned component/visual brief using fetched design components; do not collapse planned visuals into prose-only bullets.
153
156
  - Cached deck slide specs in \`DECKS.json\` are legacy projections only. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, and risks.
154
157
  - Cover, Table of Contents, and Closing are mandatory deck structure. TOC chapter headings must match the chapter grouping used for generation.
@@ -0,0 +1,190 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "fs"
2
+ import { dirname, isAbsolute, normalize, resolve } from "path"
3
+ import { activeDesign, getDesignSection } from "../design/designs"
4
+
5
+ export type DeckFoundationMode = "create" | "repair"
6
+ export type DeckFoundationStatus = "created" | "updated"
7
+
8
+ export interface CreateDeckFoundationInput {
9
+ workspaceRoot: string
10
+ outputPath: string
11
+ title: string
12
+ language: string
13
+ designName?: string
14
+ mode?: DeckFoundationMode
15
+ overwrite?: boolean
16
+ }
17
+
18
+ export interface CreateDeckFoundationResult {
19
+ ok: true
20
+ outputPath: string
21
+ design: string
22
+ includedSections: string[]
23
+ status: DeckFoundationStatus
24
+ next: string[]
25
+ }
26
+
27
+ interface FoundationParts {
28
+ fontLinks: string[]
29
+ cssBlocks: string[]
30
+ scriptBlocks: string[]
31
+ }
32
+
33
+ const SLIDES_START = "<!-- revela-slides:start -->"
34
+ const SLIDES_END = "<!-- revela-slides:end -->"
35
+
36
+ export function createDeckFoundation(input: CreateDeckFoundationInput): CreateDeckFoundationResult {
37
+ const outputPath = normalizeOutputPath(input.outputPath)
38
+ const targetPath = safeWorkspaceFilePath(input.workspaceRoot, outputPath)
39
+ const mode = input.mode ?? "create"
40
+ const canOverwrite = input.overwrite === true || mode === "repair"
41
+ const existed = existsSync(targetPath)
42
+
43
+ if (existed && !canOverwrite) {
44
+ throw new Error(`Deck HTML already exists at ${outputPath}. Pass overwrite=true or mode=repair to replace the foundation shell.`)
45
+ }
46
+
47
+ const design = input.designName || activeDesign()
48
+ const foundation = getDesignSection("foundation", design)
49
+ const parts = parseFoundationParts(foundation)
50
+ if (parts.cssBlocks.length === 0) throw new Error(`Design '${design}' foundation does not include a CSS code block.`)
51
+ if (parts.scriptBlocks.length === 0) throw new Error(`Design '${design}' foundation does not include a SlidePresentation JavaScript code block.`)
52
+
53
+ const html = renderFoundationHtml({
54
+ language: input.language || "en",
55
+ title: input.title || "Revela Deck",
56
+ fontLinks: parts.fontLinks,
57
+ css: parts.cssBlocks.join("\n\n"),
58
+ script: parts.scriptBlocks.map(guardEmptyDeckNavigation).join("\n\n"),
59
+ })
60
+
61
+ mkdirSync(dirname(targetPath), { recursive: true })
62
+ writeFileSync(targetPath, html, "utf-8")
63
+
64
+ return {
65
+ ok: true,
66
+ outputPath,
67
+ design,
68
+ includedSections: [
69
+ "design:foundation",
70
+ parts.fontLinks.length > 0 ? "foundation:font-links" : "foundation:font-links:none",
71
+ "foundation:css",
72
+ "foundation:SlidePresentation",
73
+ ],
74
+ status: existed ? "updated" : "created",
75
+ next: [
76
+ "Fetch active design rules before patching slides.",
77
+ "Fetch required layouts and components from the design before adding slide content.",
78
+ "Patch slides between the revela-slides markers chapter by chapter, then run artifact QA.",
79
+ ],
80
+ }
81
+ }
82
+
83
+ export function normalizeOutputPath(outputPath: string): string {
84
+ const trimmed = outputPath.trim()
85
+ if (!trimmed) throw new Error("outputPath is required")
86
+ if (!trimmed.endsWith(".html")) throw new Error("Deck foundation outputPath must end in .html")
87
+ if (isAbsolute(trimmed)) throw new Error("Deck foundation outputPath must be workspace-relative")
88
+ const segments = trimmed.split(/[\\/]+/)
89
+ if (segments.includes("..")) throw new Error("Deck foundation outputPath must not contain parent-directory traversal")
90
+ return normalize(trimmed).replace(/\\/g, "/")
91
+ }
92
+
93
+ function safeWorkspaceFilePath(workspaceRoot: string, outputPath: string): string {
94
+ const root = resolve(workspaceRoot)
95
+ const target = resolve(root, outputPath)
96
+ if (target !== root && !target.startsWith(`${root}/`)) {
97
+ throw new Error("Deck foundation outputPath must stay inside the workspace")
98
+ }
99
+ return target
100
+ }
101
+
102
+ function parseFoundationParts(foundation: string): FoundationParts {
103
+ const fontLinks = extractFontLinks(foundation)
104
+ const cssBlocks: string[] = []
105
+ const scriptBlocks: string[] = []
106
+ const fenceRe = /```([\w-]*)\n([\s\S]*?)```/g
107
+ let match: RegExpExecArray | null
108
+
109
+ while ((match = fenceRe.exec(foundation)) !== null) {
110
+ const lang = (match[1] || "").toLowerCase()
111
+ const body = match[2].trim()
112
+ if (!body) continue
113
+ if (lang === "css") cssBlocks.push(body)
114
+ if ((lang === "javascript" || lang === "js") && body.includes("class SlidePresentation")) {
115
+ scriptBlocks.push(body)
116
+ }
117
+ }
118
+
119
+ return { fontLinks, cssBlocks, scriptBlocks }
120
+ }
121
+
122
+ function extractFontLinks(foundation: string): string[] {
123
+ const links: string[] = []
124
+ const seen = new Set<string>()
125
+ const linkRe = /<link\b[^>]*(?:fonts\.googleapis|fonts\.gstatic|rel=["']preconnect["'])[^>]*>/gi
126
+ let match: RegExpExecArray | null
127
+ while ((match = linkRe.exec(foundation)) !== null) {
128
+ const link = match[0].trim()
129
+ if (seen.has(link)) continue
130
+ seen.add(link)
131
+ links.push(link)
132
+ }
133
+ return links
134
+ }
135
+
136
+ function guardEmptyDeckNavigation(script: string): string {
137
+ return script.replace(
138
+ /new\s+SlidePresentation\s*\(\s*\)\s*;?/g,
139
+ 'if (document.querySelector(".slide")) { new SlidePresentation(); }',
140
+ )
141
+ }
142
+
143
+ function renderFoundationHtml(input: {
144
+ language: string
145
+ title: string
146
+ fontLinks: string[]
147
+ css: string
148
+ script: string
149
+ }): string {
150
+ const fontLinks = input.fontLinks.map((link) => ` ${link}`).join("\n")
151
+ return [
152
+ "<!DOCTYPE html>",
153
+ `<html lang="${escapeAttribute(input.language)}">`,
154
+ "<head>",
155
+ " <meta charset=\"UTF-8\">",
156
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">",
157
+ ` <title>${escapeHtml(input.title)}</title>`,
158
+ fontLinks,
159
+ " <style>",
160
+ input.css,
161
+ " </style>",
162
+ "</head>",
163
+ "<body>",
164
+ ` ${SLIDES_START}`,
165
+ ` ${SLIDES_END}`,
166
+ " <script>",
167
+ input.script,
168
+ " </script>",
169
+ "</body>",
170
+ "</html>",
171
+ "",
172
+ ].filter((line) => line !== "").join("\n")
173
+ }
174
+
175
+ function escapeHtml(value: string): string {
176
+ return value
177
+ .replace(/&/g, "&amp;")
178
+ .replace(/</g, "&lt;")
179
+ .replace(/>/g, "&gt;")
180
+ .replace(/"/g, "&quot;")
181
+ .replace(/'/g, "&#39;")
182
+ }
183
+
184
+ function escapeAttribute(value: string): string {
185
+ return escapeHtml(value.trim() || "en")
186
+ }
187
+
188
+ export function deckFoundationMarkers(): { start: string; end: string } {
189
+ return { start: SLIDES_START, end: SLIDES_END }
190
+ }
package/lib/qa/index.ts CHANGED
@@ -79,6 +79,7 @@ function scanAssetRefs(htmlFilePath: string): LayoutIssue[] {
79
79
  const ref = raw.trim()
80
80
  if (!ref || ref.startsWith("data:") || ref.startsWith("#") || ref.startsWith("mailto:") || ref.startsWith("tel:")) continue
81
81
  if (/^https?:\/\//i.test(ref) || ref.startsWith("//")) {
82
+ if (isAllowedRemoteRuntimeRef(ref)) continue
82
83
  issues.push({ type: "asset", sub: "remote_url", severity: "error", detail: `Deck HTML references remote asset URL \`${ref}\`. Save network images to workspace assets and reference the local file instead.` })
83
84
  continue
84
85
  }
@@ -96,6 +97,17 @@ function scanAssetRefs(htmlFilePath: string): LayoutIssue[] {
96
97
  return issues
97
98
  }
98
99
 
100
+ function isAllowedRemoteRuntimeRef(ref: string): boolean {
101
+ try {
102
+ const url = new URL(ref.startsWith("//") ? `https:${ref}` : ref)
103
+ if (url.hostname === "fonts.googleapis.com" || url.hostname === "fonts.gstatic.com") return true
104
+ if (url.hostname === "cdn.jsdelivr.net" && url.pathname.startsWith("/npm/echarts@")) return true
105
+ } catch {
106
+ return false
107
+ }
108
+ return false
109
+ }
110
+
99
111
  function looksLikeImageRef(ref: string): boolean {
100
112
  return /\.(?:png|jpe?g|webp|gif|svg)(?:[?#].*)?$/i.test(ref)
101
113
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.17.5",
3
+ "version": "0.17.6",
4
4
  "description": "OpenCode plugin for trusted narrative artifacts from local sources, research, and evidence",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/plugin.ts CHANGED
@@ -81,6 +81,7 @@ import decksTool from "./tools/decks"
81
81
  import designsAuthorTool from "./tools/designs-author"
82
82
  import designsTool from "./tools/designs"
83
83
  import domainsTool from "./tools/domains"
84
+ import deckFoundationTool from "./tools/deck-foundation"
84
85
  import mediaBatchSaveTool from "./tools/media-batch-save"
85
86
  import mediaSaveTool from "./tools/media-save"
86
87
  import researchImagesListTool from "./tools/research-images-list"
@@ -586,6 +587,7 @@ const server: Plugin = (async (pluginCtx) => {
586
587
  tool: {
587
588
  "revela-decks": decksTool,
588
589
  "revela-designs": designsTool,
590
+ "revela-deck-foundation": deckFoundationTool,
589
591
  "revela-designs-author": designsAuthorTool,
590
592
  "revela-domains": domainsTool,
591
593
  "revela-media-batch-save": mediaBatchSaveTool,
package/skill/SKILL.md CHANGED
@@ -64,8 +64,13 @@ handoff exactly:
64
64
  5. Use `readDeckPlan` to inspect the current `deck-plan/` projection before
65
65
  artifact review or HTML generation. Diagnostics are advisory unless they are
66
66
  artifact validity errors handled by QA.
67
- 6. Fetch the required design layouts/components with `revela-designs read`.
68
- 7. Write HTML when the user proceeds and the deck contract can be satisfied.
67
+ 6. For a new deck HTML file, call `revela-deck-foundation` to create the
68
+ active-design foundation shell. The helper is file-native and must not create
69
+ narrative slide content, choose layouts/components, or read/write `DECKS.json`.
70
+ 7. Fetch the required design rules, layouts, and components with
71
+ `revela-designs read`.
72
+ 8. Patch slides between the foundation shell's `revela-slides` markers when the
73
+ user proceeds and the deck contract can be satisfied.
69
74
 
70
75
  Before any HTML generation, call `revela-decks` action `readDeckPlan` and follow
71
76
  the current `deck-plan/`: Source Authority, deck parameters, Chapter Writing
@@ -164,7 +169,8 @@ one broad `write`, `edit`, or `apply_patch` call.
164
169
 
165
170
  For decks with 5 or more slides:
166
171
 
167
- - First create a stable HTML shell plus structural slides and the first chapter.
172
+ - First call `revela-deck-foundation` for new files, then patch structural
173
+ slides and the first chapter between the `revela-slides` markers.
168
174
  - Then fill or revise exactly one chapter range at a time.
169
175
  - Do not mix multiple central-claim chapters in the same write.
170
176
  - Chapter divider or chapter TOC slides are allowed as structural wayfinding and
@@ -193,14 +199,17 @@ If a write produces QA hard errors, fix them before continuing.
193
199
  Before writing or materially changing HTML:
194
200
 
195
201
  1. Read the deck-plan projection's layout and component names.
196
- 2. Call `revela-designs` with `action: "read"` and `section: "rules"` to fetch
202
+ 2. For a new deck HTML file, call `revela-deck-foundation` before adding slide
203
+ content. Use `mode: "repair"` only for explicit foundation repair or QA
204
+ foundation contract fixes, not normal Review Comment edits.
205
+ 3. Call `revela-designs` with `action: "read"` and `section: "rules"` to fetch
197
206
  the active design's current composition and usage rules.
198
- 3. Call `revela-designs` with `action: "read"` and `layout` set to all required
207
+ 4. Call `revela-designs` with `action: "read"` and `layout` set to all required
199
208
  layout names, comma-separated.
200
- 4. Call `revela-designs` with `action: "read"` and `component` set to all
209
+ 5. Call `revela-designs` with `action: "read"` and `component` set to all
201
210
  required component names, comma-separated.
202
- 5. Fetch `section: "chart-rules"` before using ECharts.
203
- 6. Do not update legacy `requiredInputs`; design fetching is an execution step,
211
+ 6. Fetch `section: "chart-rules"` before using ECharts.
212
+ 7. Do not update legacy `requiredInputs`; design fetching is an execution step,
204
213
  not a workflow permission gate.
205
214
 
206
215
  Never generate HTML from memory or prior knowledge of a design. Copy the fetched
@@ -0,0 +1,48 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+ import { createDeckFoundation } from "../lib/deck-html/foundation"
3
+
4
+ export default tool({
5
+ description:
6
+ "Create or repair a file-native Revela deck HTML foundation shell from the active design. " +
7
+ "Writes a deterministic empty deck shell with doctype, html/head/body, active design foundation CSS, complete SlidePresentation JavaScript, and stable slide insertion markers. " +
8
+ "It does not create narrative slide content, choose layouts/components, or read/write DECKS.json.",
9
+ args: {
10
+ outputPath: tool.schema
11
+ .string()
12
+ .describe("Workspace-relative HTML output path, usually decks/{name}.html."),
13
+ title: tool.schema
14
+ .string()
15
+ .describe("Presentation title for the HTML <title> tag only; this does not create a cover slide."),
16
+ language: tool.schema
17
+ .string()
18
+ .describe("HTML language tag, e.g. en or zh-CN."),
19
+ designName: tool.schema
20
+ .string()
21
+ .optional()
22
+ .describe("Optional design name. Defaults to the active design."),
23
+ mode: tool.schema
24
+ .enum(["create", "repair"])
25
+ .optional()
26
+ .describe("create protects existing files unless overwrite=true; repair may replace an existing foundation shell."),
27
+ overwrite: tool.schema
28
+ .boolean()
29
+ .optional()
30
+ .describe("Whether create mode may overwrite an existing HTML file. Defaults to false."),
31
+ },
32
+ async execute(args, { directory }) {
33
+ try {
34
+ const result = createDeckFoundation({
35
+ workspaceRoot: directory || process.cwd(),
36
+ outputPath: args.outputPath,
37
+ title: args.title,
38
+ language: args.language,
39
+ designName: args.designName,
40
+ mode: args.mode,
41
+ overwrite: args.overwrite ?? false,
42
+ })
43
+ return JSON.stringify(result, null, 2)
44
+ } catch (e: any) {
45
+ return JSON.stringify({ error: e?.message || String(e) })
46
+ }
47
+ },
48
+ })