@cyber-dash-tech/revela 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -16
- package/README.zh-CN.md +16 -16
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/edit.ts +7 -5
- package/lib/commands/help.ts +5 -3
- package/lib/commands/inspect.ts +7 -5
- package/lib/commands/narrative.ts +160 -0
- package/lib/decks-state.ts +33 -0
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspect/prompt.ts +15 -2
- package/lib/inspect/requests.ts +21 -2
- package/lib/inspection-context/compile.ts +230 -10
- package/lib/inspection-context/match.ts +71 -1
- package/lib/inspection-context/project.ts +131 -8
- package/lib/inspection-context/result.ts +183 -0
- package/lib/narrative-state/coverage.ts +100 -0
- package/lib/narrative-state/display.ts +219 -0
- package/lib/narrative-state/executive-brief.ts +246 -0
- package/lib/narrative-state/hash.ts +9 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +54 -0
- package/lib/narrative-state/queries.ts +434 -0
- package/lib/narrative-state/readiness.ts +71 -1
- package/lib/narrative-state/render-plan.ts +44 -1
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +33 -0
- package/lib/refine/server.ts +91 -13
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +56 -2
- package/lib/workspace-state/types.ts +10 -1
- package/package.json +1 -1
- package/plugin.ts +33 -2
- package/tools/decks.ts +86 -1
- package/tools/edit.ts +10 -8
- package/tools/inspection-result.ts +37 -0
- package/tools/narrative-view.ts +84 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type { NarrativeMap, NarrativeMapClaim, NarrativeMapClaimRelation } from "./map"
|
|
2
|
+
import { emptyDisplayModel, isChineseLanguage, isJapaneseLanguage, relationKey, type ValidatedNarrativeDisplayModel } from "./display"
|
|
3
|
+
|
|
4
|
+
interface FlowNode {
|
|
5
|
+
id: string
|
|
6
|
+
claim: NarrativeMapClaim
|
|
7
|
+
title: string
|
|
8
|
+
displayCard?: ReturnType<ValidatedNarrativeDisplayModel["claimCards"]["get"]>
|
|
9
|
+
detailHtml: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function renderNarrativeMapHtml(map: NarrativeMap, display?: ValidatedNarrativeDisplayModel): string {
|
|
13
|
+
return renderNarrativeMapHtmlWithDisplay(map, display)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function renderNarrativeMapHtmlWithDisplay(map: NarrativeMap, display: ValidatedNarrativeDisplayModel = emptyDisplayModel("en")): string {
|
|
17
|
+
const title = `Revela Claim Flow - ${map.snapshot.narrativeHash}`
|
|
18
|
+
const nodes = buildFlowNodes(map, display)
|
|
19
|
+
const initial = nodes[0]
|
|
20
|
+
const inferredCount = map.claimRelations.filter((relation) => relation.inferred).length
|
|
21
|
+
const pageTitle = display.pageTitle ?? valueOrFallback(map.snapshot.thesis, map.snapshot.decisionAction || "Narrative claim flow")
|
|
22
|
+
const summaryLine = display.summaryLine ?? "Claims are the main path. Evidence, risks, gaps, objections, and artifact coverage stay in the selected-claim panel."
|
|
23
|
+
const nonCurrentArtifacts = map.artifactCoverage.filter((artifact) => artifact.coverageStatus !== "current").length
|
|
24
|
+
return `<!doctype html>
|
|
25
|
+
<html lang="${escapeAttr(display.language)}">
|
|
26
|
+
<head>
|
|
27
|
+
<meta charset="utf-8" />
|
|
28
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
29
|
+
<title>${escapeHtml(title)}</title>
|
|
30
|
+
<style>
|
|
31
|
+
:root { color-scheme:light; --bg:#f5f1e9; --paper:#fffdf8; --ink:#1c1917; --muted:#766d63; --line:#ded4c7; --accent:#d8612b; --good:#177044; --warn:#a56015; --bad:#a33434; --soft:#f7f0e7; --shadow:0 20px 54px rgba(54,43,31,.13); font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
|
32
|
+
* { box-sizing:border-box; }
|
|
33
|
+
body { margin:0; min-height:100vh; background:radial-gradient(circle at 12% 0,#fff7ed 0,transparent 26rem),radial-gradient(circle at 85% 10%,#edf4f0 0,transparent 24rem),var(--bg); color:var(--ink); }
|
|
34
|
+
.shell { max-width:1440px; margin:0 auto; padding:22px; }
|
|
35
|
+
.topbar { background:rgba(255,253,248,.9); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); padding:20px 22px; display:grid; grid-template-columns:minmax(0,1fr) auto; gap:18px; align-items:start; }
|
|
36
|
+
.eyebrow { margin:0; color:var(--accent); font-size:12px; font-weight:850; letter-spacing:.15em; text-transform:uppercase; }
|
|
37
|
+
h1 { margin:7px 0 0; max-width:860px; font-size:clamp(24px,3.2vw,42px); line-height:1.02; letter-spacing:-.05em; }
|
|
38
|
+
.summary { margin:10px 0 0; color:var(--muted); font-size:14px; line-height:1.45; max-width:920px; }
|
|
39
|
+
.pills { display:flex; flex-wrap:wrap; justify-content:flex-end; gap:8px; }
|
|
40
|
+
.pill { display:inline-flex; border-radius:999px; padding:7px 10px; font-size:12px; font-weight:780; border:1px solid var(--line); background:#fff; color:var(--muted); white-space:nowrap; }
|
|
41
|
+
.pill.current,.pill.supported { color:var(--good); background:#e8f4ed; border-color:#b9dcc8; }
|
|
42
|
+
.pill.stale,.pill.missing { color:var(--bad); background:#fbe7e7; border-color:#efb9b9; }
|
|
43
|
+
.pill.partial,.pill.weak,.pill.open { color:var(--warn); background:#fff1dc; border-color:#edd0a5; }
|
|
44
|
+
.layout { display:grid; grid-template-columns:minmax(0,1fr) minmax(360px,430px); gap:18px; margin-top:18px; align-items:start; }
|
|
45
|
+
.flow,.detail-panel { background:rgba(255,253,248,.92); border:1px solid var(--line); border-radius:24px; box-shadow:var(--shadow); }
|
|
46
|
+
.flow { padding:20px; }
|
|
47
|
+
.flow-head { display:flex; justify-content:space-between; gap:14px; align-items:flex-start; margin-bottom:18px; }
|
|
48
|
+
.flow-head h2 { margin:0; font-size:18px; letter-spacing:-.025em; }
|
|
49
|
+
.flow-note { margin:4px 0 0; color:var(--muted); font-size:13px; line-height:1.45; }
|
|
50
|
+
.claim-list { display:flex; flex-direction:column; gap:0; }
|
|
51
|
+
.claim-step { display:grid; grid-template-columns:42px minmax(0,1fr); gap:14px; }
|
|
52
|
+
.step-rail { display:flex; flex-direction:column; align-items:center; }
|
|
53
|
+
.step-dot { width:32px; height:32px; border-radius:999px; display:grid; place-items:center; background:#fff; border:1px solid var(--line); color:var(--muted); font-size:12px; font-weight:850; }
|
|
54
|
+
.step-line { flex:1; width:2px; min-height:30px; background:linear-gradient(var(--line),rgba(222,212,199,.25)); margin:8px 0; }
|
|
55
|
+
.claim-step:last-child .step-line { display:none; }
|
|
56
|
+
.claim-card { width:100%; text-align:left; cursor:pointer; border:1px solid var(--line); border-left:6px solid var(--good); background:#fff; color:var(--ink); border-radius:18px; padding:15px 16px; margin-bottom:16px; box-shadow:0 10px 26px rgba(67,49,31,.08); transition:border-color .15s ease,box-shadow .15s ease,transform .15s ease; }
|
|
57
|
+
.claim-card:hover,.claim-card.active { border-color:var(--accent); box-shadow:0 14px 32px rgba(143,62,24,.15); transform:translateY(-1px); }
|
|
58
|
+
.claim-card.partial,.claim-card.weak { border-left-color:var(--warn); }
|
|
59
|
+
.claim-card.missing { border-left-color:var(--bad); }
|
|
60
|
+
.claim-title { display:block; font-size:18px; font-weight:850; line-height:1.2; letter-spacing:-.018em; }
|
|
61
|
+
.claim-meta { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
|
|
62
|
+
.tag { display:inline-flex; border-radius:999px; padding:4px 8px; background:var(--soft); color:var(--muted); font-size:11px; font-weight:800; }
|
|
63
|
+
.claim-sections { margin-top:13px; display:grid; gap:9px; }
|
|
64
|
+
.claim-section { border-top:1px solid #eee4d8; padding-top:9px; }
|
|
65
|
+
.section-label { display:block; margin-bottom:3px; color:var(--accent); font-size:10px; font-weight:900; letter-spacing:.08em; text-transform:uppercase; }
|
|
66
|
+
.section-text { display:block; color:#51483f; font-size:13px; line-height:1.46; white-space:pre-line; }
|
|
67
|
+
.relation-strip { margin-top:12px; display:grid; gap:7px; }
|
|
68
|
+
.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
|
+
.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; }
|
|
70
|
+
.relation-target { display:block; color:#51483f; font-weight:720; }
|
|
71
|
+
.relation-note { display:block; margin-top:3px; color:var(--muted); }
|
|
72
|
+
.relation.inferred .relation-badge { background:#f4f0ea; border-color:var(--line); color:var(--muted); }
|
|
73
|
+
.detail-panel { position:sticky; top:18px; max-height:calc(100vh - 36px); overflow:hidden; display:flex; flex-direction:column; }
|
|
74
|
+
.detail-head { padding:20px 20px 14px; border-bottom:1px solid var(--line); }
|
|
75
|
+
.detail-title { margin:7px 0 0; font-size:22px; line-height:1.12; letter-spacing:-.035em; }
|
|
76
|
+
.detail-sub { margin-top:8px; color:var(--muted); font-size:13px; line-height:1.4; }
|
|
77
|
+
.detail-body { padding:16px 20px 22px; overflow:auto; }
|
|
78
|
+
.detail-card { border:1px solid var(--line); border-radius:16px; padding:13px; background:#fff; margin-bottom:10px; }
|
|
79
|
+
.detail-card h3 { margin:0 0 8px; font-size:13px; letter-spacing:-.01em; }
|
|
80
|
+
.detail-card p { margin:0; color:var(--muted); line-height:1.45; font-size:13px; }
|
|
81
|
+
.empty { color:var(--muted); font-style:italic; }
|
|
82
|
+
.hidden-detail { display:none; }
|
|
83
|
+
@media (max-width:1100px) { .layout { grid-template-columns:1fr; } .detail-panel { position:static; max-height:none; } .topbar { grid-template-columns:1fr; } .pills { justify-content:flex-start; } }
|
|
84
|
+
@media (max-width:680px) { .shell { padding:12px; } .topbar,.flow,.detail-panel { border-radius:18px; } .claim-step { grid-template-columns:30px minmax(0,1fr); gap:10px; } .step-dot { width:26px; height:26px; font-size:11px; } .claim-title { font-size:16px; } }
|
|
85
|
+
</style>
|
|
86
|
+
</head>
|
|
87
|
+
<body>
|
|
88
|
+
<main class="shell">
|
|
89
|
+
<header class="topbar">
|
|
90
|
+
<div>
|
|
91
|
+
<p class="eyebrow">${escapeHtml(display.labels.eyebrow)}</p>
|
|
92
|
+
<h1>${escapeHtml(pageTitle)}</h1>
|
|
93
|
+
<p class="summary">${escapeHtml(summaryLine)}</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="pills">
|
|
96
|
+
<span class="pill ${escapeAttr(map.snapshot.approval)}">${escapeHtml(systemTerm("approval", display))}: ${escapeHtml(localizeValue(map.snapshot.approval, display))}</span>
|
|
97
|
+
<span class="pill">${escapeHtml(display.labels.status)}: ${escapeHtml(localizeValue(map.snapshot.status, display))}</span>
|
|
98
|
+
<span class="pill supported">${escapeHtml(systemTerm("claims", display))}: ${nodes.length}</span>
|
|
99
|
+
<span class="pill ${inferredCount > 0 ? "open" : "current"}">${escapeHtml(systemTerm("relations", display))}: ${map.claimRelations.length}${inferredCount > 0 ? ` (${inferredCount} ${escapeHtml(systemTerm("inferred", display))})` : ""}</span>
|
|
100
|
+
<span class="pill ${nonCurrentArtifacts > 0 ? "partial" : "current"}">${escapeHtml(systemTerm("artifacts", display))}: ${map.artifactCoverage.length}${nonCurrentArtifacts > 0 ? ` (${nonCurrentArtifacts} ${escapeHtml(systemTerm("attention", display))})` : ""}</span>
|
|
101
|
+
</div>
|
|
102
|
+
</header>
|
|
103
|
+
<div class="layout">
|
|
104
|
+
<section class="flow" aria-label="Narrative claim flow board">
|
|
105
|
+
<div class="flow-head">
|
|
106
|
+
<div>
|
|
107
|
+
<h2>${escapeHtml(display.labels.claimFlow)}</h2>
|
|
108
|
+
<p class="flow-note">${escapeHtml(display.labels.flowNote)}</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="claim-list">
|
|
112
|
+
${nodes.length ? nodes.map((node, index) => renderStep(node, map, display, index, index === 0)).join("") : emptyCard(display.labels.claimFlow, display.labels.noClaims)}
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
<aside class="detail-panel">
|
|
116
|
+
<div class="detail-head">
|
|
117
|
+
<p class="eyebrow">${escapeHtml(display.labels.selectedClaim)}</p>
|
|
118
|
+
<h2 class="detail-title" id="detail-title">${escapeHtml(initial?.title ?? display.labels.noClaims)}</h2>
|
|
119
|
+
<div class="detail-sub" id="detail-sub">${escapeHtml(initial ? claimSubtitle(initial.claim, display) : "Run /revela init to create narrative claims.")}</div>
|
|
120
|
+
</div>
|
|
121
|
+
<div class="detail-body" id="detail-body">${initial?.detailHtml ?? emptyCard(display.labels.claimFlow, display.labels.noClaims)}</div>
|
|
122
|
+
</aside>
|
|
123
|
+
</div>
|
|
124
|
+
</main>
|
|
125
|
+
<div class="hidden-detail">
|
|
126
|
+
${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
|
+
</div>
|
|
128
|
+
<script>
|
|
129
|
+
const buttons = Array.from(document.querySelectorAll('.claim-card'));
|
|
130
|
+
const title = document.getElementById('detail-title');
|
|
131
|
+
const sub = document.getElementById('detail-sub');
|
|
132
|
+
const body = document.getElementById('detail-body');
|
|
133
|
+
function selectClaim(id) {
|
|
134
|
+
const template = document.getElementById('detail-' + CSS.escape(id));
|
|
135
|
+
if (!template) return;
|
|
136
|
+
title.textContent = template.dataset.title || '';
|
|
137
|
+
sub.textContent = template.dataset.subtitle || '';
|
|
138
|
+
body.innerHTML = template.innerHTML;
|
|
139
|
+
buttons.forEach((button) => button.classList.toggle('active', button.dataset.nodeId === id));
|
|
140
|
+
}
|
|
141
|
+
buttons.forEach((button) => button.addEventListener('click', () => selectClaim(button.dataset.nodeId)));
|
|
142
|
+
</script>
|
|
143
|
+
</body>
|
|
144
|
+
</html>`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildFlowNodes(map: NarrativeMap, display: ValidatedNarrativeDisplayModel): FlowNode[] {
|
|
148
|
+
return allClaims(map).map((claim) => ({
|
|
149
|
+
id: nodeId(claim.id),
|
|
150
|
+
claim,
|
|
151
|
+
title: display.claimCards.get(claim.id)?.displayTitle ?? claim.text,
|
|
152
|
+
displayCard: display.claimCards.get(claim.id),
|
|
153
|
+
detailHtml: claimDetail(claim, map, display),
|
|
154
|
+
}))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function renderStep(node: FlowNode, map: NarrativeMap, display: ValidatedNarrativeDisplayModel, index: number, active: boolean): string {
|
|
158
|
+
const outgoing = map.claimRelations.filter((relation) => relation.fromClaimId === node.claim.id)
|
|
159
|
+
return `<div class="claim-step">
|
|
160
|
+
<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">
|
|
162
|
+
<span class="claim-title">${escapeHtml(node.title)}</span>
|
|
163
|
+
<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
|
+
${renderDisplayCardSummary(node.displayCard, display)}
|
|
165
|
+
${outgoing.length ? `<span class="relation-strip">${outgoing.map((relation) => renderOutgoingRelation(relation, display)).join("")}</span>` : ""}
|
|
166
|
+
</button>
|
|
167
|
+
</div>`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function renderDisplayCardSummary(card: FlowNode["displayCard"], display: ValidatedNarrativeDisplayModel): string {
|
|
171
|
+
if (!card) return ""
|
|
172
|
+
const labels = sectionLabels(display)
|
|
173
|
+
const rows: Array<[string, string | undefined]> = [
|
|
174
|
+
[labels.role, card.roleLabel],
|
|
175
|
+
[labels.narrativeJob, card.narrativeJob],
|
|
176
|
+
[labels.evidenceSummary, card.evidenceSummary],
|
|
177
|
+
[labels.riskOrGapSummary, card.riskOrGapSummary],
|
|
178
|
+
]
|
|
179
|
+
if (rows.length === 0) return ""
|
|
180
|
+
return `<span class="claim-sections">${rows.filter(([, value]) => Boolean(value)).map(([label, value]) => `<span class="claim-section"><span class="section-label">${escapeHtml(label)}</span><span class="section-text">${escapeHtml(value)}</span></span>`).join("")}</span>`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function renderOutgoingRelation(relation: NarrativeMapClaimRelation, display: ValidatedNarrativeDisplayModel): string {
|
|
184
|
+
const label = relationDisplayLabel(relation, display, true)
|
|
185
|
+
const target = displayClaimText(relation.toClaimId, relation.toClaimText, display)
|
|
186
|
+
const rationale = relationDisplayRationale(relation, display)
|
|
187
|
+
return `<span class="relation${relation.inferred ? " inferred" : ""}"><span class="relation-badge">${escapeHtml(label)}</span><span><span class="relation-target">${escapeHtml(systemTerm("to", display))}: ${escapeHtml(shorten(target, 120))}</span>${rationale ? `<span class="relation-note">${escapeHtml(systemTerm("rationale", display))}: ${escapeHtml(rationale)}</span>` : ""}</span></span>`
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function claimDetail(claim: NarrativeMapClaim, map: NarrativeMap, display: ValidatedNarrativeDisplayModel): string {
|
|
191
|
+
const incoming = map.claimRelations.filter((relation) => relation.toClaimId === claim.id)
|
|
192
|
+
const outgoing = map.claimRelations.filter((relation) => relation.fromClaimId === claim.id)
|
|
193
|
+
const objections = map.objections.filter((item) => item.claimId === claim.id)
|
|
194
|
+
const risks = map.risks.filter((item) => item.claimId === claim.id)
|
|
195
|
+
const gaps = map.researchGaps.filter((item) => item.targetType === "claim" && item.targetId === claim.id)
|
|
196
|
+
const slideRefs = map.artifactCoverage.flatMap((artifact) => artifact.slideRefs.filter((ref) => ref.claimId === claim.id).map((ref) => `${artifact.type} slide ${ref.slideIndex} (${ref.role}, ${ref.match}/${ref.location}, coverage:${artifact.coverageStatus})`))
|
|
197
|
+
const coverageGaps = map.artifactCoverage.filter((artifact) => artifact.missingClaimIds.includes(claim.id) || artifact.affectedClaimIds.includes(claim.id))
|
|
198
|
+
const card = display.claimCards.get(claim.id)
|
|
199
|
+
return detailCards([
|
|
200
|
+
[display.labels.claim, claim.text],
|
|
201
|
+
...(card?.narrativeJob ? [[card.roleLabel || display.labels.claim, card.narrativeJob] as [string, string]] : []),
|
|
202
|
+
...(card?.evidenceSummary ? [[display.labels.evidence, card.evidenceSummary] as [string, string]] : []),
|
|
203
|
+
...(card?.riskOrGapSummary ? [[display.labels.researchGaps, card.riskOrGapSummary] as [string, string]] : []),
|
|
204
|
+
[display.labels.claimId, claim.id],
|
|
205
|
+
[display.labels.status, `${localizeValue(claim.evidenceStatus, display)} / ${localizeValue(claim.importance, display)} / ${localizeValue(claim.kind, display)}`],
|
|
206
|
+
...(claim.supportedScope ? [[display.labels.supportedScope, claim.supportedScope] as [string, string]] : []),
|
|
207
|
+
...(claim.unsupportedScope ? [[display.labels.unsupportedScope, claim.unsupportedScope] as [string, string]] : []),
|
|
208
|
+
[display.labels.incomingRelations, incoming.length ? incoming.map((relation) => relationText(relation, display)).join("<br><br>") : display.labels.none],
|
|
209
|
+
[display.labels.outgoingRelations, outgoing.length ? outgoing.map((relation) => relationText(relation, display)).join("<br><br>") : display.labels.none],
|
|
210
|
+
...(claim.evidence.length ? claim.evidence.map((evidence) => [`${display.labels.evidence}: ${evidence.source}`, evidenceDetailText(evidence, display)] as [string, string]) : [[display.labels.evidence, display.labels.none] as [string, string]]),
|
|
211
|
+
...(objections.length ? [[display.labels.objections, objections.map((item) => `${item.text}${item.response ? ` -> ${item.response}` : ""}`).join("<br>")] as [string, string]] : []),
|
|
212
|
+
...(risks.length ? [[display.labels.risks, risks.map((item) => `${item.text}${item.mitigation ? ` -> ${item.mitigation}` : ""}`).join("<br>")] as [string, string]] : []),
|
|
213
|
+
...(gaps.length ? [[display.labels.researchGaps, gaps.map((item) => `${item.question} [${item.status}/${item.priority}]`).join("<br>")] as [string, string]] : []),
|
|
214
|
+
...(slideRefs.length ? [[display.labels.coveredSlides, slideRefs.map((ref) => localizeSlideRef(ref, display)).join("<br>")] as [string, string]] : []),
|
|
215
|
+
...(coverageGaps.length ? [[systemTerm("artifactCoverage", display), coverageGaps.map((artifact) => `${artifact.type}: ${artifact.coverageStatus}${artifact.staleReasons.length ? ` - ${artifact.staleReasons.join("; ")}` : ""}`).join("<br>")] as [string, string]] : []),
|
|
216
|
+
])
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function relationText(relation: NarrativeMapClaimRelation, display: ValidatedNarrativeDisplayModel): string {
|
|
220
|
+
const from = displayClaimText(relation.fromClaimId, relation.fromClaimText, display)
|
|
221
|
+
const to = displayClaimText(relation.toClaimId, relation.toClaimText, display)
|
|
222
|
+
const label = relationDisplayLabel(relation, display, false)
|
|
223
|
+
const rationale = relationDisplayRationale(relation, display)
|
|
224
|
+
return `${systemTerm("relation", display)}: ${label}${relation.inferred ? ` (${systemTerm("inferred", display)})` : ""}<br>${systemTerm("from", display)}: ${from}<br>${systemTerm("to", display)}: ${to}${rationale ? `<br>${systemTerm("rationale", display)}: ${rationale}` : ""}`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function displayClaimText(claimId: string, fallback: string | undefined, display: ValidatedNarrativeDisplayModel): string {
|
|
228
|
+
return display.claimCards.get(claimId)?.displayTitle ?? fallback ?? claimId
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function relationDisplayLabel(relation: NarrativeMapClaimRelation, display: ValidatedNarrativeDisplayModel, _includeInferred: boolean): string {
|
|
232
|
+
if (relation.inferred) return inferredRelationLabel(display)
|
|
233
|
+
const label = display.relations.get(relationKey(relation))?.displayLabel ?? localizeValue(relation.relation, display)
|
|
234
|
+
return label
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function relationDisplayRationale(relation: NarrativeMapClaimRelation, display: ValidatedNarrativeDisplayModel): string | undefined {
|
|
238
|
+
const displayRationale = display.relations.get(relationKey(relation))?.displayRationale
|
|
239
|
+
if (displayRationale) return displayRationale
|
|
240
|
+
if (relation.inferred) return inferredRationale(display)
|
|
241
|
+
return relation.rationale ?? missingRationale(display)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function inferredRationale(display: ValidatedNarrativeDisplayModel): string {
|
|
245
|
+
if (isChineseLanguage(display.language)) return "仅表示两个主张在当前叙事顺序中相邻;系统未判断因果、支撑或依赖关系。需要在 claimRelations 中写入客观 rationale 后才算确认。"
|
|
246
|
+
if (isJapaneseLanguage(display.language)) return "現在のナラティブ順序で隣接していることだけを示します。因果、裏付け、依存関係は判断していません。確認するには claimRelations に客観的な rationale を記録してください。"
|
|
247
|
+
return "Only indicates that the two claims are adjacent in the current narrative order; the system has not judged causality, support, or dependency. Record objective rationale in claimRelations to confirm it."
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function inferredRelationLabel(display: ValidatedNarrativeDisplayModel): string {
|
|
251
|
+
if (isChineseLanguage(display.language)) return "未确认顺序提示"
|
|
252
|
+
if (isJapaneseLanguage(display.language)) return "未確認の順序メモ"
|
|
253
|
+
return "unconfirmed order note"
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function missingRationale(display: ValidatedNarrativeDisplayModel): string {
|
|
257
|
+
if (isChineseLanguage(display.language)) return "因果依据未记录。"
|
|
258
|
+
if (isJapaneseLanguage(display.language)) return "因果関係の根拠は記録されていません。"
|
|
259
|
+
return "Causal rationale is not recorded."
|
|
260
|
+
}
|
|
261
|
+
|
|
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("")
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function emptyCard(label: string, value: string): string {
|
|
267
|
+
return `<div class="detail-card"><h3>${escapeHtml(label)}</h3><p class="empty">${escapeHtml(value)}</p></div>`
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function evidenceDetailText(evidence: NarrativeMapClaim["evidence"][number], display: ValidatedNarrativeDisplayModel): string {
|
|
271
|
+
return [
|
|
272
|
+
`${systemTerm("strength", display)}: ${localizeValue(evidence.strength, display)}`,
|
|
273
|
+
evidence.findingsFile ? `${systemTerm("findingsFile", display)}: ${evidence.findingsFile}` : "",
|
|
274
|
+
evidence.location ? `${systemTerm("location", display)}: ${evidence.location}` : "",
|
|
275
|
+
evidence.quote ? `${systemTerm("quote", display)}: ${evidence.quote}` : "",
|
|
276
|
+
evidence.unsupportedScope ? `${display.labels.unsupportedScope}: ${evidence.unsupportedScope}` : "",
|
|
277
|
+
evidence.caveat ? `${systemTerm("caveat", display)}: ${evidence.caveat}` : "",
|
|
278
|
+
].filter(Boolean).join(" | ")
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function allClaims(map: NarrativeMap): NarrativeMapClaim[] {
|
|
282
|
+
return map.claimFlow.length > 0 ? map.claimFlow : map.claims.supported.concat(map.claims.partial, map.claims.weak, map.claims.missing, map.claims.not_required)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function claimSubtitle(claim: NarrativeMapClaim, display: ValidatedNarrativeDisplayModel): string {
|
|
286
|
+
return `${localizeValue(claim.kind, display)} / ${localizeValue(claim.importance, display)} / ${localizeValue(claim.evidenceStatus, display)}`
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function sectionLabels(display: ValidatedNarrativeDisplayModel): Record<string, string> {
|
|
290
|
+
if (isChineseLanguage(display.language)) return { role: "角色", narrativeJob: "叙事任务", evidenceSummary: "证据摘要", riskOrGapSummary: "风险/缺口" }
|
|
291
|
+
if (isJapaneseLanguage(display.language)) return { role: "役割", narrativeJob: "ナラティブ上の役割", evidenceSummary: "根拠の要約", riskOrGapSummary: "リスク/ギャップ" }
|
|
292
|
+
return { role: "Role", narrativeJob: "Narrative job", evidenceSummary: "Evidence summary", riskOrGapSummary: "Risk / gap" }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
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" }
|
|
299
|
+
return (isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : en)[term] ?? term
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function localizeValue(value: string, display: ValidatedNarrativeDisplayModel): string {
|
|
303
|
+
const zh: Record<string, string> = {
|
|
304
|
+
current: "当前", stale: "已过期", missing: "缺失", approved: "已批准", ready_for_approval: "待批准", needs_research: "需要研究", needs_user_confirmation: "需要用户确认", blocked: "受阻", draft: "草稿",
|
|
305
|
+
supported: "已支持", partial: "部分支持", weak: "弱支持", not_required: "无需证据", central: "核心", supporting: "支撑", background: "背景",
|
|
306
|
+
context: "背景", problem: "问题", opportunity: "机会", evidence: "证据", recommendation: "建议", risk: "风险", assumption: "假设", ask: "请求",
|
|
307
|
+
leads_to: "推进到", supports: "支持", depends_on: "依赖", contrasts_with: "对比", constrains: "约束", answers: "回应", strong: "强", medium: "中", low: "低",
|
|
308
|
+
}
|
|
309
|
+
const ja: Record<string, string> = {
|
|
310
|
+
current: "現行", stale: "古い", missing: "不足", approved: "承認済み", ready_for_approval: "承認待ち", needs_research: "調査が必要", needs_user_confirmation: "ユーザー確認が必要", blocked: "ブロック", draft: "下書き",
|
|
311
|
+
supported: "裏付けあり", partial: "一部裏付け", weak: "弱い裏付け", not_required: "根拠不要", central: "中心", supporting: "補助", background: "背景",
|
|
312
|
+
context: "文脈", problem: "課題", opportunity: "機会", evidence: "根拠", recommendation: "提案", risk: "リスク", assumption: "仮定", ask: "依頼",
|
|
313
|
+
leads_to: "つながる", supports: "支える", depends_on: "依存", contrasts_with: "対比", constrains: "制約", answers: "答える", strong: "強", medium: "中", low: "低",
|
|
314
|
+
}
|
|
315
|
+
const table = isChineseLanguage(display.language) ? zh : isJapaneseLanguage(display.language) ? ja : {}
|
|
316
|
+
return table[value] ?? value
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function localizeSlideRef(value: string, display: ValidatedNarrativeDisplayModel): string {
|
|
320
|
+
if (isChineseLanguage(display.language)) return value.replace(/slide/g, "页面")
|
|
321
|
+
if (isJapaneseLanguage(display.language)) return value.replace(/slide/g, "スライド")
|
|
322
|
+
return value
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function nodeId(id: string): string {
|
|
326
|
+
return `claim-${id}`.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function shorten(value: string | undefined, max: number): string {
|
|
330
|
+
const text = valueOrFallback(value, "-")
|
|
331
|
+
return text.length > max ? `${text.slice(0, max - 1)}...` : text
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function valueOrFallback(value: string | undefined, fallback: string): string {
|
|
335
|
+
return value?.trim() || fallback
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function allowBreaks(value: string): string {
|
|
339
|
+
return escapeHtml(value).replace(/<br>/g, "<br>")
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function escapeHtml(value: string | undefined): string {
|
|
343
|
+
return (value ?? "").replace(/[&<>"]/g, (ch) => ({ "&": "&", "<": "<", ">": ">", "\"": """ }[ch] ?? ch))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function escapeAttr(value: string | undefined): string {
|
|
347
|
+
return escapeHtml(value).replace(/[^a-zA-Z0-9_-]/g, "_")
|
|
348
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
import type { RenderTarget } from "../workspace-state/types"
|
|
3
|
+
import { computeNarrativeHash } from "./hash"
|
|
4
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
5
|
+
import {
|
|
6
|
+
getArtifactClaimRefs,
|
|
7
|
+
getClaimEvidenceBoard,
|
|
8
|
+
getObjectionRiskClaimIndex,
|
|
9
|
+
type ClaimEvidenceRecord,
|
|
10
|
+
type ClaimEvidenceBindingRecord,
|
|
11
|
+
type ClaimSlideRef,
|
|
12
|
+
} from "./queries"
|
|
13
|
+
import { reviewNarrativeState } from "./readiness"
|
|
14
|
+
import type { NarrativeClaim, NarrativeClaimRelation, NarrativeResearchGap, NarrativeStateV1 } from "./types"
|
|
15
|
+
|
|
16
|
+
export interface NarrativeMap {
|
|
17
|
+
version: 1
|
|
18
|
+
snapshot: NarrativeMapSnapshot
|
|
19
|
+
claims: Record<NarrativeClaim["evidenceStatus"], NarrativeMapClaim[]>
|
|
20
|
+
claimFlow: NarrativeMapClaim[]
|
|
21
|
+
claimRelations: NarrativeMapClaimRelation[]
|
|
22
|
+
objections: NarrativeMapObjection[]
|
|
23
|
+
risks: NarrativeMapRisk[]
|
|
24
|
+
researchGaps: NarrativeMapResearchGap[]
|
|
25
|
+
artifactCoverage: NarrativeMapArtifact[]
|
|
26
|
+
nextActions: string[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface NarrativeMapSnapshot {
|
|
30
|
+
narrativeId: string
|
|
31
|
+
narrativeHash: string
|
|
32
|
+
status: NarrativeStateV1["status"]
|
|
33
|
+
primaryAudience: string
|
|
34
|
+
beliefBefore: string
|
|
35
|
+
beliefAfter: string
|
|
36
|
+
decisionAction: string
|
|
37
|
+
thesis?: string
|
|
38
|
+
approval: "current" | "stale" | "missing"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type NarrativeMapClaim = ClaimEvidenceRecord
|
|
42
|
+
|
|
43
|
+
export type NarrativeMapEvidence = ClaimEvidenceBindingRecord
|
|
44
|
+
|
|
45
|
+
export interface NarrativeMapClaimRelation extends NarrativeClaimRelation {
|
|
46
|
+
fromClaimText?: string
|
|
47
|
+
toClaimText?: string
|
|
48
|
+
inferred?: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface NarrativeMapObjection {
|
|
52
|
+
id: string
|
|
53
|
+
text: string
|
|
54
|
+
claimId?: string
|
|
55
|
+
claimText?: string
|
|
56
|
+
priority: "high" | "medium" | "low"
|
|
57
|
+
response?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NarrativeMapRisk {
|
|
61
|
+
id: string
|
|
62
|
+
text: string
|
|
63
|
+
claimId?: string
|
|
64
|
+
claimText?: string
|
|
65
|
+
severity: "high" | "medium" | "low"
|
|
66
|
+
mitigation?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type NarrativeMapResearchGap = NarrativeResearchGap & { targetText?: string }
|
|
70
|
+
|
|
71
|
+
export interface NarrativeMapArtifact {
|
|
72
|
+
id: string
|
|
73
|
+
type: RenderTarget["type"]
|
|
74
|
+
outputPath?: string
|
|
75
|
+
contractStatus?: RenderTarget["contractStatus"]
|
|
76
|
+
sourceNodeIds: string[]
|
|
77
|
+
claimIds: string[]
|
|
78
|
+
narrativeIds: string[]
|
|
79
|
+
slideRefs: ClaimSlideRef[]
|
|
80
|
+
coverageStatus: "current" | "stale" | "partial" | "missing"
|
|
81
|
+
affectedClaimIds: string[]
|
|
82
|
+
missingClaimIds: string[]
|
|
83
|
+
staleReasons: string[]
|
|
84
|
+
stale: boolean
|
|
85
|
+
staleReason?: string
|
|
86
|
+
note?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildNarrativeMap(state: DecksState): NarrativeMap {
|
|
90
|
+
const narrative = state.narrative ?? normalizeNarrativeState(state)
|
|
91
|
+
const reviewed = reviewNarrativeState({ ...state, narrative })
|
|
92
|
+
const readiness = reviewed.result
|
|
93
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
94
|
+
const board = getClaimEvidenceBoard({ ...state, narrative })
|
|
95
|
+
const objectionRisk = getObjectionRiskClaimIndex({ ...state, narrative })
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
version: 1,
|
|
99
|
+
snapshot: {
|
|
100
|
+
narrativeId: narrative.id,
|
|
101
|
+
narrativeHash,
|
|
102
|
+
status: readiness.status === "approved" ? "approved" : narrative.status,
|
|
103
|
+
primaryAudience: narrative.audience.primary,
|
|
104
|
+
beliefBefore: narrative.audience.beliefBefore,
|
|
105
|
+
beliefAfter: narrative.audience.beliefAfter,
|
|
106
|
+
decisionAction: narrative.decision.action,
|
|
107
|
+
thesis: narrative.thesis?.statement,
|
|
108
|
+
approval: readiness.approval?.current ? "current" : readiness.approval?.stale ? "stale" : "missing",
|
|
109
|
+
},
|
|
110
|
+
claims: board.claims,
|
|
111
|
+
claimFlow: claimRecordsInNarrativeOrder(narrative, board.claims),
|
|
112
|
+
claimRelations: mapClaimRelations(narrative),
|
|
113
|
+
objections: objectionRisk.objections,
|
|
114
|
+
risks: objectionRisk.risks,
|
|
115
|
+
researchGaps: (narrative.researchGaps ?? []).map((gap) => ({ ...gap, targetText: targetText(narrative, gap) })),
|
|
116
|
+
artifactCoverage: getArtifactClaimRefs({ ...state, narrative }).map((artifact) => ({
|
|
117
|
+
id: artifact.artifactId,
|
|
118
|
+
type: artifact.type,
|
|
119
|
+
outputPath: artifact.outputPath,
|
|
120
|
+
contractStatus: artifact.contractStatus,
|
|
121
|
+
sourceNodeIds: artifact.sourceNodeIds,
|
|
122
|
+
claimIds: artifact.claimIds,
|
|
123
|
+
narrativeIds: artifact.narrativeIds,
|
|
124
|
+
slideRefs: artifact.slideRefs,
|
|
125
|
+
coverageStatus: artifact.coverageStatus,
|
|
126
|
+
affectedClaimIds: artifact.affectedClaimIds,
|
|
127
|
+
missingClaimIds: artifact.missingClaimIds,
|
|
128
|
+
staleReasons: artifact.staleReasons,
|
|
129
|
+
stale: artifact.stale,
|
|
130
|
+
staleReason: artifact.staleReason,
|
|
131
|
+
note: artifact.note,
|
|
132
|
+
})),
|
|
133
|
+
nextActions: readiness.nextActions,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function claimRecordsInNarrativeOrder(narrative: NarrativeStateV1, claimsByStatus: Record<NarrativeClaim["evidenceStatus"], NarrativeMapClaim[]>): NarrativeMapClaim[] {
|
|
138
|
+
const records = new Map(Object.values(claimsByStatus).flat().map((claim) => [claim.id, claim]))
|
|
139
|
+
return narrative.claims.map((claim) => records.get(claim.id)).filter((claim): claim is NarrativeMapClaim => Boolean(claim))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function mapClaimRelations(narrative: NarrativeStateV1): NarrativeMapClaimRelation[] {
|
|
143
|
+
const explicit = (narrative.claimRelations ?? []).map((relation) => withClaimRelationText(narrative, relation, false))
|
|
144
|
+
if (explicit.length > 0) return explicit
|
|
145
|
+
const flowClaims = narrative.claims.filter((claim) => claim.importance === "central" || claim.kind === "recommendation" || claim.kind === "ask" || claim.kind === "problem" || claim.kind === "evidence")
|
|
146
|
+
return flowClaims.slice(0, -1).map((claim, index) => withClaimRelationText(narrative, {
|
|
147
|
+
id: `inferred:${claim.id}:${flowClaims[index + 1].id}`,
|
|
148
|
+
fromClaimId: claim.id,
|
|
149
|
+
toClaimId: flowClaims[index + 1].id,
|
|
150
|
+
relation: inferredRelation(claim.kind, flowClaims[index + 1].kind),
|
|
151
|
+
rationale: "Inferred from claim order and claim kind for display only.",
|
|
152
|
+
}, true))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function withClaimRelationText(narrative: NarrativeStateV1, relation: NarrativeClaimRelation, inferred: boolean): NarrativeMapClaimRelation {
|
|
156
|
+
return {
|
|
157
|
+
...relation,
|
|
158
|
+
fromClaimText: narrative.claims.find((claim) => claim.id === relation.fromClaimId)?.text,
|
|
159
|
+
toClaimText: narrative.claims.find((claim) => claim.id === relation.toClaimId)?.text,
|
|
160
|
+
inferred,
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function inferredRelation(fromKind: NarrativeClaim["kind"], toKind: NarrativeClaim["kind"]): NarrativeClaimRelation["relation"] {
|
|
165
|
+
if (toKind === "risk") return "constrains"
|
|
166
|
+
if (toKind === "ask" || toKind === "recommendation") return "leads_to"
|
|
167
|
+
if (fromKind === "problem" && toKind === "evidence") return "supports"
|
|
168
|
+
return "leads_to"
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function formatNarrativeMap(map: NarrativeMap): string {
|
|
172
|
+
const lines: string[] = []
|
|
173
|
+
lines.push("## Narrative Snapshot")
|
|
174
|
+
lines.push(`- Status: ${map.snapshot.status}`)
|
|
175
|
+
lines.push(`- Approval: ${map.snapshot.approval}`)
|
|
176
|
+
lines.push(`- Narrative hash: ${map.snapshot.narrativeHash}`)
|
|
177
|
+
lines.push(`- Audience: ${valueOrDash(map.snapshot.primaryAudience)}`)
|
|
178
|
+
lines.push(`- Belief before: ${valueOrDash(map.snapshot.beliefBefore)}`)
|
|
179
|
+
lines.push(`- Belief after: ${valueOrDash(map.snapshot.beliefAfter)}`)
|
|
180
|
+
lines.push(`- Decision/action: ${valueOrDash(map.snapshot.decisionAction)}`)
|
|
181
|
+
lines.push(`- Thesis: ${valueOrDash(map.snapshot.thesis)}`)
|
|
182
|
+
|
|
183
|
+
lines.push("", "## Claim Evidence Board")
|
|
184
|
+
for (const status of ["supported", "partial", "weak", "missing", "not_required"] as const) {
|
|
185
|
+
const claims = map.claims[status]
|
|
186
|
+
lines.push(`### ${status} (${claims.length})`)
|
|
187
|
+
if (claims.length === 0) {
|
|
188
|
+
lines.push("- None")
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
for (const claim of claims) {
|
|
192
|
+
lines.push(`- ${claim.text} [${claim.importance}/${claim.kind}]`)
|
|
193
|
+
if (claim.supportedScope) lines.push(` Supported scope: ${claim.supportedScope}`)
|
|
194
|
+
if (claim.unsupportedScope) lines.push(` Unsupported scope: ${claim.unsupportedScope}`)
|
|
195
|
+
for (const caveat of claim.caveats) lines.push(` Caveat: ${caveat}`)
|
|
196
|
+
if (claim.evidence.length === 0) lines.push(" Evidence: none")
|
|
197
|
+
for (const evidence of claim.evidence) {
|
|
198
|
+
lines.push(` Evidence: ${evidenceLine(evidence)}`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push("", "## Claim Flow")
|
|
204
|
+
if (map.claimRelations.length === 0) lines.push("- No claim relations recorded")
|
|
205
|
+
for (const relation of map.claimRelations) {
|
|
206
|
+
lines.push(`- ${valueOrDash(relation.fromClaimText ?? relation.fromClaimId)} --${relationLabel(relation)}--> ${valueOrDash(relation.toClaimText ?? relation.toClaimId)}`)
|
|
207
|
+
if (relation.inferred) lines.push(" Rationale: unconfirmed order note only; no causal, support, or dependency relation has been judged")
|
|
208
|
+
else if (relation.rationale) lines.push(` Rationale: ${relation.rationale}`)
|
|
209
|
+
else if (!relation.inferred) lines.push(" Rationale: causal rationale is not recorded")
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
lines.push("", "## Objections & Risks")
|
|
213
|
+
if (map.objections.length === 0 && map.risks.length === 0) lines.push("- None recorded")
|
|
214
|
+
for (const objection of map.objections) {
|
|
215
|
+
lines.push(`- Objection (${objection.priority}): ${objection.text}`)
|
|
216
|
+
if (objection.claimText) lines.push(` Challenges: ${objection.claimText}`)
|
|
217
|
+
if (objection.response) lines.push(` Response: ${objection.response}`)
|
|
218
|
+
}
|
|
219
|
+
for (const risk of map.risks) {
|
|
220
|
+
lines.push(`- Risk (${risk.severity}): ${risk.text}`)
|
|
221
|
+
if (risk.claimText) lines.push(` Constrains: ${risk.claimText}`)
|
|
222
|
+
if (risk.mitigation) lines.push(` Mitigation: ${risk.mitigation}`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
lines.push("", "## Research Gaps")
|
|
226
|
+
if (map.researchGaps.length === 0) lines.push("- None recorded")
|
|
227
|
+
for (const gap of map.researchGaps) {
|
|
228
|
+
lines.push(`- ${gap.question} [${gap.status}/${gap.priority}]`)
|
|
229
|
+
lines.push(` Target: ${gap.targetType}${gap.targetText ? ` - ${gap.targetText}` : ""}`)
|
|
230
|
+
if (gap.findingsFile) lines.push(` Findings: ${gap.findingsFile}`)
|
|
231
|
+
if (gap.evidenceBindingIds?.length) lines.push(` Evidence bindings: ${gap.evidenceBindingIds.join(", ")}`)
|
|
232
|
+
if (gap.notes) lines.push(` Notes: ${gap.notes}`)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
lines.push("", "## Render Target Coverage")
|
|
236
|
+
if (map.artifactCoverage.length === 0) lines.push("- No render targets recorded")
|
|
237
|
+
for (const artifact of map.artifactCoverage) {
|
|
238
|
+
lines.push(`- ${artifact.type}: ${artifact.outputPath ?? artifact.id} [${artifact.contractStatus ?? "unknown"}, coverage: ${artifact.coverageStatus}]`)
|
|
239
|
+
if (artifact.narrativeIds.length > 0) lines.push(` Narrative refs: ${artifact.narrativeIds.join(", ")}`)
|
|
240
|
+
if (artifact.claimIds.length > 0) lines.push(` Claim refs: ${artifact.claimIds.join(", ")}`)
|
|
241
|
+
if (artifact.missingClaimIds.length > 0) lines.push(` Missing claim refs: ${artifact.missingClaimIds.join(", ")}`)
|
|
242
|
+
if (artifact.affectedClaimIds.length > 0) lines.push(` Affected claim refs: ${artifact.affectedClaimIds.join(", ")}`)
|
|
243
|
+
for (const ref of artifact.slideRefs) lines.push(` Slide ${ref.slideIndex}: ${ref.claimId} [${ref.role}] (${ref.match}/${ref.location})`)
|
|
244
|
+
for (const reason of artifact.staleReasons) lines.push(` Coverage note: ${reason}`)
|
|
245
|
+
if (artifact.note) lines.push(` Note: ${artifact.note}`)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
lines.push("", "## Next Actions")
|
|
249
|
+
if (map.nextActions.length === 0) lines.push("- None")
|
|
250
|
+
else for (const action of map.nextActions) lines.push(`- ${action}`)
|
|
251
|
+
return lines.join("\n")
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function relationLabel(relation: NarrativeMapClaimRelation): string {
|
|
255
|
+
return relation.inferred ? "unconfirmed_order" : relation.relation
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function evidenceLine(evidence: NarrativeMapEvidence): string {
|
|
259
|
+
return [
|
|
260
|
+
evidence.source,
|
|
261
|
+
`strength: ${evidence.strength}`,
|
|
262
|
+
evidence.findingsFile ? `findings: ${evidence.findingsFile}` : "",
|
|
263
|
+
evidence.location ? `location: ${evidence.location}` : "",
|
|
264
|
+
evidence.quote ? `quote: ${evidence.quote}` : "",
|
|
265
|
+
evidence.unsupportedScope ? `unsupported scope: ${evidence.unsupportedScope}` : "",
|
|
266
|
+
evidence.caveat ? `caveat: ${evidence.caveat}` : "",
|
|
267
|
+
].filter(Boolean).join(" | ")
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function valueOrDash(value: string | undefined): string {
|
|
271
|
+
return value?.trim() || "-"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function targetText(narrative: NarrativeStateV1, gap: NarrativeResearchGap): string | undefined {
|
|
275
|
+
if (!gap.targetId) return undefined
|
|
276
|
+
if (gap.targetType === "claim") return narrative.claims.find((claim) => claim.id === gap.targetId)?.text
|
|
277
|
+
if (gap.targetType === "objection") return narrative.objections.find((objection) => objection.id === gap.targetId)?.text
|
|
278
|
+
if (gap.targetType === "risk") return narrative.risks.find((risk) => risk.id === gap.targetId)?.text
|
|
279
|
+
if (gap.targetType === "decision") return narrative.decision.action
|
|
280
|
+
if (gap.targetType === "narrative") return narrative.thesis?.statement
|
|
281
|
+
return undefined
|
|
282
|
+
}
|