@cyber-dash-tech/revela 0.8.6 → 0.8.8

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
@@ -209,6 +209,7 @@ Minimum readiness conditions:
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
212
+ - evidence-sensitive claims have slide-level evidence with useful source trace when available, such as `findingsFile`, `sourcePath`, `location`, `quote`, `url`, or `caveat`
212
213
  - the user has confirmed the slide plan
213
214
  - required design layouts and components have been fetched
214
215
  - every slide has a title, layout, components, and structured content
@@ -225,6 +226,8 @@ The gate checks:
225
226
  - every slide has title, layout, components, and structured content
226
227
  - every needed research axis is `done`, `read`, or `skipped`
227
228
 
229
+ `/revela review` distinguishes missing evidence from weak source-only evidence. A claim with no evidence remains a blocker; evidence that only names a source is reported as a warning so the agent can add source trace before writing the deck.
230
+
228
231
  If the gate blocks a write, Revela writes a marker file under `.opencode/revela/blocked-writes/` instead of creating or overwriting the deck HTML. This makes the failure visible to the agent while keeping the real deck file untouched.
229
232
 
230
233
  For `apply_patch`, Revela only checks whether the patch touches `decks/*.html`. If not ready, the whole patch is replaced with a blocked marker patch. The `edit` tool is not gated.
package/README.zh-CN.md CHANGED
@@ -208,6 +208,7 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
208
208
  - source materials 已识别,或明确不需要源材料
209
209
  - research need 已评估
210
210
  - 需要调研时,相关 findings 已读取并反映到逐页规格中
211
+ - evidence-sensitive claim 尽量带有逐页 evidence 和可用 source trace,例如 `findingsFile`、`sourcePath`、`location`、`quote`、`url` 或 `caveat`
211
212
  - 用户已确认 slide plan
212
213
  - 需要的 design layouts 和 components 已获取
213
214
  - 每页都有 title、layout、components 和结构化 content
@@ -224,6 +225,8 @@ Revela 使用工作区根目录的 `DECKS.json` 做跨会话记忆和 deck 生
224
225
  - 每页都有 title、layout、components 和结构化 content
225
226
  - 每个 needed research axis 都是 `done`、`read` 或 `skipped`
226
227
 
228
+ `/revela review` 会区分缺失 evidence 和较弱的 source-only evidence。完全没有 evidence 的 claim 仍是 blocker;只写了 source 名称但缺少 source trace 的 evidence 会作为 warning 报告,方便 agent 在写 deck 前补足溯源信息。
229
+
227
230
  如果门禁阻止写入,Revela 会在 `.opencode/revela/blocked-writes/` 下写一个 marker 文件,而不是创建或覆盖真实 HTML deck。这样 agent 能看到失败原因,同时真实 deck 文件不会被污染。
228
231
 
229
232
  对 `apply_patch`,Revela 只检查 patch 是否触碰 `decks/*.html`。如果未 ready,整个 patch 会被替换成 blocked marker patch。`edit` 工具不做门禁。
