@cyber-dash-tech/revela 0.8.6 → 0.8.7

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.
@@ -18,6 +18,7 @@ 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.
21
22
 
22
23
  Current state:
23
24
  - ${state}
@@ -34,8 +35,8 @@ Workflow:
34
35
  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
36
  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
37
  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.
38
+ 5. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
39
+ 6. 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
40
 
40
41
  Minimum conditions for \`ready\`:
41
42
  - Topic, audience, slide count, language, and visual style/design are decided.
@@ -44,9 +45,16 @@ Minimum conditions for \`ready\`:
44
45
  - If research is needed, all relevant findings have been read and reflected in the slide specs.
45
46
  - The user has confirmed the slide plan.
46
47
  - ${DECKS_STATE_FILE} contains per-slide specs with content, layout, components, and evidence where applicable.
48
+ - Evidence-sensitive slide claims have compact evidence references. Numeric claims and strong recommendations should not be unsupported.
47
49
  - The needed design layouts and components have been fetched with \`revela-designs read\`.
48
50
  - No unresolved blockers remain.
49
51
 
52
+ Report format:
53
+ - Start with \`Ready: yes/no\`.
54
+ - If blocked, list each blocker with slide index/title when the tool provides it, the issue type, and the suggested next action.
55
+ - If warnings exist but the deck is otherwise ready, say the deck can be written but note the residual risks.
56
+ - Do not invent evidence or silently downgrade blockers. Use the tool result as authoritative.
57
+
50
58
  Rules:
51
59
  - Do not write or overwrite \`decks/*.html\` during review.
52
60
  - Treat the workspace as one deck project. If the user wants another deck, tell them to use a separate workspace/folder.
@@ -140,6 +140,28 @@ export interface DeckStateReadinessResult {
140
140
  status?: WriteReadinessStatus
141
141
  blocker: string
142
142
  blockers: string[]
143
+ warnings: string[]
144
+ issues: ReadinessIssue[]
145
+ }
146
+
147
+ export type ReadinessSeverity = "blocker" | "warning"
148
+
149
+ export type ReadinessIssueType =
150
+ | "missing_required_input"
151
+ | "missing_slide_spec"
152
+ | "research_not_ready"
153
+ | "missing_evidence"
154
+ | "weak_evidence"
155
+ | "source_not_processed"
156
+
157
+ export interface ReadinessIssue {
158
+ type: ReadinessIssueType
159
+ severity: ReadinessSeverity
160
+ message: string
161
+ suggestedAction: string
162
+ slideIndex?: number
163
+ slideTitle?: string
164
+ claimText?: string
143
165
  }
144
166
 
145
167
  export function decksStatePath(workspaceRoot: string): string {
@@ -279,11 +301,20 @@ export function reviewDeckState(state: DecksState, slug?: string): { state: Deck
279
301
  slug: missing,
280
302
  blocker: `Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`,
281
303
  blockers: [`Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`],
304
+ warnings: [],
305
+ issues: [{
306
+ type: "missing_slide_spec",
307
+ severity: "blocker",
308
+ message: `Deck ${missing} does not exist in ${DECKS_STATE_FILE}.`,
309
+ suggestedAction: "Create the current workspace deck spec with revela-decks upsertDeck before reviewing readiness.",
310
+ }],
282
311
  },
283
312
  }
284
313
  }
285
314
 
286
- const blockers = computeDeckBlockers(deck)
315
+ const issues = computeDeckReadinessIssues(deck, normalized.workspace)
316
+ const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
317
+ const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
287
318
  deck.writeReadiness = {
288
319
  status: blockers.length === 0 ? "ready" : "blocked",
289
320
  blockers,
@@ -300,6 +331,8 @@ export function reviewDeckState(state: DecksState, slug?: string): { state: Deck
300
331
  status: deck.writeReadiness.status,
301
332
  blocker: blockers.join("; "),
302
333
  blockers,
334
+ warnings,
335
+ issues,
303
336
  },
304
337
  }
305
338
  }
@@ -321,18 +354,48 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
321
354
  slug: targetSlug,
322
355
  blocker: currentDeckBlocker(normalized),
323
356
  blockers: [currentDeckBlocker(normalized)],
357
+ warnings: [],
358
+ issues: [{
359
+ type: "missing_slide_spec",
360
+ severity: "blocker",
361
+ message: currentDeckBlocker(normalized),
362
+ suggestedAction: "Create or select the current workspace deck through revela-decks before writing deck HTML.",
363
+ }],
324
364
  }
325
365
  }
326
366
 
327
- const blockers = computeDeckBlockers(deck)
367
+ const issues = computeDeckReadinessIssues(deck, normalized.workspace)
368
+ const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
369
+ const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
328
370
  if (normalizeDeckPath(deck.outputPath) !== targetPath) {
329
- blockers.unshift(`Deck outputPath is ${deck.outputPath || "missing"}, not ${targetPath}`)
371
+ const message = `Deck outputPath is ${deck.outputPath || "missing"}, not ${targetPath}`
372
+ blockers.unshift(message)
373
+ issues.unshift({
374
+ type: "missing_slide_spec",
375
+ severity: "blocker",
376
+ message,
377
+ suggestedAction: "Update deck.outputPath through revela-decks or write to the reviewed outputPath.",
378
+ })
330
379
  }
331
380
  if (deck.writeReadiness.status !== "ready") {
332
- blockers.unshift(`Deck writeReadiness is ${deck.writeReadiness.status || "missing"}, not ready`)
381
+ const message = `Deck writeReadiness is ${deck.writeReadiness.status || "missing"}, not ready`
382
+ blockers.unshift(message)
383
+ issues.unshift({
384
+ type: "missing_slide_spec",
385
+ severity: "blocker",
386
+ message,
387
+ suggestedAction: "Run /revela review and resolve all readiness blockers before writing deck HTML.",
388
+ })
333
389
  }
334
390
  if (deck.writeReadiness.blockers.length > 0) {
335
- blockers.unshift(`Deck still has readiness blockers: ${deck.writeReadiness.blockers.join("; ")}`)
391
+ const message = `Deck still has readiness blockers: ${deck.writeReadiness.blockers.join("; ")}`
392
+ blockers.unshift(message)
393
+ issues.unshift({
394
+ type: "missing_slide_spec",
395
+ severity: "blocker",
396
+ message,
397
+ suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
398
+ })
336
399
  }
337
400
 
338
401
  return {
@@ -341,6 +404,8 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
341
404
  status: deck.writeReadiness.status,
342
405
  blocker: blockers.join("; "),
343
406
  blockers,
407
+ warnings,
408
+ issues,
344
409
  }
345
410
  }
346
411
 
@@ -416,30 +481,161 @@ function currentDeckBlocker(state: DecksState): string {
416
481
  return `${DECKS_STATE_FILE} contains multiple deck records and no activeDeck. Select one current deck explicitly or move extra decks to separate workspaces.`
417
482
  }
418
483
 
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"}`)
484
+ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["workspace"]): ReadinessIssue[] {
485
+ const issues: ReadinessIssue[] = []
486
+ if (!deck.goal.trim()) issues.push(blockerIssue("missing_slide_spec", "Deck goal is missing", "Set the deck goal through revela-decks upsertDeck."))
487
+ if (!isDeckHtmlPath(deck.outputPath)) {
488
+ issues.push(blockerIssue(
489
+ "missing_slide_spec",
490
+ `outputPath must be decks/*.html, got ${deck.outputPath || "missing"}`,
491
+ "Set outputPath to the target decks/*.html file through revela-decks upsertDeck.",
492
+ ))
493
+ }
423
494
 
424
495
  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`)
496
+ if (value !== true) {
497
+ issues.push(blockerIssue(
498
+ "missing_required_input",
499
+ `requiredInputs.${key} is not true`,
500
+ `Complete and explicitly record requiredInputs.${key} before writing the deck.`,
501
+ ))
502
+ }
426
503
  }
427
504
 
428
- if (deck.slides.length === 0) blockers.push("slides are missing")
505
+ if (deck.slides.length === 0) issues.push(blockerIssue("missing_slide_spec", "slides are missing", "Add the confirmed slide plan through revela-decks upsertSlides."))
429
506
  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`)
