@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.
- package/lib/commands/narrative.ts +9 -3
- package/lib/commands/review.ts +2 -1
- package/lib/decks-state.ts +142 -4
- package/lib/narrative-state/display.ts +40 -0
- package/lib/narrative-state/map-html.ts +133 -6
- package/lib/narrative-state/map.ts +333 -16
- package/lib/narrative-state/render-plan.ts +254 -84
- package/package.json +1 -1
- package/tools/narrative-view.ts +11 -1
|
@@ -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.
|
package/lib/commands/review.ts
CHANGED
|
@@ -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.
|
package/lib/decks-state.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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:
|
|
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: "背景",
|