@@ -29,7 +29,7 @@ Given a research brief specifying your topic and axis, you will:
29
29
  2. Use \`DECKS.json\` through \`revela-decks\` as the workspace material index when it exists
30
30
  3. Run a lightweight workspace freshness check when needed
31
31
  4. Search the web for current data, reports, and case studies when the brief requires it
32
- 5. Write all findings to ONE structured file: \`researches/{topic-key}/{axis-name}.md\`
32
+ 5. Write all findings to ONE structured file: \`researches/{topic-key}/{axis-name}.md\` with source trace detailed enough for slide-level evidence mapping
33
33
  6. Return a brief summary of what you found
34
34
 
35
35
  ---
@@ -112,18 +112,21 @@ Use **\`revela-research-save\`** to write ONE file with all your findings.
112
112
  - \`content\`: structured findings using the four sections below
113
113
  - \`sources\`: list of all URLs and filenames used
114
114
 
115
+ The primary agent will map your findings into \`DECKS.json\` slide-level evidence.
116
+ Preserve compact source trace so it can do that without rediscovering sources.
117
+
115
118
  ### Findings file format
116
119
 
117
120
  Use these four sections — omit any that are empty:
118
121
 
119
122
  \`\`\`markdown
120
123
  ## Data
121
- - {stat or finding} [Source: {url or filename}]
122
- - {stat or finding} [Source: {url or filename}]
124
+ - {stat or finding} [Source: {url or filename}; Location: {page/slide/sheet/section if known}; Quote: "{short exact snippet if available}"; Caveat: {scope/uncertainty if relevant}]
125
+ - {stat or finding} [Source: {url or filename}; Location: {page/slide/sheet/section if known}; Quote: "{short exact snippet if available}"; Caveat: {scope/uncertainty if relevant}]
123
126
  (5–10 items, most argument-worthy only)
124
127
 
125
128
  ## Cases
126
- - **{Company/Entity}**: {1–2 sentence profile with key metrics} [Source: {url}]
129
+ - **{Company/Entity}**: {1–2 sentence profile with key metrics} [Source: {url or filename}; Location: {page/slide/sheet/section if known}; Caveat: {scope/uncertainty if relevant}]
127
130
  (2–4 entries max)
128
131
 
129
132
  ## Images
@@ -136,6 +139,10 @@ Use these four sections — omit any that are empty:
136
139
 
137
140
  Content rules:
138
141
  - Every data point MUST have inline source attribution: \`[Source: {url}]\` or \`[Source: AI knowledge — verify]\` or \`[Source: {filename}]\`
142
+ - For workspace documents, identify the original filename and available page, slide, sheet, or section location. Do not cite only the extracted summary.
143
+ - When extracted materials were used, include \`extractedTextPath\` or \`extractedManifestPath\` when useful for traceability.
144
+ - Preserve compact direct snippets or quotes when available. Do not invent quotes, page references, locations, URLs, or caveats.
145
+ - Include caveats or scope limitations for estimates, rankings, market sizes, forecasts, and conflicting sources.
139
146
  - Preserve raw numbers and direct quotes — do not summarize prematurely
140
147
  - Use tables for comparative data when 3+ entities are compared
141
148
  - Include publication dates where available
@@ -170,6 +177,7 @@ Gaps:
170
177
  - **Always** call \`revela-extract-document-materials\` for every selected workspace file before deciding which extracted materials to read next
171
178
  - **Avoid** repeated extraction or deep reading for files that are clearly irrelevant to this axis
172
179
  - **Always** include source attribution on every data point
180
+ - **Always** preserve source trace: URL or filename, location when available, compact quote/snippet when available, and caveat/scope where relevant
173
181
  - **Always** use tables for comparative data (more useful than bullets for presentations)
174
182
  - **Preserve** raw data — the primary agent will select what to include in slides
175
183
  - **Note** data freshness — include publication dates where available
@@ -48,8 +48,10 @@ Workflow:
48
48
  6. Before extracting or deeply reading a selected document, check \`DECKS.json.workspace.sourceMaterials\`. If the same path has the same fingerprint and valid extraction paths, reuse those paths instead of repeating extraction.
49
49
  7. Read only the materials needed to form a conservative workspace memory. Do not exhaustively read every file if the workspace is large.
50
50
  8. 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.
51
- 9. 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.
52
- 10. Report what was initialized or updated and list any open questions.
51
+ 9. When adopting an existing HTML deck, analyze the artifact and create one conservative \`SlideSpec\` per identifiable slide/page. Record only visible source notes or explicit source information as evidence; do not infer original evidence that is not present in the artifact.
52
+ 10. When a read or extracted source material clearly supports a specific slide claim, you may attach compact evidence fields such as \`sourcePath\`, \`location\`, \`extractedTextPath\`, or \`extractedManifestPath\`. Attach extraction cache paths only when they support that specific claim, not to every slide by default.
53
+ 11. Treat \`workspace.sourceMaterials\` as a reusable candidate index, not proof by itself. A source material record alone is not slide evidence.
54
+ 12. Report what was initialized or updated and list any open questions.
53
55
 
54
56
  Memory rules:
55
57
  - Only write facts supported by workspace files into ${DECKS_STATE_FILE} workspace state, source materials, deck memory, and open questions.
@@ -18,6 +18,8 @@ Goal:
18
18
  - Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, and production status.
19
19
  - Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
20
20
  - Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
21
+ - Treat this as an evidence-readiness review, not only a checklist review: unsupported numbers, market sizing, recommendations, competitor comparisons, technical assertions, or investment conclusions should be made visible before writing.
22
+ - Treat source trace mapping as part of evidence readiness: when research findings have been read, relevant findings should appear in slide-level \`slides[].evidence[]\` records rather than only in raw research files.
21
23
 
22
24
  Current state:
23
25
  - ${state}
@@ -32,21 +34,33 @@ Workspace boundary rules:
32
34
  Workflow:
33
35
  1. Call \`revela-decks\` with action \`read\` for the current workspace deck.
34
36
  2. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, and researchPlan. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
35
- 3. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, layout, components, structured content, evidence, visuals, and status.
36
- 4. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
37
- 5. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` for the current workspace deck.
38
- 6. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool.
37
+ 3. If \`researchPlan[].status\` is \`done\` or \`read\` and \`researchPlan[].findingsFile\` exists, verify that evidence-sensitive slide claims are backed by compact \`slides[].evidence[]\` records that reference the relevant findings file or source material where known.
38
+ 4. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, layout, components, structured content, evidence, visuals, and status.
39
+ 5. Prefer evidence records with \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, \`caveat\`, \`extractedTextPath\`, or \`extractedManifestPath\` when those fields are known from research files or extracted workspace materials.
40
+ 6. Do not invent quotes, page references, locations, URLs, caveats, or extraction paths. If source trace is missing, preserve the blocker or warning and report exactly what trace is needed.
41
+ 7. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
42
+ 8. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
43
+ 9. Briefly report whether the deck is ready. If blocked, list the exact blockers returned by the tool. If warnings exist, list them after blockers as residual risks.
39
44
 
40
45
  Minimum conditions for \`ready\`:
41
46
  - Topic, audience, slide count, language, and visual style/design are decided.
42
47
  - Source materials have been identified or explicitly deemed unnecessary.
43
48
  - Research need has been assessed.
44
49
  - If research is needed, all relevant findings have been read and reflected in the slide specs.
50
+ - Read or done research findings are mapped into \`slides[].evidence[]\` where they support evidence-sensitive slide claims.
45
51
  - The user has confirmed the slide plan.
46
52
  - ${DECKS_STATE_FILE} contains per-slide specs with content, layout, components, and evidence where applicable.
53
+ - Evidence-sensitive slide claims have compact evidence references with source trace where available. Numeric claims and strong recommendations should not be unsupported or source-only when trace exists.
47
54
  - The needed design layouts and components have been fetched with \`revela-designs read\`.
48
55
  - No unresolved blockers remain.
49
56
 
57
+ Report format:
58
+ - Start with \`Ready: yes/no\`.
59
+ - If blocked, list each blocker with slide index/title when the tool provides it, the issue type, and the suggested next action.
60
+ - If warnings exist but the deck is otherwise ready, say the deck can be written but note the residual risks.
61
+ - Do not invent evidence or silently downgrade blockers. Use the tool result as authoritative.
62
+ - When reporting weak evidence, say whether the missing trace is \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, or \`caveat\` if that is clear from the reviewed materials.
63
+
50
64
  Rules:
51
65
  - Do not write or overwrite \`decks/*.html\` during review.
52
66
  - Treat the workspace as one deck project. If the user wants another deck, tell them to use a separate workspace/folder.
@@ -117,6 +117,12 @@ export interface EvidenceRef {
117
117
  quote?: string
118
118
  page?: string
119
119
  url?: string
120
+ sourcePath?: string
121
+ location?: string
122
+ findingsFile?: string
123
+ caveat?: string
124
+ extractedTextPath?: string
125
+ extractedManifestPath?: string
120
126
  }
121
127
 
122
128
  export interface VisualBrief {
@@ -140,8 +146,32 @@ export interface DeckStateReadinessResult {
140
146
  status?: WriteReadinessStatus
141
147
  blocker: string
142
148
  blockers: string[]
149
+ warnings: string[]
150
+ issues: ReadinessIssue[]
151
+ }
152
+
153
+ export type ReadinessSeverity = "blocker" | "warning"
154
+
155
+ export type ReadinessIssueType =
156
+ | "missing_required_input"
157
+ | "missing_slide_spec"
158
+ | "research_not_ready"
159
+ | "missing_evidence"
160
+ | "weak_evidence"
161
+ | "source_not_processed"
162
+
163
+ export interface ReadinessIssue {
164
+ type: ReadinessIssueType
165
+ severity: ReadinessSeverity
166
+ message: string
167
+ suggestedAction: string
168
+ slideIndex?: number
169
+ slideTitle?: string
170
+ claimText?: string
143
171
  }
144
172
 
173
+ const SOURCE_TRACE_ACTION = "Add slide evidence with source plus source trace such as findingsFile or sourcePath, and quote, location, url, or caveat where available; otherwise reframe the claim as an explicit assumption/opinion."
174
+
145
175
  export function decksStatePath(workspaceRoot: string): string {
146
176
  return join(workspaceRoot, DECKS_STATE_FILE)
147
177
  }
@@ -279,11 +309,20 @@ export function reviewDeckState(state: DecksState, slug?: string): { state: Deck
279
309
  slug: missing,
280
310
  blocker: `Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`,
281
311
  blockers: [`Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`],
312
+ warnings: [],
313
+ issues: [{
314
+ type: "missing_slide_spec",
315
+ severity: "blocker",
316
+ message: `Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`,
317
+ suggestedAction: "Create the current workspace deck spec with revela-decks upsertDeck before reviewing readiness.",
318
+ }],
282
319
  },
283
320
  }
284
321
  }
285
322
 
286
- const blockers = computeDeckBlockers(deck)
323
+ const issues = computeDeckReadinessIssues(deck, normalized.workspace)
324
+ const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
325
+ const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
287
326
  deck.writeReadiness = {
288
327
  status: blockers.length === 0 ? "ready" : "blocked",
289
328
  blockers,
@@ -300,6 +339,8 @@ export function reviewDeckState(state: DecksState, slug?: string): { state: Deck
300
339
  status: deck.writeReadiness.status,
301
340
  blocker: blockers.join("; "),
302
341
  blockers,
342
+ warnings,
343
+ issues,
303
344
  },
304
345
  }
305
346
  }
@@ -321,18 +362,48 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
321
362
  slug: targetSlug,
322
363
  blocker: currentDeckBlocker(normalized),
323
364
  blockers: [currentDeckBlocker(normalized)],
365
+ warnings: [],
366
+ issues: [{
367
+ type: "missing_slide_spec",
368
+ severity: "blocker",
369
+ message: currentDeckBlocker(normalized),
370
+ suggestedAction: "Create or select the current workspace deck through revela-decks before writing deck HTML.",
371
+ }],
324
372
  }
325
373
  }
326
374
 
327
- const blockers = computeDeckBlockers(deck)
375
+ const issues = computeDeckReadinessIssues(deck, normalized.workspace)
376
+ const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
377
+ const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
328
378
  if (normalizeDeckPath(deck.outputPath) !== targetPath) {
329
- blockers.unshift(`Deck outputPath is ${deck.outputPath || "missing"}, not ${targetPath}`)
379
+ const message = `Deck outputPath is ${deck.outputPath || "missing"}, not ${targetPath}`
380
+ blockers.unshift(message)
381
+ issues.unshift({
382
+ type: "missing_slide_spec",
383
+ severity: "blocker",
384
+ message,
385
+ suggestedAction: "Update deck.outputPath through revela-decks or write to the reviewed outputPath.",
386
+ })
330
387
  }
331
388
  if (deck.writeReadiness.status !== "ready") {
332
- blockers.unshift(`Deck writeReadiness is ${deck.writeReadiness.status || "missing"}, not ready`)
389
+ const message = `Deck writeReadiness is ${deck.writeReadiness.status || "missing"}, not ready`
390
+ blockers.unshift(message)
391
+ issues.unshift({
392
+ type: "missing_slide_spec",
393
+ severity: "blocker",
394
+ message,
395
+ suggestedAction: "Run /revela review and resolve all readiness blockers before writing deck HTML.",
396
+ })
333
397
  }
334
398
  if (deck.writeReadiness.blockers.length > 0) {
335
- blockers.unshift(`Deck still has readiness blockers: ${deck.writeReadiness.blockers.join("; ")}`)
399
+ const message = `Deck still has readiness blockers: ${deck.writeReadiness.blockers.join("; ")}`
400
+ blockers.unshift(message)
401
+ issues.unshift({
402
+ type: "missing_slide_spec",
403
+ severity: "blocker",
404
+ message,
405
+ suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
406
+ })
336
407
  }
337
408
 
338
409
  return {
@@ -341,6 +412,8 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
341
412
  status: deck.writeReadiness.status,
342
413
  blocker: blockers.join("; "),
343
414
  blockers,
415
+ warnings,
416
+ issues,
344
417
  }
345
418
  }
346
419
 
@@ -367,14 +440,58 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
367
440
  const compact = {
368
441
  sourceOfTruth: DECKS_STATE_FILE,
369
442
  activeDeck: activeKey,
370
- workspace: state.workspace,
371
- deck: active,
443
+ workspace: compactWorkspaceForPrompt(state.workspace),
444
+ deck: active ? compactDeckForPrompt(active) : undefined,
372
445
  }
373
446
  let text = JSON.stringify(compact, null, 2)
374
447
  if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
375
448
  return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
376
449
  }
377
450
 
451
+ function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
452
+ return {
453
+ brief: truncatePromptText(workspace.brief),
454
+ sourceMaterials: workspace.sourceMaterials.map((source) => ({
455
+ ...source,
456
+ summary: truncatePromptText(source.summary),
457
+ bestUsedFor: truncatePromptText(source.bestUsedFor),
458
+ })),
459
+ preferences: workspace.preferences,
460
+ deckMemory: workspace.deckMemory,
461
+ openQuestions: workspace.openQuestions.map((question) => truncatePromptText(question)).filter(Boolean) as string[],
462
+ }
463
+ }
464
+
465
+ function compactDeckForPrompt(deck: DeckSpec): DeckSpec {
466
+ return {
467
+ ...deck,
468
+ slides: deck.slides.map((slide) => ({
469
+ ...slide,
470
+ content: {
471
+ ...slide.content,
472
+ speakerNotes: truncatePromptText(slide.content.speakerNotes),
473
+ },
474
+ evidence: slide.evidence.map(compactEvidenceForPrompt),
475
+ notes: truncatePromptText(slide.notes),
476
+ })),
477
+ }
478
+ }
479
+
480
+ function compactEvidenceForPrompt(evidence: EvidenceRef): EvidenceRef {
481
+ return {
482
+ ...evidence,
483
+ source: truncatePromptText(evidence.source, 180) ?? evidence.source,
484
+ quote: truncatePromptText(evidence.quote, 320),
485
+ caveat: truncatePromptText(evidence.caveat, 220),
486
+ }
487
+ }
488
+
489
+ function truncatePromptText(text: string | undefined, maxLength = 400): string | undefined {
490
+ if (!text) return undefined
491
+ if (text.length <= maxLength) return text
492
+ return `${text.slice(0, maxLength).trimEnd()}... [truncated]`
493
+ }
494
+
378
495
  function normalizeDecksState(input: DecksState): DecksState {
379
496
  const state: DecksState = {
380
497
  version: 1,
@@ -416,31 +533,170 @@ function currentDeckBlocker(state: DecksState): string {
416
533
  return `${DECKS_STATE_FILE} contains multiple deck records and no activeDeck. Select one current deck explicitly or move extra decks to separate workspaces.`
417
534
  }
418
535
 
419
- function computeDeckBlockers(deck: DeckSpec): string[] {
420
- const blockers: string[] = []
421
- if (!deck.goal.trim()) blockers.push("Deck goal is missing")
422
- if (!isDeckHtmlPath(deck.outputPath)) blockers.push(`outputPath must be decks/*.html, got ${deck.outputPath || "missing"}`)
536
+ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["workspace"]): ReadinessIssue[] {
537
+ const issues: ReadinessIssue[] = []
538
+ if (!deck.goal.trim()) issues.push(blockerIssue("missing_slide_spec", "Deck goal is missing", "Set the deck goal through revela-decks upsertDeck."))
539
+ if (!isDeckHtmlPath(deck.outputPath)) {
540
+ issues.push(blockerIssue(
541
+ "missing_slide_spec",
542
+ `outputPath must be decks/*.html, got ${deck.outputPath || "missing"}`,
543
+ "Set outputPath to the target decks/*.html file through revela-decks upsertDeck.",
544
+ ))
545
+ }
423
546
 
424
547
  for (const [key, value] of Object.entries(deck.requiredInputs) as Array<[keyof RequiredInputs, boolean]>) {
425
- if (value !== true) blockers.push(`requiredInputs.${key} is not true`)
548
+ if (value !== true) {
549
+ issues.push(blockerIssue(
550
+ "missing_required_input",
551
+ `requiredInputs.${key} is not true`,
552
+ `Complete and explicitly record requiredInputs.${key} before writing the deck.`,
553
+ ))
554
+ }
426
555
  }
427
556
 
428
- if (deck.slides.length === 0) blockers.push("slides are missing")
557
+ if (deck.slides.length === 0) issues.push(blockerIssue("missing_slide_spec", "slides are missing", "Add the confirmed slide plan through revela-decks upsertSlides."))
429
558
  for (const slide of deck.slides) {
430
- if (!slide.title.trim()) blockers.push(`Slide ${slide.index} title is missing`)
431
- if (!slide.layout.trim()) blockers.push(`Slide ${slide.index} layout is missing`)
432
- if (slide.components.length === 0) blockers.push(`Slide ${slide.index} components are missing`)
433
- if (!hasSlideContent(slide)) blockers.push(`Slide ${slide.index} content is missing`)
559
+ const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
560
+ if (!slide.title.trim()) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} title is missing`, "Add a slide title to the slide spec.", slideRef))
561
+ if (!slide.layout.trim()) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} layout is missing`, "Fetch and record the intended design layout for this slide.", slideRef))
562
+ if (slide.components.length === 0) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} components are missing`, "Record the design components needed for this slide.", slideRef))
563
+ if (!hasSlideContent(slide)) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} content is missing`, "Add structured headline/body/bullets/data content to the slide spec.", slideRef))
564
+
565
+ const claim = findEvidenceSensitiveClaim(slide)
566
+ if (claim && slide.evidence.length === 0) {
567
+ issues.push(blockerIssue(
568
+ "missing_evidence",
569
+ `Slide ${slide.index} has an evidence-sensitive claim without evidence: ${claim}`,
570
+ SOURCE_TRACE_ACTION,
571
+ { ...slideRef, claimText: claim },
572
+ ))
573
+ } else if (claim && slide.evidence.some((item) => !hasEvidenceDetail(item))) {
574
+ issues.push(warningIssue(
575
+ "weak_evidence",
576
+ `Slide ${slide.index} evidence for a high-risk claim has no source trace detail: ${claim}`,
577
+ "Add source trace detail to this evidence record: findingsFile or sourcePath plus quote, location, url, or caveat where available so the writing agent can ground the slide reliably.",
578
+ { ...slideRef, claimText: claim },
579
+ ))
580
+ }
434
581
  }
435
582
 
436
583
  for (const axis of deck.researchPlan) {
437
584
  if (axis.needed && axis.status !== "done" && axis.status !== "read" && axis.status !== "skipped") {
438
- blockers.push(`Research axis ${axis.axis || "unnamed"} is needed but ${axis.status}`)
585
+ issues.push(blockerIssue(
586
+ "research_not_ready",
587
+ `Research axis ${axis.axis || "unnamed"} is needed but ${axis.status}`,
588
+ "Complete, read, or explicitly skip this research axis before writing the deck.",
589
+ ))
590
+ }
591
+ }
592
+
593
+ const hasNeededResearch = deck.researchPlan.some((axis) => axis.needed && axis.status !== "skipped")
594
+ for (const material of workspace.sourceMaterials ?? []) {
595
+ if (material.status !== "discovered") continue
596
+ const message = `Source material ${material.path} has been identified but not extracted, summarized, or researched`
597
+ if (hasNeededResearch) {
598
+ issues.push(blockerIssue(
599
+ "source_not_processed",
600
+ message,
601
+ "Extract, summarize, research, or explicitly exclude this source before writing evidence-backed slides.",
602
+ ))
603
+ } else {
604
+ issues.push(warningIssue(
605
+ "source_not_processed",
606
+ message,
607
+ "Consider extracting or excluding this source if it may support the deck narrative.",
608
+ ))
439
609
  }
440
610
  }
441
- return blockers
611
+
612
+ return issues
613
+ }
614
+
615
+ function blockerIssue(type: ReadinessIssueType, message: string, suggestedAction: string, extra: Partial<ReadinessIssue> = {}): ReadinessIssue {
616
+ return { type, severity: "blocker", message, suggestedAction, ...extra }
617
+ }
618
+
619
+ function warningIssue(type: ReadinessIssueType, message: string, suggestedAction: string, extra: Partial<ReadinessIssue> = {}): ReadinessIssue {
620
+ return { type, severity: "warning", message, suggestedAction, ...extra }
621
+ }
622
+
623
+ function findEvidenceSensitiveClaim(slide: SlideSpec): string | undefined {
624
+ const candidates = [
625
+ slide.title,
626
+ slide.purpose,
627
+ slide.content?.headline,
628
+ ...(slide.content?.body ?? []),
629
+ ...(slide.content?.bullets ?? []),
630
+ ]
631
+ .map((item) => item?.trim())
632
+ .filter((item): item is string => Boolean(item))
633
+
634
+ return candidates.find(isEvidenceSensitiveClaim)
635
+ }
636
+
637
+ function isEvidenceSensitiveClaim(text: string): boolean {
638
+ const normalized = text.toLowerCase()
639
+ return hasNumericClaim(normalized) || EVIDENCE_SENSITIVE_TERMS.some((pattern) => pattern.test(normalized))
640
+ }
641
+
642
+ function hasNumericClaim(text: string): boolean {
643
+ return /(?:[$¥€£]\s?\d|\d+(?:\.\d+)?\s?(?:%|x|倍|万|亿|m|mn|million|b|bn|billion|k|千|年|months?|days?|users?|customers?|revenue|margin|cagr|tam|sam|som)\b|\b20\d{2}\b)/i.test(text)
442
644
  }
443
645
 
646
+ function hasEvidenceDetail(evidence: EvidenceRef): boolean {
647
+ return Boolean(
648
+ evidence.quote?.trim() ||
649
+ evidence.page?.trim() ||
650
+ evidence.location?.trim() ||
651
+ evidence.url?.trim() ||
652
+ evidence.findingsFile?.trim() ||
653
+ evidence.sourcePath?.trim() ||
654
+ evidence.extractedTextPath?.trim()
655
+ )
656
+ }
657
+
658
+ const EVIDENCE_SENSITIVE_TERMS = [
659
+ /\bmarket size\b/,
660
+ /\bcagr\b/,
661
+ /\btam\b/,
662
+ /\bsam\b/,
663
+ /\bsom\b/,
664
+ /\brecommend(?:ation|ed)?\b/,
665
+ /\bshould\b/,
666
+ /\bmust\b/,
667
+ /\bgo\/?no-go\b/,
668
+ /\bvs\.?\b/,
669
+ /\bbetter than\b/,
670
+ /\boutperform\b/,
671
+ /\bleading\b/,
672
+ /\bcompetitor\b/,
673
+ /\bmarket leader\b/,
674
+ /\binvest(?:ment)?\b/,
675
+ /\brevenue\b/,
676
+ /\bmargin\b/,
677
+ /\bcost\b/,
678
+ /\brisk\b/,
679
+ /\blatency\b/,
680
+ /\baccuracy\b/,
681
+ /\bscalable\b/,
682
+ /\barchitecture\b/,
683
+ /市场规模/,
684
+ /增长/,
685
+ /领先/,
686
+ /超过/,
687
+ /竞品/,
688
+ /建议/,
689
+ /必须/,
690
+ /投资/,
691
+ /收入/,
692
+ /利润/,
693
+ /成本/,
694
+ /风险/,
695
+ /性能/,
696
+ /架构/,
697
+ /可扩展/,
698
+ ]
699
+
444
700
  function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
445
701
  return slides
446
702
  .map((slide) => ({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-dash-tech/revela",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
package/tools/decks.ts CHANGED
@@ -86,10 +86,16 @@ export default tool({
86
86
  }).describe("Structured slide content."),
87
87
  evidence: tool.schema.array(tool.schema.object({
88
88
  source: tool.schema.string().describe("Source file, URL, or research note."),
89
- quote: tool.schema.string().optional(),
90
- page: tool.schema.string().optional(),
91
- url: tool.schema.string().optional(),
92
- })).optional().describe("Evidence references for this slide."),
89
+ quote: tool.schema.string().optional().describe("Compact quote or snippet supporting the slide claim."),
90
+ page: tool.schema.string().optional().describe("Legacy page reference; prefer location for new page/slide/sheet/section references."),
91
+ url: tool.schema.string().optional().describe("Source URL when available."),
92
+ sourcePath: tool.schema.string().optional().describe("Workspace source file path when the evidence came from a local material."),
93
+ location: tool.schema.string().optional().describe("Generic page, slide, sheet, section, or other source location reference."),
94
+ findingsFile: tool.schema.string().optional().describe("researches/{topic}/{axis}.md findings file that records the supporting evidence."),
95
+ caveat: tool.schema.string().optional().describe("Scope, uncertainty, or limitation that should travel with this evidence."),
96
+ extractedTextPath: tool.schema.string().optional().describe("Reusable extracted text cache path when this evidence came from extracted materials."),
97
+ extractedManifestPath: tool.schema.string().optional().describe("Reusable extracted materials manifest path when available."),
98
+ })).optional().describe("Compact evidence references and source trace for this slide."),
93
99
  visuals: tool.schema.array(tool.schema.object({
94
100
  id: tool.schema.string().optional(),
95
101
  purpose: tool.schema.string().optional(),