507
+ const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
508
+ 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))
509
+ 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))
510
+ 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))
511
+ 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))
512
+
513
+ const claim = findEvidenceSensitiveClaim(slide)
514
+ if (claim && slide.evidence.length === 0) {
515
+ issues.push(blockerIssue(
516
+ "missing_evidence",
517
+ `Slide ${slide.index} has an evidence-sensitive claim without evidence: ${claim}`,
518
+ "Add a compact evidence reference to slides[].evidence or reframe the claim as an explicit assumption/opinion.",
519
+ { ...slideRef, claimText: claim },
520
+ ))
521
+ } else if (claim && slide.evidence.some((item) => !hasEvidenceDetail(item))) {
522
+ issues.push(warningIssue(
523
+ "weak_evidence",
524
+ `Slide ${slide.index} evidence for a high-risk claim has no quote, page, or URL detail: ${claim}`,
525
+ "Add quote/page/url details where available so the writing agent can ground the slide more reliably.",
526
+ { ...slideRef, claimText: claim },
527
+ ))
528
+ }
434
529
  }
435
530
 
436
531
  for (const axis of deck.researchPlan) {
437
532
  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}`)
533
+ issues.push(blockerIssue(
534
+ "research_not_ready",
535
+ `Research axis ${axis.axis || "unnamed"} is needed but ${axis.status}`,
536
+ "Complete, read, or explicitly skip this research axis before writing the deck.",
537
+ ))
439
538
  }
440
539
  }
441
- return blockers
442
- }
540
+
541
+ const hasNeededResearch = deck.researchPlan.some((axis) => axis.needed && axis.status !== "skipped")
542
+ for (const material of workspace.sourceMaterials ?? []) {
543
+ if (material.status !== "discovered") continue
544
+ const message = `Source material ${material.path} has been identified but not extracted, summarized, or researched`
545
+ if (hasNeededResearch) {
546
+ issues.push(blockerIssue(
547
+ "source_not_processed",
548
+ message,
549
+ "Extract, summarize, research, or explicitly exclude this source before writing evidence-backed slides.",
550
+ ))
551
+ } else {
552
+ issues.push(warningIssue(
553
+ "source_not_processed",
554
+ message,
555
+ "Consider extracting or excluding this source if it may support the deck narrative.",
556
+ ))
557
+ }
558
+ }
559
+
560
+ return issues
561
+ }
562
+
563
+ function blockerIssue(type: ReadinessIssueType, message: string, suggestedAction: string, extra: Partial<ReadinessIssue> = {}): ReadinessIssue {
564
+ return { type, severity: "blocker", message, suggestedAction, ...extra }
565
+ }
566
+
567
+ function warningIssue(type: ReadinessIssueType, message: string, suggestedAction: string, extra: Partial<ReadinessIssue> = {}): ReadinessIssue {
568
+ return { type, severity: "warning", message, suggestedAction, ...extra }
569
+ }
570
+
571
+ function findEvidenceSensitiveClaim(slide: SlideSpec): string | undefined {
572
+ const candidates = [
573
+ slide.title,
574
+ slide.purpose,
575
+ slide.content?.headline,
576
+ ...(slide.content?.body ?? []),
577
+ ...(slide.content?.bullets ?? []),
578
+ ]
579
+ .map((item) => item?.trim())
580
+ .filter((item): item is string => Boolean(item))
581
+
582
+ return candidates.find(isEvidenceSensitiveClaim)
583
+ }
584
+
585
+ function isEvidenceSensitiveClaim(text: string): boolean {
586
+ const normalized = text.toLowerCase()
587
+ return hasNumericClaim(normalized) || EVIDENCE_SENSITIVE_TERMS.some((pattern) => pattern.test(normalized))
588
+ }
589
+
590
+ function hasNumericClaim(text: string): boolean {
591
+ 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)
592
+ }
593
+
594
+ function hasEvidenceDetail(evidence: EvidenceRef): boolean {
595
+ return Boolean(evidence.quote?.trim() || evidence.page?.trim() || evidence.url?.trim())
596
+ }
597
+
598
+ const EVIDENCE_SENSITIVE_TERMS = [
599
+ /\bmarket size\b/,
600
+ /\bcagr\b/,
601
+ /\btam\b/,
602
+ /\bsam\b/,
603
+ /\bsom\b/,
604
+ /\brecommend(?:ation|ed)?\b/,
605
+ /\bshould\b/,
606
+ /\bmust\b/,
607
+ /\bgo\/?no-go\b/,
608
+ /\bvs\.?\b/,
609
+ /\bbetter than\b/,
610
+ /\boutperform\b/,
611
+ /\bleading\b/,
612
+ /\bcompetitor\b/,
613
+ /\bmarket leader\b/,
614
+ /\binvest(?:ment)?\b/,
615
+ /\brevenue\b/,
616
+ /\bmargin\b/,
617
+ /\bcost\b/,
618
+ /\brisk\b/,
619
+ /\blatency\b/,
620
+ /\baccuracy\b/,
621
+ /\bscalable\b/,
622
+ /\barchitecture\b/,
623
+ /市场规模/,
624
+ /增长/,
625
+ /领先/,
626
+ /超过/,
627
+ /竞品/,
628
+ /建议/,
629
+ /必须/,
630
+ /投资/,
631
+ /收入/,
632
+ /利润/,
633
+ /成本/,
634
+ /风险/,
635
+ /性能/,
636
+ /架构/,
637
+ /可扩展/,
638
+ ]
443
639
 
444
640
  function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
445
641
  return slides
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.7",
4
4
  "description": "OpenCode plugin that turns AI into an HTML slide deck generator",
5
5
  "type": "module",
6
6
  "main": "./index.ts",