@cyber-dash-tech/revela 0.15.4 → 0.16.1

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.
@@ -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
  }
@@ -44,6 +44,23 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
44
44
  .layout { display:grid; grid-template-columns:minmax(0,1fr) minmax(360px,430px); gap:18px; margin-top:18px; align-items:start; }
45
45
  .flow,.detail-panel { background:rgba(255,253,248,.92); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); }
46
46
  .flow { padding:20px; }
47
+ .workbench { margin-top:18px; background:rgba(255,253,248,.92); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); padding:18px 20px; }
48
+ .workbench h2 { margin:0; font-size:18px; letter-spacing:-.025em; }
49
+ .workbench-summary { display:grid; grid-template-columns:repeat(auto-fit,minmax(190px,1fr)); gap:10px; margin-top:14px; }
50
+ .summary-item { border:1px solid var(--line); border-radius:14px; background:#fff; padding:11px 12px; }
51
+ .summary-label { display:block; color:var(--muted); font-size:11px; font-weight:850; letter-spacing:.05em; text-transform:uppercase; }
52
+ .summary-value { display:block; margin-top:4px; color:#51483f; font-size:14px; font-weight:850; }
53
+ .filter-row { display:flex; flex-wrap:wrap; gap:8px; margin-top:14px; }
54
+ .filter-button { cursor:pointer; border:1px solid var(--line); border-radius:999px; background:#fff; color:var(--muted); padding:8px 11px; font-size:12px; font-weight:850; }
55
+ .filter-button.active { border-color:var(--accent); color:var(--accent); background:#fff4ea; }
56
+ .filter-status { margin:10px 0 0; color:var(--muted); font-size:12px; font-weight:780; }
57
+ .filter-empty { display:none; margin-top:10px; border:1px dashed var(--line); border-radius:14px; padding:12px; color:var(--muted); background:#fffaf3; font-size:13px; }
58
+ .coverage-grid { margin-top:16px; display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:10px; }
59
+ .coverage-item { border:1px solid var(--line); border-radius:16px; background:#fff; padding:13px; }
60
+ .coverage-item h3 { margin:0; font-size:14px; line-height:1.2; }
61
+ .coverage-meta { display:flex; flex-wrap:wrap; gap:6px; margin-top:9px; }
62
+ .coverage-detail { margin:9px 0 0; color:var(--muted); font-size:12px; line-height:1.45; }
63
+ .coverage-detail strong { color:#51483f; }
47
64
  .flow-head { display:flex; justify-content:space-between; gap:14px; align-items:flex-start; margin-bottom:18px; }
48
65
  .flow-head h2 { margin:0; font-size:18px; letter-spacing:-.025em; }
49
66
  .flow-note { margin:4px 0 0; color:var(--muted); font-size:13px; line-height:1.45; }
@@ -64,6 +81,10 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
64
81
  .claim-section { border-top:1px solid #eee4d8; padding-top:9px; }
65
82
  .section-label { display:block; margin-bottom:3px; color:var(--accent); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
66
83
  .section-text { display:block; color:#51483f; font-size:13px; line-height:1.46; white-space:pre-line; }
84
+ .next-actions { display:flex; flex-direction:column; gap:8px; }
85
+ .next-action { border:1px solid #eee4d8; border-radius:12px; padding:9px; background:#fffaf3; }
86
+ .next-action strong { display:block; color:#51483f; font-size:13px; }
87
+ .next-action code { display:inline-block; margin-top:5px; color:#9c4d1d; font-size:12px; }
67
88
  .relation-strip { margin-top:12px; display:grid; gap:7px; }
68
89
  .relation { display:grid; grid-template-columns:auto minmax(0,1fr); gap:8px; align-items:flex-start; color:var(--muted); font-size:13px; line-height:1.35; }
69
90
  .relation-badge { flex:0 0 auto; border-radius:999px; padding:3px 7px; background:#fff4e8; color:#9c4d1d; border:1px solid #efcfb8; font-size:10px; font-weight:850; text-transform:uppercase; letter-spacing:.04em; }
@@ -121,12 +142,16 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
121
142
  <div class="detail-body" id="detail-body">${initial?.detailHtml ?? emptyCard(display.labels.claimFlow, display.labels.noClaims)}</div>
122
143
  </aside>
123
144
  </div>
145
+ ${renderWorkbench(map, display)}
124
146
  </main>
125
147
  <div class="hidden-detail">
126
148
  ${nodes.map((node) => `<template id="detail-${escapeAttr(node.id)}" data-title="${escapeHtml(node.title)}" data-subtitle="${escapeHtml(claimSubtitle(node.claim, display))}">${node.detailHtml}</template>`).join("")}
127
149
  </div>
128
150
  <script>
129
151
  const buttons = Array.from(document.querySelectorAll('.claim-card'));
152
+ const filters = Array.from(document.querySelectorAll('.filter-button'));
153
+ const filterStatus = document.getElementById('filter-status');
154
+ const filterEmpty = document.getElementById('filter-empty');
130
155
  const title = document.getElementById('detail-title');
131
156
  const sub = document.getElementById('detail-sub');
132
157
  const body = document.getElementById('detail-body');
@@ -139,6 +164,19 @@ export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: Va
139
164
  buttons.forEach((button) => button.classList.toggle('active', button.dataset.nodeId === id));
140
165
  }
141
166
  buttons.forEach((button) => button.addEventListener('click', () => selectClaim(button.dataset.nodeId)));
167
+ filters.forEach((button) => button.addEventListener('click', () => {
168
+ const filter = button.dataset.filterId || 'all';
169
+ filters.forEach((item) => item.classList.toggle('active', item === button));
170
+ buttons.forEach((claimButton) => {
171
+ const flags = (claimButton.dataset.filters || '').split(' ');
172
+ claimButton.closest('.claim-step').style.display = filter === 'all' || flags.includes(filter) ? '' : 'none';
173
+ });
174
+ const visibleButtons = buttons.filter((claimButton) => claimButton.closest('.claim-step').style.display !== 'none');
175
+ const activeButton = buttons.find((claimButton) => claimButton.classList.contains('active'));
176
+ if (visibleButtons.length > 0 && (!activeButton || activeButton.closest('.claim-step').style.display === 'none')) selectClaim(visibleButtons[0].dataset.nodeId);
177
+ if (filterStatus) filterStatus.textContent = (button.dataset.filterLabel || filter) + ': ' + visibleButtons.length;
178
+ if (filterEmpty) filterEmpty.style.display = visibleButtons.length === 0 ? 'block' : 'none';
179
+ }));
142
180
  </script>
143
181
  </body>
144
182
  </html>`
@@ -158,7 +196,7 @@ function renderStep(node: FlowNode, map: NarrativeMap, display: ValidatedNarrati
158
196
  const outgoing = map.claimRelations.filter((relation) => relation.fromClaimId === node.claim.id)
159
197
  return `<div class="claim-step">
160
198
  <div class="step-rail"><div class="step-dot">${index + 1}</div><div class="step-line"></div></div>
161
- <button class="claim-card ${escapeAttr(node.claim.evidenceStatus)}${active ? " active" : ""}" data-node-id="${escapeAttr(node.id)}" type="button">
199
+ <button class="claim-card ${escapeAttr(node.claim.evidenceStatus)}${active ? " active" : ""}" data-node-id="${escapeAttr(node.id)}" data-filters="${escapeHtml(node.claim.workbenchFlags.join(" "))}" type="button">
162
200
  <span class="claim-title">${escapeHtml(node.title)}</span>
163
201
  <span class="claim-meta"><span class="tag">${escapeHtml(localizeValue(node.claim.kind, display))}</span><span class="tag">${escapeHtml(localizeValue(node.claim.importance, display))}</span><span class="tag">${escapeHtml(localizeValue(node.claim.evidenceStatus, display))}</span><span class="tag">${escapeHtml(node.claim.id)}</span></span>
164
202
  ${renderDisplayCardSummary(node.displayCard, display)}
@@ -213,9 +251,66 @@ function claimDetail(claim: NarrativeMapClaim, map: NarrativeMap, display: Valid
213
251
  ...(gaps.length ? [[display.labels.researchGaps, gaps.map((item) => `${item.question} [${item.status}/${item.priority}]`).join("<br>")] as [string, string]] : []),
214
252
  ...(slideRefs.length ? [[display.labels.coveredSlides, slideRefs.map((ref) => localizeSlideRef(ref, display)).join("<br>")] as [string, string]] : []),
215
253
  ...(coverageGaps.length ? [[systemTerm("artifactCoverage", display), coverageGaps.map((artifact) => `${artifact.type}: ${artifact.coverageStatus}${artifact.staleReasons.length ? ` - ${artifact.staleReasons.join("; ")}` : ""}`).join("<br>")] as [string, string]] : []),
254
+ ...(claim.nextActions.length ? [[systemTerm("nextActions", display), renderNextActions(claim, display), true] as [string, string, boolean]] : []),
216
255
  ])
217
256
  }
218
257
 
258
+ function renderWorkbench(map: NarrativeMap, display: ValidatedNarrativeDisplayModel): string {
259
+ return `<section class="workbench" aria-label="Story workbench">
260
+ <h2>${escapeHtml(systemTerm("storyWorkbench", display))}</h2>
261
+ <p class="flow-note">${escapeHtml(workbenchNote(display))}</p>
262
+ ${renderWorkbenchSummary(map, display)}
263
+ <div class="filter-row" aria-label="Story filters">
264
+ ${map.workbench.filters.map((filter, index) => `<button type="button" class="filter-button${index === 0 ? " active" : ""}" data-filter-id="${escapeAttr(filter.id)}" data-filter-label="${escapeHtml(localizeFilter(filter.label, display))}">${escapeHtml(localizeFilter(filter.label, display))} (${filter.count})</button>`).join("")}
265
+ </div>
266
+ <p class="filter-status" id="filter-status">${escapeHtml(localizeFilter(map.workbench.filters[0]?.label ?? "All claims", display))}: ${map.workbench.filters[0]?.count ?? 0}</p>
267
+ <div class="filter-empty" id="filter-empty">${escapeHtml(noClaimsMatchFilter(display))}</div>
268
+ <div class="coverage-grid">
269
+ ${map.workbench.artifactCoverage.length ? map.workbench.artifactCoverage.map((item) => renderCoverageItem(item, display)).join("") : renderNoRenderTargetCard(map, display)}
270
+ </div>
271
+ </section>`
272
+ }
273
+
274
+ function renderWorkbenchSummary(map: NarrativeMap, display: ValidatedNarrativeDisplayModel): string {
275
+ const summary = map.workbench.summary
276
+ return `<div class="workbench-summary" aria-label="Story readiness summary">
277
+ <div class="summary-item"><span class="summary-label">${escapeHtml(systemTerm("approval", display))}</span><span class="summary-value">${escapeHtml(localizeValue(summary.approval, display))}</span></div>
278
+ <div class="summary-item"><span class="summary-label">${escapeHtml(readinessSummaryTerm("evidenceBlockers", display))}</span><span class="summary-value">${summary.evidenceBlockersCount}</span></div>
279
+ <div class="summary-item"><span class="summary-label">${escapeHtml(readinessSummaryTerm("artifactStatus", display))}</span><span class="summary-value">${escapeHtml(localizeValue(summary.artifactStatus, display))}</span></div>
280
+ <div class="summary-item"><span class="summary-label">${escapeHtml(readinessSummaryTerm("primaryNextCommand", display))}</span><span class="summary-value"><code>${escapeHtml(summary.primaryNextCommand)}</code></span></div>
281
+ <div class="summary-item"><span class="summary-label">${escapeHtml(readinessSummaryTerm("primaryNextReason", display))}</span><span class="summary-value">${escapeHtml(summary.primaryNextReason)}</span></div>
282
+ </div>`
283
+ }
284
+
285
+ function renderNoRenderTargetCard(map: NarrativeMap, display: ValidatedNarrativeDisplayModel): string {
286
+ const action = map.workbench.renderTargetAction
287
+ if (!action) return emptyCard(systemTerm("artifactCoverage", display), systemTerm("noRenderTargets", display))
288
+ return `<article class="coverage-item">
289
+ <h3>${escapeHtml(systemTerm("artifactCoverage", display))}</h3>
290
+ <p class="coverage-detail">${escapeHtml(systemTerm("noRenderTargets", display))}</p>
291
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("notes", display))}:</strong> ${escapeHtml(localizeAction(action.label, display))} - ${escapeHtml(action.reason)}</p>
292
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("recommendedNextCommand", display))}:</strong> <code>${escapeHtml(action.command)}</code></p>
293
+ </article>`
294
+ }
295
+
296
+ function renderCoverageItem(item: NarrativeMap["workbench"]["artifactCoverage"][number], display: ValidatedNarrativeDisplayModel): string {
297
+ const title = item.outputPath ?? item.artifactId
298
+ const slides = item.affectedSlides.map((slide) => `${localizeSlideRef(`slide ${slide.slideIndex}`, display)}: ${slide.slideTitle} (${slide.claimId}, ${slide.role}/${slide.location})`).join("<br>")
299
+ return `<article class="coverage-item">
300
+ <h3>${escapeHtml(title)}</h3>
301
+ <div class="coverage-meta"><span class="pill ${escapeAttr(item.coverageStatus)}">${escapeHtml(localizeValue(item.coverageStatus, display))}</span><span class="tag">${escapeHtml(item.type)}</span>${item.contractStatus ? `<span class="tag">${escapeHtml(item.contractStatus)}</span>` : ""}</div>
302
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("missingClaims", display))}:</strong> ${escapeHtml(item.missingClaimIds.join(", ") || systemTerm("none", display))}</p>
303
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("affectedClaims", display))}:</strong> ${escapeHtml(item.affectedClaimIds.join(", ") || systemTerm("none", display))}</p>
304
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("affectedSlides", display))}:</strong> ${slides ? allowBreaks(slides) : escapeHtml(systemTerm("none", display))}</p>
305
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("notes", display))}:</strong> ${escapeHtml([item.statusNote, ...item.staleReasons].filter(Boolean).join("; ") || systemTerm("none", display))}</p>
306
+ <p class="coverage-detail"><strong>${escapeHtml(systemTerm("recommendedNextCommand", display))}:</strong> <code>${escapeHtml(item.recommendedNextCommand)}</code></p>
307
+ </article>`
308
+ }
309
+
310
+ function renderNextActions(claim: NarrativeMapClaim, display: ValidatedNarrativeDisplayModel): string {
311
+ return `<span class="next-actions">${claim.nextActions.map((action) => `<span class="next-action"><strong>${escapeHtml(localizeAction(action.label, display))}</strong>${escapeHtml(action.reason)}<br><code>${escapeHtml(action.command)}</code></span>`).join("")}</span>`
312
+ }
313
+
219
314
  function relationText(relation: NarrativeMapClaimRelation, display: ValidatedNarrativeDisplayModel): string {
220
315
  const from = displayClaimText(relation.fromClaimId, relation.fromClaimText, display)
221
316
  const to = displayClaimText(relation.toClaimId, relation.toClaimText, display)
@@ -259,8 +354,8 @@ function missingRationale(display: ValidatedNarrativeDisplayModel): string {
259
354
  return "Causal rationale is not recorded."
260
355
  }
261
356
 
262
- function detailCards(rows: Array<[string, string]>): string {
263
- return rows.map(([label, value]) => `<div class="detail-card"><h3>${escapeHtml(label)}</h3><p>${allowBreaks(value)}</p></div>`).join("")
357
+ function detailCards(rows: Array<[string, string] | [string, string, boolean]>): string {
358
+ return rows.map(([label, value, raw]) => `<div class="detail-card"><h3>${escapeHtml(label)}</h3><p>${raw ? value : allowBreaks(value)}</p></div>`).join("")
264
359
  }
265
360
 
266
361
  function emptyCard(label: string, value: string): string {
@@ -292,14 +387,46 @@ function sectionLabels(display: ValidatedNarrativeDisplayModel): Record<string,
292
387
  return { role: "Role", narrativeJob: "Narrative job", evidenceSummary: "Evidence summary", riskOrGapSummary: "Risk / gap" }
293
388
  }
294
389
 
390
+ function workbenchNote(display: ValidatedNarrativeDisplayModel): string {
391
+ return display.labels.workbenchNote
392
+ }
393
+
394
+ function localizeFilter(value: string, display: ValidatedNarrativeDisplayModel): string {
395
+ const zh: Record<string, string> = { "All claims": "全部主张", "Missing evidence": "证据缺失", "Partial evidence": "部分证据", "Stale artifacts": "过期产物", "Open gaps": "开放缺口", Risks: "风险", "High-priority objections": "高优先级异议" }
396
+ const ja: Record<string, string> = { "All claims": "すべてのクレーム", "Missing evidence": "根拠不足", "Partial evidence": "一部根拠", "Stale artifacts": "古い成果物", "Open gaps": "未解決ギャップ", Risks: "リスク", "High-priority objections": "高優先度の反論" }
397
+ const table = isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : {}
398
+ return table[value] ?? value
399
+ }
400
+
401
+ function localizeAction(value: string, display: ValidatedNarrativeDisplayModel): string {
402
+ const zh: Record<string, string> = { "Research this gap": "研究这个缺口", "Attach findings": "附加研究发现", "Narrow claim": "收窄主张", "Approve narrative": "批准叙事", "Make deck": "制作 deck", "Remake stale artifact": "重新生成过期产物" }
403
+ const ja: Record<string, string> = { "Research this gap": "このギャップを調査", "Attach findings": "調査結果を紐付け", "Narrow claim": "クレームを絞る", "Approve narrative": "ナラティブを承認", "Make deck": "デッキを作成", "Remake stale artifact": "古い成果物を再生成" }
404
+ const table = isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : {}
405
+ return table[value] ?? value
406
+ }
407
+
408
+ function readinessSummaryTerm(value: string, display: ValidatedNarrativeDisplayModel): string {
409
+ const zh: Record<string, string> = { evidenceBlockers: "证据阻塞", artifactStatus: "产物状态", primaryNextCommand: "首要建议命令", primaryNextReason: "首要建议原因" }
410
+ const ja: Record<string, string> = { evidenceBlockers: "根拠ブロッカー", artifactStatus: "成果物ステータス", primaryNextCommand: "最優先コマンド", primaryNextReason: "最優先理由" }
411
+ const en: Record<string, string> = { evidenceBlockers: "Evidence blockers", artifactStatus: "Artifact status", primaryNextCommand: "Primary next command", primaryNextReason: "Primary next reason" }
412
+ return (isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : en)[value] ?? value
413
+ }
414
+
415
+ function noClaimsMatchFilter(display: ValidatedNarrativeDisplayModel): string {
416
+ if (isChineseLanguage(display.language)) return "没有主张匹配这个过滤器。"
417
+ if (isJapaneseLanguage(display.language)) return "このフィルターに一致するクレームはありません。"
418
+ return "No claims match this filter."
419
+ }
420
+
295
421
  function systemTerm(term: string, display: ValidatedNarrativeDisplayModel): string {
296
- const zh: Record<string, string> = { approval: "审批", claims: "主张", relations: "关系", inferred: "未确认", relation: "关系", from: "来自", to: "指向", rationale: "说明", strength: "强度", findingsFile: "研究文件", location: "位置", quote: "引用", caveat: "注意事项", artifacts: "产物", attention: "需关注", artifactCoverage: "产物覆盖" }
297
- const ja: Record<string, string> = { approval: "承認", claims: "クレーム", relations: "関係", inferred: "未確認", relation: "関係", from: "起点", to: "終点", rationale: "理由", strength: "強度", findingsFile: "調査ファイル", location: "場所", quote: "引用", caveat: "留意点", artifacts: "成果物", attention: "要確認", artifactCoverage: "成果物カバレッジ" }
298
- const en: Record<string, string> = { approval: "approval", claims: "claims", relations: "relations", inferred: "unconfirmed", relation: "relation", from: "from", to: "to", rationale: "rationale", strength: "strength", findingsFile: "findings file", location: "location", quote: "quote", caveat: "caveat", artifacts: "artifacts", attention: "need attention", artifactCoverage: "Artifact coverage" }
422
+ const zh: Record<string, string> = { approval: "审批", claims: "主张", relations: "关系", inferred: "未确认", relation: "关系", from: "来自", to: "指向", rationale: "说明", strength: "强度", findingsFile: "研究文件", location: "位置", quote: "引用", caveat: "注意事项", artifacts: "产物", attention: "需关注", artifactCoverage: display.labels.artifactCoverage, storyWorkbench: display.labels.storyWorkbench, noRenderTargets: display.labels.noRenderTargets, nextActions: display.labels.nextActions, missingClaims: display.labels.missingClaims, affectedClaims: display.labels.affectedClaims, affectedSlides: display.labels.affectedSlides, notes: display.labels.notes, recommendedNextCommand: display.labels.recommendedNextCommand, none: display.labels.none }
423
+ const ja: Record<string, string> = { approval: "承認", claims: "クレーム", relations: "関係", inferred: "未確認", relation: "関係", from: "起点", to: "終点", rationale: "理由", strength: "強度", findingsFile: "調査ファイル", location: "場所", quote: "引用", caveat: "留意点", artifacts: "成果物", attention: "要確認", artifactCoverage: display.labels.artifactCoverage, storyWorkbench: display.labels.storyWorkbench, noRenderTargets: display.labels.noRenderTargets, nextActions: display.labels.nextActions, missingClaims: display.labels.missingClaims, affectedClaims: display.labels.affectedClaims, affectedSlides: display.labels.affectedSlides, notes: display.labels.notes, recommendedNextCommand: display.labels.recommendedNextCommand, none: display.labels.none }
424
+ const en: Record<string, string> = { approval: "approval", claims: "claims", relations: "relations", inferred: "unconfirmed", relation: "relation", from: "from", to: "to", rationale: "rationale", strength: "strength", findingsFile: "findings file", location: "location", quote: "quote", caveat: "caveat", artifacts: "artifacts", attention: "need attention", artifactCoverage: display.labels.artifactCoverage, storyWorkbench: display.labels.storyWorkbench, noRenderTargets: display.labels.noRenderTargets, nextActions: display.labels.nextActions, missingClaims: display.labels.missingClaims, affectedClaims: display.labels.affectedClaims, affectedSlides: display.labels.affectedSlides, notes: display.labels.notes, recommendedNextCommand: display.labels.recommendedNextCommand, none: display.labels.none }
299
425
  return (isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : en)[term] ?? term
300
426
  }
301
427
 
302
428
  function localizeValue(value: string, display: ValidatedNarrativeDisplayModel): string {
429
+ if (value === "no_target") return isChineseLanguage(display.language) ? "无 render target" : isJapaneseLanguage(display.language) ? "render target なし" : "no target"
303
430
  const zh: Record<string, string> = {
304
431
  current: "当前", stale: "已过期", missing: "缺失", approved: "已批准", ready_for_approval: "待批准", needs_research: "需要研究", needs_user_confirmation: "需要用户确认", blocked: "受阻", draft: "草稿",
305
432
  supported: "已支持", partial: "部分支持", weak: "弱支持", not_required: "无需证据", central: "核心", supporting: "支撑", background: "背景",