@cyber-dash-tech/revela 0.8.2 → 0.8.4

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 CHANGED
@@ -205,7 +205,7 @@ Review the current deck state with:
205
205
 
206
206
  Minimum readiness conditions:
207
207
 
208
- - topic, audience, slide count, language, and visual style/design are decided
208
+ - topic, audience, language, visual style/design, and slide plan are decided
209
209
  - source materials are identified or explicitly deemed unnecessary
210
210
  - research need is assessed
211
211
  - needed research findings have been read and reflected in the slide specs
@@ -222,7 +222,6 @@ The gate checks:
222
222
  - `writeReadiness.blockers` is empty
223
223
  - the deck `outputPath` exactly matches the target `decks/*.html` path
224
224
  - all `requiredInputs` booleans are true
225
- - `slides.length` matches `slideCount` when a slide count is set
226
225
  - every slide has title, layout, components, and structured content
227
226
  - every needed research axis is `done`, `read`, or `skipped`
228
227
 
package/README.zh-CN.md CHANGED
@@ -204,7 +204,7 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
204
204
 
205
205
  最小 readiness 条件:
206
206
 
207
- - topic、audience、slide count、language、visual style/design 已确定
207
+ - topic、audience、language、visual style/design 和 slide plan 已确定
208
208
  - source materials 已识别,或明确不需要源材料
209
209
  - research need 已评估
210
210
  - 需要调研时,相关 findings 已读取并反映到逐页规格中
@@ -221,7 +221,6 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
221
221
  - `writeReadiness.blockers` 为空
222
222
  - deck `outputPath` 精确匹配目标 `decks/*.html` 路径
223
223
  - 所有 `requiredInputs` 布尔值都是 true
224
- - 如果设置了 `slideCount`,`slides.length` 必须匹配
225
224
  - 每页都有 title、layout、components 和结构化 content
226
225
  - 每个 needed research axis 都是 `done`、`read` 或 `skipped`
227
226
 
