@cyber-dash-tech/revela 0.15.4 → 0.16.2

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.
@@ -124,7 +124,13 @@ export function buildNarrativeViewPrompt(options: { workspaceRoot: string; langu
124
124
  })),
125
125
  relations: map.claimRelations.map((relation) => ({ id: relation.id, fromClaimId: relation.fromClaimId, toClaimId: relation.toClaimId, relation: relation.relation, rationale: relation.rationale, inferred: relation.inferred })),
126
126
  researchGaps: map.researchGaps.map((gap) => ({ id: gap.id, targetType: gap.targetType, targetId: gap.targetId, status: gap.status, priority: gap.priority, question: gap.question })),
127
- artifactCoverage: map.artifactCoverage.map((artifact) => ({ type: artifact.type, outputPath: artifact.outputPath, stale: artifact.stale, slideRefs: artifact.slideRefs.map((ref) => ({ claimId: ref.claimId, slideIndex: ref.slideIndex, role: ref.role, match: ref.match, location: ref.location })) })),
127
+ artifactCoverage: map.artifactCoverage.map((artifact) => ({ type: artifact.type, outputPath: artifact.outputPath, stale: artifact.stale, coverageStatus: artifact.coverageStatus, affectedClaimIds: artifact.affectedClaimIds, missingClaimIds: artifact.missingClaimIds, slideRefs: artifact.slideRefs.map((ref) => ({ claimId: ref.claimId, slideIndex: ref.slideIndex, role: ref.role, match: ref.match, location: ref.location })) })),
128
+ workbench: {
129
+ summary: map.workbench.summary,
130
+ filters: map.workbench.filters,
131
+ renderTargetAction: map.workbench.renderTargetAction,
132
+ artifactCoverage: map.workbench.artifactCoverage.map((item) => ({ type: item.type, outputPath: item.outputPath, coverageStatus: item.coverageStatus, affectedClaimIds: item.affectedClaimIds, missingClaimIds: item.missingClaimIds, statusNote: item.statusNote, recommendedNextCommand: item.recommendedNextCommand })),
133
+ },
128
134
  }
129
135
 
130
136
  return `Prepare the read-only Revela narrative UI display model.
@@ -141,11 +147,11 @@ Hard rules:
141
147
  - Do not invent new claims, evidence, relations, slide coverage, source paths, findings files, quotes, or caveats.
142
148
  - Preserve every claimId exactly.
143
149
  - Preserve every relation endpoint exactly: fromClaimId, toClaimId, relation.
144
- - You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
150
+ - You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels including Story workbench labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
145
151
  - For inferred relations, do not provide relation displayLabel or displayRationale; inferred relations are unconfirmed order notes, not causal/support/dependency judgments.
146
152
  - relation displayRationale may only localize or clarify an existing canonical relation rationale. If relation.rationale is missing or the relation is inferred, do not provide displayRationale; the UI will show the missing or inferred status.
147
153
  - Keep source paths, findings files, claim IDs, narrative hash, and numbers unchanged.
148
- - Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
154
+ - Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels including workbench labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
149
155
  - Do not translate claim IDs, relation endpoints, narrative hash, source paths, findings files, URLs, numbers, or quoted/source facts.
150
156
  - Use natural business and manufacturing terminology in the target language, not word-by-word machine translation.
151
157
  - If a fact is missing, describe it as missing instead of filling it in.
@@ -25,18 +25,19 @@ Current state:
25
25
  ${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
26
26
 
27
27
  Closed-loop workflow:
28
- 1. Call \`revela-decks\` action \`read\`, then \`reviewNarrative\`.
29
- 2. If current research gaps are missing or stale, call \`deriveResearchGaps\` when useful. Do not invent gaps that are not tied to a claim, objection, risk, decision, or narrative issue.
28
+ 1. Call \`revela-decks\` action \`read\`, then \`reviewNarrative\`, then \`deriveResearchTargets\`. Treat the returned \`selected\` target as the deterministic first target unless it is clearly blocked by user-only information.
29
+ 2. If current research gaps are missing or stale, call \`deriveResearchGaps\` when useful, then call \`deriveResearchTargets\` again. Do not invent gaps that are not tied to a claim, objection, risk, decision, or narrative issue.
30
30
  3. Run up to 3 research loops unless the stop conditions below are met earlier.
31
- 4. At the start of each loop, choose the highest-value 2-3 targets: open/in-progress high-priority gaps, unattached saved findings, unsupported central claims, weak evidence, high-priority objections/risks, and claim_chain_gap relations. Prefer targets that can improve readiness or materially reduce caveats. Do not repeat searches for claims already strongly supported.
32
- 5. If a target already has saved findings, prioritize binding/narrowing before doing more external search.
33
- 6. For targets needing external evidence, mark matching gaps \`in_progress\` with \`revela-decks updateResearchGap\`, then delegate search to the \`revela-research\` subagent. Ask it for source URLs, quotes/snippets, dates or locations when available, caveats, remaining gaps, and a \`## Recommended evidence bindings\` section with claimId, quote, source, supportScope, unsupportedScope, caveat, and strength. Save findings with \`revela-research-save\` under \`researches/{topic}/{axis}.md\` using \`## Data\`, \`## Cases\`, \`## Images\`, and \`## Gaps\` sections as applicable.
34
- 7. After findings are saved or existing findings are selected, read or inspect the findings file. Attach it with \`revela-decks attachResearchFindings\` when it maps to an existing research axis.
35
- 8. Automatically bind evidence when all binding criteria are met. Use \`revela-decks applyEvidenceCandidates\` for concrete candidate ids when available, or \`upsertNarrative\` to preserve canonical evidence bindings with exact source, URL/path, quote/snippet, support scope, unsupported scope, caveat, and strength.
36
- 9. Binding criteria: claimId exists; quote/snippet is traceable to the source and is not invented; source URL or workspace source path is present; supportScope and unsupportedScope are explicit; strength is strong or useful partial; caveat is preserved; binding does not expand the claim beyond the evidence.
37
- 10. If a claim or relation is broader than the evidence, narrow the claim text, supportedScope, unsupportedScope, or relation rationale through \`upsertNarrative\` only when the narrower wording preserves the user's strategic meaning and source boundaries. Do not silently change the decision ask, central recommendation, or approval meaning.
38
- 11. Update matching gaps after binding: use \`evidence_bound\` when canonical evidence was added, \`closed\` when the gap is resolved or non-researchable, \`findings_saved\` only when findings exist but binding criteria are not met, and \`open\` with notes when more external research is still warranted.
39
- 12. Re-run \`reviewNarrative\` after each loop. Compare against the previous loop: fewer open gaps, fewer unattached findings, stronger evidence, narrower unsupported scope, or clearer internal-data caveats should count as progress.
31
+ 4. At the start of each loop, use \`deriveResearchTargets\` as the target order. Work the \`selected\` target first, then the next 1-2 highest-priority targets only when they are related. Do not repeat searches for claims already strongly supported.
32
+ 5. If a target has \`findingsFile\` or \`kind: "unattached_findings"\`, inspect \`bindingDiagnostic\` before doing external search. Prefer existing findings before external research.
33
+ 6. When \`bindingDiagnostic.bindable\` is false, do not bind or package the findings as strong evidence. Report the exact \`failureReasons\` such as \`missing_quote\`, \`unclear_source\`, \`unsupported_scope\`, \`caveat_conflict\`, \`weak_source\`, \`source_mismatch\`, or \`context_only_finding\`, then either narrow the claim safely or run targeted research for the missing fields.
34
+ 7. For targets needing external evidence, mark matching gaps \`in_progress\` with \`revela-decks updateResearchGap\`, then delegate search to the \`revela-research\` subagent. Ask it for source URLs, quotes/snippets, dates or locations when available, caveats, remaining gaps, and a \`## Recommended evidence bindings\` section with claimId, quote, source, supportScope, unsupportedScope, caveat, and strength. Save findings with \`revela-research-save\` under \`researches/{topic}/{axis}.md\` using \`## Data\`, \`## Cases\`, \`## Images\`, and \`## Gaps\` sections as applicable.
35
+ 8. After findings are saved or existing findings are selected, read or inspect the findings file. Attach it with \`revela-decks attachResearchFindings\` when it maps to an existing research axis. Re-run \`deriveResearchTargets\` so the next loop sees updated \`bindingDiagnostic\` and target order.
36
+ 9. Automatically bind evidence only when all binding criteria are met and the diagnostic is \`bindable: true\` or the same fields are explicit in the findings. Use \`revela-decks applyEvidenceCandidates\` for concrete candidate ids when available, or \`upsertNarrative\` to preserve canonical evidence bindings with exact source, URL/path, quote/snippet, support scope, unsupported scope, caveat, and strength.
37
+ 10. Binding criteria: claimId exists; quote/snippet is traceable to the source and is not invented; source URL or workspace source path is present; supportScope and unsupportedScope are explicit; strength is strong or useful partial; caveat is preserved; binding does not expand the claim beyond the evidence.
38
+ 11. If a claim or relation is broader than the evidence, narrow the claim text, supportedScope, unsupportedScope, or relation rationale through \`upsertNarrative\` only when the narrower wording preserves the user's strategic meaning and source boundaries. Do not silently change the decision ask, central recommendation, or approval meaning.
39
+ 12. Update matching gaps after binding: use \`evidence_bound\` when canonical evidence was added, \`closed\` when the gap is resolved or non-researchable, \`findings_saved\` only when findings exist but binding criteria are not met, and \`open\` with notes when more external research is still warranted.
40
+ 13. Re-run \`reviewNarrative\` and \`deriveResearchTargets\` after each loop. Compare against the previous loop: fewer open gaps, fewer unattached findings, stronger evidence, narrower unsupported scope, or clearer internal-data caveats should count as progress.
40
41
 
41
42
  Stop conditions:
42
43
  - No open externally researchable gaps remain.
@@ -47,20 +48,26 @@ Stop conditions:
47
48
 
48
49
  Report format:
49
50
  - Start with \`Research loop completed after <n> round(s).\`
50
- - List bound evidence by claim id and count.
51
- - List gaps closed or moved to evidence_bound.
52
- - List claims or relations narrowed, with the remaining unsupported scope.
53
- - List remaining caveats only as one of: \`internal_data_needed\`, \`not_publicly_researchable\`, \`source_quality_limit\`, or \`still_open\`.
54
- - If no binding happened, say why binding criteria failed and what exact source type is needed next.
55
- - End with the next smallest story action, not a generic request for user confirmation.
51
+ - Then use these exact sections in order:
52
+ - \`Selected target\`: report \`kind\`, \`priority\`, \`reason\`, \`question\`, \`targetId\`, \`claimId\`, and any \`findingsFile\`.
53
+ - \`Existing findings inspected\`: for each file, report \`findingsFile\`, \`bindingDiagnostic.bindable\`, \`failureReasons\`, and which explicit fields were present: \`source\`, \`quoteOrSnippet\`, \`supportScope\`, \`unsupportedScope\`, \`caveat\`, \`strength\`. If none were inspected, write \`none\`.
54
+ - \`Attachments\`: list findings attached with axis/status, or \`none\`.
55
+ - \`Evidence bound\`: list evidence bindings by claim id, source, quote/snippet, supportScope, unsupportedScope, caveat, and strength, or \`none\`.
56
+ - \`Unbound findings\`: list every inspected but unbound findings file with structured failure reasons such as \`missing_quote\`, \`unclear_source\`, \`unsupported_scope\`, \`caveat_conflict\`, \`weak_source\`, \`source_mismatch\`, or \`context_only_finding\`. If none, write \`none\`.
57
+ - \`Gap updates\`: list gaps moved to \`in_progress\`, \`findings_saved\`, \`attached\`, \`evidence_bound\`, \`closed\`, or still \`open\` with notes.
58
+ - \`Narrative changes\`: list claims or relations narrowed, with remaining unsupported scope. If none, write \`none\`.
59
+ - \`Remaining caveats\`: use only \`internal_data_needed\`, \`not_publicly_researchable\`, \`source_quality_limit\`, or \`still_open\`.
60
+ - \`Next smallest story action\`: end with one concrete next command or action, not a generic request for confirmation.
61
+ - If no binding happened, the \`Unbound findings\` or \`Remaining caveats\` section must say why binding criteria failed and what exact source type is needed next.
56
62
 
57
63
  Rules:
58
64
  - Do not use primary-agent broad websearch. Use the \`revela-research\` subagent for external search.
59
65
  - Do not invent quotes, source paths, URLs, page references, locations, or caveats.
60
66
  - Do not treat \`researches/**/*.md\` as canonical evidence until attached or evidence-bound, but do not stop at findings_saved when binding criteria are met.
67
+ - Do not bypass \`deriveResearchTargets\`; target selection, \`selected\`, and \`bindingDiagnostic\` are deterministic inputs, not LLM judgement.
61
68
  - Do not mutate canonical claims merely to fit a source; narrow only to preserve evidence boundaries and avoid overstated claims.
62
69
  - Do not ask the user to approve each evidence binding. Ask only when binding would change strategic meaning, downgrade a central claim, rely on suspicious sources, or require narrative approval.
63
70
  - Do not store secrets, credentials, tokens, or sensitive personal information.
64
71
 
65
- Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, reviewing current readiness, and running the first research/binding loop.`
72
+ Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, reviewing current readiness, deriving research targets, and running the first research/binding loop from the selected target.`
66
73
  }
@@ -205,7 +205,7 @@ Workflow:
205
205
  8. For substantial decision decks, preserve a compact \`narrativeBrief\` through \`upsertDeck\` when the conversation or confirmed plan supports it. Do not invent stakeholder beliefs, objections, or risks; leave gaps visible if unknown.
206
206
  9. For substantial decision decks, launch the Task subagent with \`subagent_type: "revela-narrative-reviewer"\` after deck/slides are up to date. Ask it to read the current \`DECKS.json\`, run only its fixed rubric, use stable finding IDs, return \`Findings: none\` when all checks pass, and avoid optional pre-write improvements. Do not ask it to write state, call \`revela-decks review\`, or produce HTML.
207
207
  10. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
208
- 11. 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; separate evidence/source warnings from narrative warnings when possible. If the review result includes \`evidenceCandidates\`, add a separate \`Candidate evidence bindings\` section with candidateId, slide index/title, supported claim scope, sourceKind, findingsFile/sourcePath, quote/snippet, caveat, evidenceDraft summary, unsupportedScope, and recommendedRewrite. Tell the user they may explicitly ask to apply selected candidate IDs; do not apply them during review. If candidates are absent but \`evidenceCandidateSearch\` is present, briefly report searched file counts and the best near misses so the user can tell whether review failed to search or searched but did not find a bindable match. If the reviewer returned findings, include them in a separate \`Narrative reviewer notes\` section and label them advisory.
208
+ 11. 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; separate evidence/source warnings from narrative warnings when possible. If the review result includes \`diagnostics\`, include a \`Plan and coverage diagnostics\` section with plan quality blockers/warnings, artifact \`coverageStatus\`, \`missingClaimIds\`, \`affectedClaimIds\`, stale reasons, and \`nextActions\`. If the review result includes \`evidenceCandidates\`, add a separate \`Candidate evidence bindings\` section with candidateId, slide index/title, supported claim scope, sourceKind, findingsFile/sourcePath, quote/snippet, caveat, evidenceDraft summary, unsupportedScope, and recommendedRewrite. Tell the user they may explicitly ask to apply selected candidate IDs; do not apply them during review. If candidates are absent but \`evidenceCandidateSearch\` is present, briefly report searched file counts and the best near misses so the user can tell whether review failed to search or searched but did not find a bindable match. If the reviewer returned findings, include them in a separate \`Narrative reviewer notes\` section and label them advisory.
209
209
 
210
210
  Minimum conditions for \`ready\`:
211
211
  - Topic, audience, slide count, language, and visual style/design are decided.
@@ -225,6 +225,7 @@ Report format:
225
225
  - Start with \`Ready: yes/no\`.
226
226
  - If blocked, list each blocker with slide index/title when the tool provides it, the issue type, and the suggested next action.
227
227
  - If warnings exist but the deck is otherwise ready, say the deck can be written but note the residual risks.
228
+ - Include coverage-driven make diagnostics when returned: whether the active deck artifact coverage is current/stale/partial/missing, which required claims are missing, which claims are affected, and the next command/action recommended by the tool.
228
229
  - Report \`narrative_gap\` warnings as story-structure risks such as weak so-what, missing risk/assumption handling, conclusion before support, missing audience framing, or abrupt transition.
229
230
  - Do not invent evidence or silently downgrade blockers. Use the tool result as authoritative.
230
231
  - Do not convert \`revela-narrative-reviewer\` advisory findings into tool readiness issues. Keep them separate from \`revela-decks review\` blockers and warnings, and preserve the reviewer's stable finding IDs when reporting them.
@@ -8,7 +8,7 @@ import {
8
8
  workspaceStatePath,
9
9
  writeWorkspaceState,
10
10
  } from "./workspace-state/repository"
11
- import { ensureActiveHtmlDeckRenderTarget } from "./workspace-state/render-targets"
11
+ import { activeHtmlDeckRenderTarget, ensureActiveHtmlDeckRenderTarget } from "./workspace-state/render-targets"
12
12
  import {
13
13
  activeReviewTargetId,
14
14
  appendReviewSnapshot,
@@ -19,6 +19,7 @@ import {
19
19
  import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
20
20
  import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
21
21
  import { computeNarrativeHash } from "./narrative-state/hash"
22
+ import { getArtifactClaimRefs } from "./narrative-state/queries"
22
23
  import type { NarrativeStateV1 } from "./narrative-state/types"
23
24
 
24
25
  export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
@@ -107,6 +108,13 @@ export interface DeckPlanReview {
107
108
  confirmedAt?: string
108
109
  confirmedBy?: "user"
109
110
  summary?: string
111
+ qualityChecks?: DeckPlanQualityCheck[]
112
+ }
113
+
114
+ export interface DeckPlanQualityCheck {
115
+ id: string
116
+ status: "pass" | "warning" | "blocker"
117
+ message: string
110
118
  }
111
119
 
112
120
  export interface NarrativeBrief {
@@ -206,6 +214,24 @@ export interface DeckStateReadinessResult {
206
214
  warnings: string[]
207
215
  issues: ReadinessIssue[]
208
216
  evidenceCandidates?: EvidenceBindingCandidate[]
217
+ diagnostics?: DeckReadinessDiagnostics
218
+ }
219
+
220
+ export interface DeckReadinessDiagnostics {
221
+ planQuality: DeckPlanQualityCheck[]
222
+ artifactCoverage?: ArtifactCoverageDiagnostic
223
+ nextActions: string[]
224
+ }
225
+
226
+ export interface ArtifactCoverageDiagnostic {
227
+ artifactId?: string
228
+ outputPath?: string
229
+ coverageStatus: "current" | "stale" | "partial" | "missing" | "unknown"
230
+ requiredClaimIds: string[]
231
+ coveredClaimIds: string[]
232
+ missingClaimIds: string[]
233
+ affectedClaimIds: string[]
234
+ staleReasons: string[]
209
235
  }
210
236
 
211
237
  export type ReadinessSeverity = "blocker" | "warning"
@@ -214,6 +240,8 @@ export type ReadinessIssueType =
214
240
  | "missing_required_input"
215
241
  | "missing_slide_spec"
216
242
  | "slide_plan_unconfirmed"
243
+ | "plan_quality"
244
+ | "artifact_coverage"
217
245
  | "research_not_ready"
218
246
  | "missing_evidence"
219
247
  | "weak_evidence"
@@ -444,6 +472,7 @@ export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptio
444
472
  confirmedAt: options.now ?? new Date().toISOString(),
445
473
  confirmedBy: options.approvedBy ?? "user",
446
474
  summary: cleanOptionalText(options.note),
475
+ qualityChecks: pending?.qualityChecks,
447
476
  }
448
477
  deck.requiredInputs = { ...deck.requiredInputs, slidePlanConfirmed: true }
449
478
  deck.writeReadiness = { status: "blocked", blockers: [] }
@@ -579,7 +608,7 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
579
608
  }
580
609
  }
581
610
 
582
- const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
611
+ const issues = computeDeckReadinessIssues(normalized, deck, {
583
612
  ...options,
584
613
  narrativeHash: options.narrativeHash ?? computeNarrativeHash(normalizeNarrativeState(normalized)),
585
614
  })
@@ -604,6 +633,7 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
604
633
  warnings,
605
634
  issues,
606
635
  evidenceCandidates,
636
+ diagnostics: deckReadinessDiagnostics(normalized, deck, issues),
607
637
  }
608
638
  appendReviewSnapshot(normalized, createReviewSnapshot(normalized, { slug: deck.slug, result, reviewedAt }))
609
639
  return {
@@ -639,7 +669,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
639
669
  }
640
670
  }
641
671
 
642
- const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
672
+ const issues = computeDeckReadinessIssues(normalized, deck, {
643
673
  narrativeHash: computeNarrativeHash(normalizeNarrativeState(normalized)),
644
674
  })
645
675
  const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
@@ -715,6 +745,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
715
745
  blockers,
716
746
  warnings,
717
747
  issues,
748
+ diagnostics: deckReadinessDiagnostics(normalized, deck, issues),
718
749
  }
719
750
  }
720
751
 
@@ -871,9 +902,23 @@ function normalizeDeckPlanReview(input: DeckPlanReview | undefined): DeckPlanRev
871
902
  confirmedAt: cleanOptionalText(input.confirmedAt),
872
903
  confirmedBy: input.confirmedBy === "user" ? "user" : undefined,
873
904
  summary: cleanOptionalText(input.summary),
905
+ qualityChecks: normalizeDeckPlanQualityChecks(input.qualityChecks),
874
906
  }
875
907
  }
876
908
 
909
+ function normalizeDeckPlanQualityChecks(input: DeckPlanQualityCheck[] | undefined): DeckPlanQualityCheck[] | undefined {
910
+ if (!Array.isArray(input)) return undefined
911
+ const checks = input.flatMap((item): DeckPlanQualityCheck[] => {
912
+ if (!item || typeof item !== "object") return []
913
+ const id = cleanOptionalText(item.id)
914
+ const message = cleanOptionalText(item.message)
915
+ if (!id || !message) return []
916
+ const status = item.status === "blocker" || item.status === "warning" ? item.status : "pass"
917
+ return [{ id, status, message }]
918
+ })
919
+ return checks.length > 0 ? checks : undefined
920
+ }
921
+
877
922
  function currentDeckKey(state: DecksState): string | undefined {
878
923
  if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
879
924
  const keys = Object.keys(state.decks)
@@ -887,8 +932,9 @@ function currentDeckBlocker(state: DecksState): string {
887
932
  return `${DECKS_STATE_FILE} contains multiple deck records and no activeDeck. Select one current deck explicitly or move extra decks to separate workspaces.`
888
933
  }
889
934
 
890
- function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["workspace"], options: ReviewDeckStateOptions = {}): ReadinessIssue[] {
935
+ function computeDeckReadinessIssues(state: DecksState, deck: DeckSpec, options: ReviewDeckStateOptions = {}): ReadinessIssue[] {
891
936
  const issues: ReadinessIssue[] = []
937
+ const workspace = state.workspace
892
938
  if (!deck.goal.trim()) issues.push(blockerIssue("missing_slide_spec", "Deck goal is missing", "Set the deck goal through revela-decks upsertDeck."))
893
939
  if (!isDeckHtmlPath(deck.outputPath)) {
894
940
  issues.push(blockerIssue(
@@ -919,6 +965,8 @@ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["works
919
965
  ))
920
966
  }
921
967
  }
968
+ issues.push(...deckPlanQualityIssues(deck))
969
+ issues.push(...artifactCoverageIssues(state, deck))
922
970
  for (const slide of deck.slides) {
923
971
  const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
924
972
  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))
@@ -985,6 +1033,96 @@ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["works
985
1033
  return issues
986
1034
  }
987
1035
 
1036
+ function deckPlanQualityIssues(deck: DeckSpec): ReadinessIssue[] {
1037
+ const checks = deck.planReview?.qualityChecks ?? []
1038
+ return checks.flatMap((check): ReadinessIssue[] => {
1039
+ if (check.status === "pass") return []
1040
+ const suggestedAction = check.status === "blocker"
1041
+ ? "Re-run compileDeckPlan or revise the deck projection so the deterministic plan includes Cover, TOC, central claim coverage, compatible components, and Closing/Decision Ask before confirming or writing the deck."
1042
+ : "Keep the stated claim boundaries visible in the plan and rendered artifact; do not stretch partial evidence beyond the supported scope."
1043
+ return [{
1044
+ type: "plan_quality",
1045
+ severity: check.status,
1046
+ message: check.message,
1047
+ suggestedAction,
1048
+ }]
1049
+ })
1050
+ }
1051
+
1052
+ function artifactCoverageIssues(state: DecksState, deck: DeckSpec): ReadinessIssue[] {
1053
+ const coverage = artifactCoverageDiagnostic(state, deck)
1054
+ if (!coverage) return []
1055
+ const issues: ReadinessIssue[] = []
1056
+ if (coverage.missingClaimIds.length > 0) {
1057
+ issues.push(blockerIssue(
1058
+ "artifact_coverage",
1059
+ `Active deck plan is missing required narrative claims: ${coverage.missingClaimIds.join(", ")}`,
1060
+ "Re-run compileDeckPlan or revise the deck projection so every central or evidence-required claim appears in the planned slides before writing the deck.",
1061
+ ))
1062
+ }
1063
+ if (coverage.coverageStatus === "stale") {
1064
+ issues.push(blockerIssue(
1065
+ "artifact_coverage",
1066
+ `Active deck artifact coverage is stale: ${coverage.staleReasons.join("; ") || "narrative or render target changed"}`,
1067
+ "Re-run /revela make --deck so the deck plan and artifact coverage are regenerated from the current approved narrative state.",
1068
+ ))
1069
+ } else if (coverage.coverageStatus === "partial") {
1070
+ issues.push(warningIssue(
1071
+ "artifact_coverage",
1072
+ `Active deck artifact coverage is partial: ${coverage.affectedClaimIds.join(", ") || "some claims are not fully mapped"}`,
1073
+ "Keep the partial coverage visible in the make report and review the affected claims before exporting or presenting the deck.",
1074
+ ))
1075
+ }
1076
+ return issues
1077
+ }
1078
+
1079
+ function deckReadinessDiagnostics(state: DecksState, deck: DeckSpec, issues: ReadinessIssue[]): DeckReadinessDiagnostics {
1080
+ const planQuality = deck.planReview?.qualityChecks ?? []
1081
+ const artifactCoverage = artifactCoverageDiagnostic(state, deck)
1082
+ return {
1083
+ planQuality,
1084
+ ...(artifactCoverage ? { artifactCoverage } : {}),
1085
+ nextActions: readinessNextActions(issues, artifactCoverage),
1086
+ }
1087
+ }
1088
+
1089
+ function artifactCoverageDiagnostic(state: DecksState, deck: DeckSpec): ArtifactCoverageDiagnostic | undefined {
1090
+ const target = activeHtmlDeckRenderTarget(state)
1091
+ const artifact = getArtifactClaimRefs(state).find((item) => item.type === "html_deck" && normalizeDeckPath(item.outputPath ?? "") === normalizeDeckPath(deck.outputPath))
1092
+ const data = target?.data ?? {}
1093
+ const requiredClaimIds = stringArray(data.requiredClaimIds)
1094
+ const coveredClaimIds = stringArray(data.coveredClaimIds)
1095
+ const missingClaimIds = [...new Set([...(artifact?.missingClaimIds ?? []), ...stringArray(data.missingClaimIds)])].sort()
1096
+ const affectedClaimIds = [...new Set([...(artifact?.affectedClaimIds ?? []), ...missingClaimIds])].sort()
1097
+ const staleReasons = artifact?.staleReasons ?? []
1098
+ const coverageStatus = artifact?.coverageStatus ?? (target ? (missingClaimIds.length > 0 ? "missing" : "current") : "unknown")
1099
+ if (!target && !artifact && requiredClaimIds.length === 0 && coveredClaimIds.length === 0 && missingClaimIds.length === 0) return undefined
1100
+ return {
1101
+ artifactId: artifact?.artifactId ?? target?.id,
1102
+ outputPath: artifact?.outputPath ?? target?.outputPath ?? deck.outputPath,
1103
+ coverageStatus,
1104
+ requiredClaimIds: [...new Set([...(artifact?.claimIds ?? []), ...requiredClaimIds, ...missingClaimIds])].filter((id) => requiredClaimIds.length === 0 || requiredClaimIds.includes(id) || missingClaimIds.includes(id)).sort(),
1105
+ coveredClaimIds: [...new Set([...(artifact?.claimIds ?? []), ...coveredClaimIds])].filter((id) => missingClaimIds.length === 0 || !missingClaimIds.includes(id)).sort(),
1106
+ missingClaimIds,
1107
+ affectedClaimIds,
1108
+ staleReasons,
1109
+ }
1110
+ }
1111
+
1112
+ function readinessNextActions(issues: ReadinessIssue[], coverage?: ArtifactCoverageDiagnostic): string[] {
1113
+ const actions = issues
1114
+ .filter((issue) => issue.severity === "blocker" || issue.type === "plan_quality" || issue.type === "artifact_coverage")
1115
+ .map((issue) => issue.suggestedAction)
1116
+ if (coverage?.missingClaimIds.length) actions.unshift("Review missingClaimIds in artifactCoverage and recompile the deterministic deck plan before writing HTML.")
1117
+ if (coverage?.coverageStatus === "stale") actions.unshift("Regenerate the deck plan from the current narrative before writing or exporting artifacts.")
1118
+ return [...new Set(actions)].slice(0, 5)
1119
+ }
1120
+
1121
+ function stringArray(value: unknown): string[] {
1122
+ if (!Array.isArray(value)) return []
1123
+ return [...new Set(value.filter((item): item is string => typeof item === "string" && item.trim().length > 0).map((item) => item.trim()))].sort()
1124
+ }
1125
+
988
1126
  function findEvidenceBindingCandidates(deck: DeckSpec, slide: SlideSpec, claimText: string, options: ReviewDeckStateOptions): { candidates: EvidenceBindingCandidate[]; search?: EvidenceCandidateSearchDiagnostic } {
989
1127
  if (!options.workspaceRoot) return { candidates: [] }
990
1128
  const queryText = slideSearchText(slide)
@@ -29,6 +29,16 @@ export interface NarrativeDisplayLabels {
29
29
  risks: string
30
30
  researchGaps: string
31
31
  coveredSlides: string
32
+ storyWorkbench: string
33
+ workbenchNote: string
34
+ artifactCoverage: string
35
+ noRenderTargets: string
36
+ nextActions: string
37
+ missingClaims: string
38
+ affectedClaims: string
39
+ affectedSlides: string
40
+ notes: string
41
+ recommendedNextCommand: string
32
42
  noClaims: string
33
43
  none: string
34
44
  }
@@ -79,6 +89,16 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
79
89
  risks: "风险",
80
90
  researchGaps: "研究缺口",
81
91
  coveredSlides: "已覆盖页面",
92
+ storyWorkbench: "Story 工作台",
93
+ workbenchNote: "按证据缺口、风险、异议和产物覆盖过滤主张;这里只读展示下一步命令,不修改叙事状态。",
94
+ artifactCoverage: "产物覆盖",
95
+ noRenderTargets: "未记录 render target",
96
+ nextActions: "下一步",
97
+ missingClaims: "缺失主张",
98
+ affectedClaims: "受影响主张",
99
+ affectedSlides: "受影响页面",
100
+ notes: "说明",
101
+ recommendedNextCommand: "建议命令",
82
102
  noClaims: "没有记录主张",
83
103
  none: "无",
84
104
  }
@@ -101,6 +121,16 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
101
121
  risks: "リスク",
102
122
  researchGaps: "調査ギャップ",
103
123
  coveredSlides: "対応スライド",
124
+ storyWorkbench: "Story ワークベンチ",
125
+ workbenchNote: "根拠ギャップ、リスク、反論、成果物カバレッジでクレームを絞り込みます。ここでは次のコマンドだけを読み取り専用で示し、ナラティブ状態は変更しません。",
126
+ artifactCoverage: "成果物カバレッジ",
127
+ noRenderTargets: "render target は記録されていません",
128
+ nextActions: "次のアクション",
129
+ missingClaims: "不足クレーム",
130
+ affectedClaims: "影響クレーム",
131
+ affectedSlides: "影響スライド",
132
+ notes: "メモ",
133
+ recommendedNextCommand: "推奨コマンド",
104
134
  noClaims: "クレームは記録されていません",
105
135
  none: "なし",
106
136
  }
@@ -122,6 +152,16 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
122
152
  risks: "Risks",
123
153
  researchGaps: "Research gaps",
124
154
  coveredSlides: "Covered slides",
155
+ storyWorkbench: "Story workbench",
156
+ workbenchNote: "Filter claims by evidence gaps, risks, objections, and artifact coverage. This view only suggests next commands; it does not mutate narrative state.",
157
+ artifactCoverage: "Artifact coverage",
158
+ noRenderTargets: "No render targets recorded",
159
+ nextActions: "Next actions",
160
+ missingClaims: "Missing claims",
161
+ affectedClaims: "Affected claims",
162
+ affectedSlides: "Affected slides",
163
+ notes: "Notes",
164
+ recommendedNextCommand: "Recommended next command",
125
165
  noClaims: "No claims recorded",
126
166
  none: "None",
127
167
  }