@@ -16,9 +16,10 @@ export function buildInitPrompt({
16
16
  Goal:
17
17
  - Build or update ${DECKS_STATE_FILE}, the workspace-level machine-readable state file for slide deck work.
18
18
  - Use the \`revela-decks\` tool for state updates. Do not write or patch ${DECKS_STATE_FILE} directly.
19
- - Capture stable project context, available source materials, the current deck spec, slide plan, and open questions for future sessions.
19
+ - Capture stable project context, professional/domain context, topic, audience, available source materials, the current deck artifact, slide specs, and open questions for future sessions.
20
20
  - Do not treat initialization as permission to write a slide deck; the current deck must pass a later readiness review.
21
21
  - ${DECKS_STATE_FILE} is the source of truth for the single current workspace deck.
22
+ - \`slides\` is the only slide-plan source of truth. Do not track or infer a separate slide count field.
22
23
 
23
24
  Current state:
24
25
  - ${mode}
@@ -45,8 +46,9 @@ Workflow:
45
46
  4. For selected PDF/PPTX/DOCX/XLSX files, call \`revela-extract-document-materials\` before deciding what to summarize.
46
47
  5. Read only the materials needed to form a conservative workspace memory. Do not exhaustively read every file if the workspace is large.
47
48
  6. Call \`revela-decks\` with action \`init\` to create ${DECKS_STATE_FILE} if needed.
48
- 7. If this conversation already contains a concrete deck task, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` only for explicit deck spec information. Do not pass or ask for a deck key; the tool uses the workspace folder name internally. Do not mark readiness ready during init.
49
- 8. Report what was initialized or updated and list any open questions.
49
+ 7. If this conversation or the workspace contains a concrete deck task or an existing deck artifact, call \`revela-decks\` with action \`upsertDeck\` and later \`upsertSlides\` for explicit deck information. Do not pass or ask for a deck key; the tool uses the workspace folder name internally. Do not mark readiness ready during init.
50
+ 8. When adopting an existing HTML deck, analyze the artifact and create one conservative \`SlideSpec\` per identifiable slide/page. The \`SlideSpec[]\` itself is the worklist; do not create a separate target slide count.
51
+ 9. Report what was initialized or updated and list any open questions.
50
52
 
51
53
  Memory rules:
52
54
  - Only write facts supported by workspace files into ${DECKS_STATE_FILE} workspace state, source materials, deck memory, and open questions.
@@ -45,7 +45,6 @@ export interface DeckSpec {
45
45
  goal: string
46
46
  audience?: string
47
47
  language?: string
48
- slideCount?: number
49
48
  outputPath: string
50
49
  theme: {
51
50
  design?: string
@@ -65,7 +64,6 @@ export interface DeckSpec {
65
64
  export interface RequiredInputs {
66
65
  topicClarified: boolean
67
66
  audienceClarified: boolean
68
- slideCountDecided: boolean
69
67
  languageDecided: boolean
70
68
  visualStyleSelected: boolean
71
69
  sourceMaterialsIdentified: boolean
@@ -179,17 +177,15 @@ export function normalizeWorkspaceDeckState(state: DecksState, workspaceRoot: st
179
177
 
180
178
  export function defaultRequiredInputs(overrides?: Partial<RequiredInputs>): RequiredInputs {
181
179
  return {
182
- topicClarified: false,
183
- audienceClarified: false,
184
- slideCountDecided: false,
185
- languageDecided: false,
186
- visualStyleSelected: false,
187
- sourceMaterialsIdentified: false,
188
- researchNeedAssessed: false,
189
- researchFindingsRead: false,
190
- slidePlanConfirmed: false,
191
- designLayoutsFetched: false,
192
- ...overrides,
180
+ topicClarified: overrides?.topicClarified ?? false,
181
+ audienceClarified: overrides?.audienceClarified ?? false,
182
+ languageDecided: overrides?.languageDecided ?? false,
183
+ visualStyleSelected: overrides?.visualStyleSelected ?? false,
184
+ sourceMaterialsIdentified: overrides?.sourceMaterialsIdentified ?? false,
185
+ researchNeedAssessed: overrides?.researchNeedAssessed ?? false,
186
+ researchFindingsRead: overrides?.researchFindingsRead ?? false,
187
+ slidePlanConfirmed: overrides?.slidePlanConfirmed ?? false,
188
+ designLayoutsFetched: overrides?.designLayoutsFetched ?? false,
193
189
  }
194
190
  }
195
191
 
@@ -201,7 +197,6 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
201
197
  goal: input.goal ?? "",
202
198
  audience: input.audience,
203
199
  language: input.language,
204
- slideCount: input.slideCount,
205
200
  outputPath: normalizeDeckPath(input.outputPath || `decks/${slug}.html`),
206
201
  theme: input.theme ?? {},
207
202
  requiredInputs: defaultRequiredInputs(input.requiredInputs),
@@ -419,9 +414,6 @@ function computeDeckBlockers(deck: DeckSpec): string[] {
419
414
  if (value !== true) blockers.push(`requiredInputs.${key} is not true`)
420
415
  }
421
416
 
422
- if (typeof deck.slideCount === "number" && deck.slideCount > 0 && deck.slides.length !== deck.slideCount) {
423
- blockers.push(`slides length ${deck.slides.length} does not match slideCount ${deck.slideCount}`)
424
- }
425
417
  if (deck.slides.length === 0) blockers.push("slides are missing")
426
418
  for (const slide of deck.slides) {
427
419
  if (!slide.title.trim()) blockers.push(`Slide ${slide.index} title is missing`)
@@ -32,7 +32,6 @@ export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDec
32
32
  goal: existing?.goal || `Edit existing Revela deck ${deck.slug}.`,
33
33
  audience: existing?.audience || "Existing deck viewers",
34
34
  language: existing?.language || "en",
35
- slideCount: existing?.slideCount || inferSlideCount(deck.absoluteFile),
36
35
  outputPath: deck.file,
37
36
  theme: {
38
37
  design: existing?.theme?.design || safeActiveDesign(),
@@ -42,7 +41,6 @@ export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDec
42
41
  ...existing?.requiredInputs,
43
42
  topicClarified: true,
44
43
  audienceClarified: true,
45
- slideCountDecided: true,
46
44
  languageDecided: true,
47
45
  visualStyleSelected: true,
48
46
  sourceMaterialsIdentified: true,
@@ -67,10 +65,6 @@ export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDec
67
65
  }
68
66
  }
69
67
 
70
- function inferSlideCount(filePath: string): number {
71
- return inferSlides(filePath).length
72
- }
73
-
74
68
  function inferSlides(filePath: string): SlideSpec[] {
75
69
  const html = readFileSync(filePath, "utf-8")
76
70
  const chunks = html.match(/<section\b[\s\S]*?<\/section>/gi) || [html]
package/lib/edit/open.ts CHANGED
@@ -78,6 +78,7 @@ function openEditableDeckInternal(
78
78
  const session = editServer.getOrCreateSession({
79
79
  client: options.client,
80
80
  sessionID: options.sessionID,
81
+ workspaceRoot: options.workspaceRoot,
81
82
  deck,
82
83
  })
83
84
  const url = `${editServer.baseUrl}/edit?token=${encodeURIComponent(session.token)}`
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "crypto"
2
- import { readFileSync, statSync } from "fs"
3
- import { resolve, sep } from "path"
2
+ import { existsSync, readFileSync, statSync } from "fs"
3
+ import { fileURLToPath } from "url"
4
+ import { dirname, extname, isAbsolute, resolve, sep } from "path"
4
5
  import type { EditableDeck } from "./resolve-deck"
5
6
  import { buildEditPrompt, type EditCommentPayload } from "./prompt"
6
7
 
@@ -9,6 +10,11 @@ const SESSION_TTL_MS = 2 * 60 * 60 * 1000
9
10
  const IDLE_STOP_MS = 30 * 60 * 1000
10
11
  export const LIVE_EDITOR_IDLE_MS = 10 * 1000
11
12
 
13
+ interface EditAsset {
14
+ id: string
15
+ absoluteFile: string
16
+ }
17
+
12
18
  interface EditSession {
13
19
  token: string
14
20
  client: any
@@ -16,13 +22,17 @@ interface EditSession {
16
22
  deck: string
17
23
  file: string
18
24
  absoluteFile: string
25
+ workspaceRoot: string
26
+ assets: Map<string, EditAsset>
27
+ assetKeys: Map<string, string>
28
+ nextAssetId: number
19
29
  createdAt: number
20
30
  lastActiveAt: number
21
31
  }
22
32
 
23
33
  export interface EditServerHandle {
24
34
  baseUrl: string
25
- getOrCreateSession(input: { client: any; sessionID: string; deck: EditableDeck }): EditServerSessionResult
35
+ getOrCreateSession(input: { client: any; sessionID: string; workspaceRoot: string; deck: EditableDeck }): EditServerSessionResult
26
36
  }
27
37
 
28
38
  export interface EditServerSessionResult {
@@ -57,6 +67,7 @@ export function startEditServer(): EditServerHandle {
57
67
  existing.session.sessionID = input.sessionID
58
68
  existing.session.deck = input.deck.slug
59
69
  existing.session.file = input.deck.file
70
+ existing.session.workspaceRoot = resolve(input.workspaceRoot)
60
71
  return {
61
72
  token: existing.token,
62
73
  reused: true,
@@ -72,6 +83,10 @@ export function startEditServer(): EditServerHandle {
72
83
  deck: input.deck.slug,
73
84
  file: input.deck.file,
74
85
  absoluteFile: input.deck.absoluteFile,
86
+ workspaceRoot: resolve(input.workspaceRoot),
87
+ assets: new Map(),
88
+ assetKeys: new Map(),
89
+ nextAssetId: 1,
75
90
  createdAt: Date.now(),
76
91
  lastActiveAt: Date.now(),
77
92
  })
@@ -131,7 +146,13 @@ async function handleRequest(req: Request): Promise<Response> {
131
146
  if (url.pathname === "/deck" && req.method === "GET") {
132
147
  const session = validateSession(url.searchParams.get("token"))
133
148
  if (!session.ok) return session.response
134
- return htmlResponse(readFileSync(session.value.absoluteFile, "utf-8"))
149
+ return handleDeck(session.value)
150
+ }
151
+
152
+ if (url.pathname === "/__revela_asset" && (req.method === "GET" || req.method === "HEAD")) {
153
+ const session = validateSession(url.searchParams.get("token"))
154
+ if (!session.ok) return session.response
155
+ return handleAsset(session.value, url.searchParams.get("id"), req.method)
135
156
  }
136
157
 
137
158
  if (url.pathname === "/api/comment" && req.method === "POST") {
@@ -149,6 +170,203 @@ async function handleRequest(req: Request): Promise<Response> {
149
170
  return textResponse("Not found", 404)
150
171
  }
151
172
 
173
+ function handleDeck(session: EditSession): Response {
174
+ session.assets.clear()
175
+ session.assetKeys.clear()
176
+ session.nextAssetId = 1
177
+ const html = readFileSync(session.absoluteFile, "utf-8")
178
+ return htmlResponse(rewriteLocalAssetRefs(html, {
179
+ session,
180
+ sourceFile: session.absoluteFile,
181
+ contentType: "html",
182
+ }))
183
+ }
184
+
185
+ function handleAsset(session: EditSession, id: string | null, method: string): Response {
186
+ if (!id) return textResponse("Missing asset id", 400)
187
+ const asset = session.assets.get(id)
188
+ if (!asset) return textResponse("Asset not found", 404)
189
+ if (!existsSync(asset.absoluteFile)) return textResponse("Asset file not found", 404)
190
+ if (!statSync(asset.absoluteFile).isFile()) return textResponse("Asset is not a file", 404)
191
+
192
+ const mime = mimeTypeForPath(asset.absoluteFile)
193
+ const headers = {
194
+ "content-type": mime,
195
+ "cache-control": "no-store, max-age=0",
196
+ }
197
+ if (method === "HEAD") return new Response(null, { status: 200, headers })
198
+
199
+ if (mime === "text/css") {
200
+ const css = readFileSync(asset.absoluteFile, "utf-8")
201
+ return new Response(rewriteLocalAssetRefs(css, {
202
+ session,
203
+ sourceFile: asset.absoluteFile,
204
+ contentType: "css",
205
+ }), { status: 200, headers })
206
+ }
207
+
208
+ return new Response(new Uint8Array(readFileSync(asset.absoluteFile)), { status: 200, headers })
209
+ }
210
+
211
+ function rewriteLocalAssetRefs(content: string, input: { session: EditSession; sourceFile: string; contentType: "html" | "css" }): string {
212
+ const baseDir = dirname(input.sourceFile)
213
+ let rewritten = rewriteCssUrls(content, input.session, baseDir)
214
+ if (input.contentType === "css") return rewritten
215
+
216
+ rewritten = rewriteHtmlAssetAttributes(rewritten, input.session, baseDir)
217
+ rewritten = rewriteSrcsetAttributes(rewritten, input.session, baseDir)
218
+ return rewritten
219
+ }
220
+
221
+ function rewriteHtmlAssetAttributes(html: string, session: EditSession, baseDir: string): string {
222
+ const attrPattern = /\b(src|href|poster)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi
223
+ return html.replace(attrPattern, (match, name: string, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => {
224
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? ""
225
+ const assetUrl = assetUrlForRef(value, session, baseDir)
226
+ if (!assetUrl) return match
227
+ const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : ""
228
+ const escaped = quote ? assetUrl.replace(/&/g, "&amp;") : assetUrl
229
+ return `${name}=${quote}${escaped}${quote}`
230
+ })
231
+ }
232
+
233
+ function rewriteSrcsetAttributes(html: string, session: EditSession, baseDir: string): string {
234
+ const srcsetPattern = /\bsrcset\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/gi
235
+ return html.replace(srcsetPattern, (match, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => {
236
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? ""
237
+ const rewritten = rewriteSrcset(value, session, baseDir)
238
+ if (rewritten === value) return match
239
+ const quote = doubleQuoted !== undefined ? '"' : singleQuoted !== undefined ? "'" : ""
240
+ const escaped = quote ? rewritten.replace(/&/g, "&amp;") : rewritten
241
+ return `srcset=${quote}${escaped}${quote}`
242
+ })
243
+ }
244
+
245
+ function rewriteSrcset(value: string, session: EditSession, baseDir: string): string {
246
+ return value.split(",").map((part) => {
247
+ const trimmed = part.trim()
248
+ if (!trimmed) return part
249
+ const pieces = trimmed.split(/\s+/)
250
+ const assetUrl = assetUrlForRef(pieces[0], session, baseDir)
251
+ if (!assetUrl) return part
252
+ return [assetUrl, ...pieces.slice(1)].join(" ")
253
+ }).join(", ")
254
+ }
255
+
256
+ function rewriteCssUrls(content: string, session: EditSession, baseDir: string): string {
257
+ const cssUrlPattern = /url\(\s*("([^"]*)"|'([^']*)'|([^\s)]+))\s*\)/gi
258
+ return content.replace(cssUrlPattern, (match, raw: string, doubleQuoted?: string, singleQuoted?: string, unquoted?: string) => {
259
+ const value = doubleQuoted ?? singleQuoted ?? unquoted ?? ""
260
+ const assetUrl = assetUrlForRef(value, session, baseDir)
261
+ if (!assetUrl) return match
262
+ return `url("${assetUrl.replace(/"/g, "%22")}")`
263
+ })
264
+ }
265
+
266
+ function assetUrlForRef(ref: string, session: EditSession, baseDir: string): string | null {
267
+ const absoluteFile = resolveLocalAssetRef(ref, session.workspaceRoot, baseDir)
268
+ if (!absoluteFile || !existsSync(absoluteFile) || !statSync(absoluteFile).isFile()) return null
269
+ const key = resolve(absoluteFile)
270
+ let id = session.assetKeys.get(key)
271
+ if (!id) {
272
+ id = String(session.nextAssetId++)
273
+ session.assetKeys.set(key, id)
274
+ session.assets.set(id, { id, absoluteFile: key })
275
+ }
276
+ return `/__revela_asset?token=${encodeURIComponent(session.token)}&id=${encodeURIComponent(id)}`
277
+ }
278
+
279
+ function resolveLocalAssetRef(ref: string, workspaceRoot: string, baseDir: string): string | null {
280
+ const trimmed = ref.trim()
281
+ if (!trimmed || isSkippedAssetRef(trimmed)) return null
282
+
283
+ const pathPart = stripQueryAndHash(trimmed)
284
+ if (!pathPart) return null
285
+
286
+ if (pathPart.startsWith("file://")) {
287
+ try {
288
+ return resolve(fileURLToPath(pathPart))
289
+ } catch {
290
+ return null
291
+ }
292
+ }
293
+
294
+ const decodedPath = safeDecodeUri(pathPart)
295
+ if (isWindowsAbsolutePath(decodedPath)) return resolve(decodedPath)
296
+
297
+ if (isAbsolute(decodedPath)) {
298
+ const absolute = resolve(decodedPath)
299
+ if (existsSync(absolute)) return absolute
300
+ return resolve(workspaceRoot, `.${decodedPath}`)
301
+ }
302
+
303
+ return resolve(baseDir, decodedPath)
304
+ }
305
+
306
+ function stripQueryAndHash(ref: string): string {
307
+ const hashIndex = ref.indexOf("#")
308
+ const withoutHash = hashIndex >= 0 ? ref.slice(0, hashIndex) : ref
309
+ const queryIndex = withoutHash.indexOf("?")
310
+ return queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash
311
+ }
312
+
313
+ function safeDecodeUri(value: string): string {
314
+ try {
315
+ return decodeURI(value)
316
+ } catch {
317
+ return value
318
+ }
319
+ }
320
+
321
+ function isSkippedAssetRef(ref: string): boolean {
322
+ return /^(?:https?:|data:|blob:|mailto:|tel:|javascript:|#)/i.test(ref) || ref.startsWith("//")
323
+ }
324
+
325
+ function isWindowsAbsolutePath(value: string): boolean {
326
+ return /^[a-zA-Z]:[\\/]/.test(value)
327
+ }
328
+
329
+ function mimeTypeForPath(filePath: string): string {
330
+ switch (extname(filePath).toLowerCase()) {
331
+ case ".html":
332
+ case ".htm":
333
+ return "text/html; charset=utf-8"
334
+ case ".css":
335
+ return "text/css"
336
+ case ".js":
337
+ return "application/javascript"
338
+ case ".jpg":
339
+ case ".jpeg":
340
+ return "image/jpeg"
341
+ case ".png":
342
+ return "image/png"
343
+ case ".gif":
344
+ return "image/gif"
345
+ case ".webp":
346
+ return "image/webp"
347
+ case ".svg":
348
+ return "image/svg+xml"
349
+ case ".woff":
350
+ return "font/woff"
351
+ case ".woff2":
352
+ return "font/woff2"
353
+ case ".ttf":
354
+ return "font/ttf"
355
+ case ".otf":
356
+ return "font/otf"
357
+ case ".mp4":
358
+ return "video/mp4"
359
+ case ".webm":
360
+ return "video/webm"
361
+ case ".mp3":
362
+ return "audio/mpeg"
363
+ case ".wav":
364
+ return "audio/wav"
365
+ default:
366
+ return "application/octet-stream"
367
+ }
368
+ }
369
+
152
370
  function handleDeckVersion(session: EditSession): Response {
153
371
  try {
154
372
  const version = readDeckVersion(session)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/skill/SKILL.md CHANGED
@@ -23,7 +23,7 @@ Before writing any HTML, ask the user these questions **in a single message**
23
23
 
24
24
  1. **Topic** — What is the presentation about?
25
25
  2. **Audience** — Who will see it? (e.g. investors, team, conference, class)
26
- 3. **Slide count** — How many slides? (suggest 6–10 if unsure)
26
+ 3. **Scope** — How broad should the deck be? If the user has a preferred length, treat it as guidance only.
27
27
  4. **Language** — What language should the slides be in?
28
28
  5. **Reference materials** — Do you have any reference files to draw content from?
29
29
  (PDF research reports, Excel data, Word documents, PowerPoint decks, images)
@@ -43,7 +43,7 @@ research or writing HTML. The brief should capture:
43
43
  - working thesis or angle, if one has emerged
44
44
  - key questions the deck must answer
45
45
  - known workspace sources from `DECKS.json`, user attachments, or visible files
46
- - desired output shape, slide count, and visual direction
46
+ - desired output shape, approximate scope, and visual direction
47
47
 
48
48
  If the brief is unclear, ask 1–3 targeted clarification questions. Do not force
49
49
  the user to provide a research topic command; the working topic emerges from the
@@ -82,7 +82,7 @@ All subsequent file paths in this session use the current workspace deck:
82
82
  Create or update the active deck in `DECKS.json` through `revela-decks` actions
83
83
  `upsertDeck` and `upsertSlides`. Keep the deck spec current as work progresses:
84
84
  - `goal` — purpose and decision/context
85
- - `audience`, `language`, `slideCount`, `outputPath`, and `theme`
85
+ - `audience`, `language`, `outputPath`, and `theme`
86
86
  - `requiredInputs` — checklist state for prewrite readiness
87
87
  - `researchPlan` — axes, status, and findings files
88
88
  - `slides` — confirmed per-slide title, purpose, layout, components, content, evidence, visuals, and status
@@ -227,7 +227,7 @@ The exact visual style for each section comes from the active design.
227
227
  When the user asks for N slides, distribute them across these sections.
228
228
  A 6-slide deck might be: Cover → Background → Content × 3 → Closing.
229
229
  An 8-slide deck might be: Cover → TOC → Background → Content × 3 → Summary → Closing.
230
- Never skip Cover, Background, or Closing regardless of slide count.
230
+ Never skip Cover, Background, or Closing regardless of deck length.
231
231
 
232
232
  **Every `<section class="slide">` must include a `slide-qa` attribute.** Set
233
233
  `slide-qa="true"` for content-heavy layouts (those marked ✓ in the Layout Index
@@ -247,7 +247,7 @@ core rules and the visual design below.
247
247
 
248
248
  **When a domain definition is present:**
249
249
  - Follow its report structure instead of the default "Required Slide Structure" above.
250
- The domain defines its own sections, ordering, and slide count distribution.
250
+ The domain defines its own sections, ordering, and deck length guidance.
251
251
  - Follow its AI logic rules (e.g. terminology, evidence standards, risk frameworks).
252
252
  - The domain's visual preferences are **suggestions only** — the active Design's
253
253
  visual rules always take precedence for colors, fonts, animations, and layout.
package/tools/decks.ts CHANGED
@@ -28,7 +28,6 @@ export default tool({
28
28
  goal: tool.schema.string().optional().describe("For upsertDeck: deck goal."),
29
29
  audience: tool.schema.string().optional().describe("For upsertDeck: deck audience."),
30
30
  language: tool.schema.string().optional().describe("For upsertDeck: deck language."),
31
- slideCount: tool.schema.number().optional().describe("For upsertDeck: expected slide count."),
32
31
  outputPath: tool.schema.string().optional().describe("For upsertDeck: target output path, normally decks/{workspace-name}.html."),
33
32
  design: tool.schema.string().optional().describe("For upsertDeck: active design name."),
34
33
  domain: tool.schema.string().optional().describe("For upsertDeck: active domain name."),
@@ -37,7 +36,6 @@ export default tool({
37
36
  requiredInputs: tool.schema.object({
38
37
  topicClarified: tool.schema.boolean().optional(),
39
38
  audienceClarified: tool.schema.boolean().optional(),
40
- slideCountDecided: tool.schema.boolean().optional(),
41
39
  languageDecided: tool.schema.boolean().optional(),
42
40
  visualStyleSelected: tool.schema.boolean().optional(),
43
41
  sourceMaterialsIdentified: tool.schema.boolean().optional(),
@@ -111,7 +109,6 @@ export default tool({
111
109
  goal: args.goal ?? existing?.goal ?? "",
112
110
  audience: args.audience ?? existing?.audience,
113
111
  language: args.language ?? existing?.language,
114
- slideCount: args.slideCount ?? existing?.slideCount,
115
112
  outputPath: args.outputPath ?? existing?.outputPath,
116
113
  theme: {
117
114
  design: args.design ?? existing?.theme?.design,