@cyber-dash-tech/revela 0.17.0 → 0.17.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.
@@ -147,7 +147,7 @@ export function buildNarrativeViewPrompt(options: { workspaceRoot: string; langu
147
147
  relations: map.claimRelations.map((relation) => ({ id: relation.id, fromClaimId: relation.fromClaimId, toClaimId: relation.toClaimId, relation: relation.relation, rationale: relation.rationale, inferred: relation.inferred })),
148
148
  objections: map.objections.map((objection) => ({ id: objection.id, claimId: objection.claimId, priority: objection.priority, text: objection.text, response: objection.response })),
149
149
  risks: map.risks.map((risk) => ({ id: risk.id, claimId: risk.claimId, severity: risk.severity, text: risk.text, mitigation: risk.mitigation })),
150
- researchGaps: map.researchGaps.map((gap) => ({ id: gap.id, targetType: gap.targetType, targetId: gap.targetId, status: gap.status, priority: gap.priority, question: gap.question })),
150
+ researchGaps: map.researchGaps.map((gap) => ({ id: gap.id, targetType: gap.targetType, targetId: gap.targetId, status: gap.status, priority: gap.priority, question: gap.question, findingsFile: gap.findingsFile, evidenceBindingIds: gap.evidenceBindingIds, notes: gap.notes })),
151
151
  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 })) })),
152
152
  }
153
153
 
@@ -165,18 +165,27 @@ Hard rules:
165
165
  - Do not invent new claims, evidence, relations, slide coverage, source paths, findings files, quotes, or caveats.
166
166
  - Preserve every claimId exactly.
167
167
  - Preserve every relation endpoint exactly: fromClaimId, toClaimId, relation.
168
- - You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, supportRationale, supportedScope, unsupportedScope, objectionsSummary, risksSummary, riskOrGapSummary, researchGapsSummary, relation displayLabel, and relation displayRationale.
168
+ - You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, supportRationale, supportedScope, unsupportedScope, objectionsSummary, risksSummary, riskOrGapSummary, researchGapsSummary, research gap displayQuestion, relation displayLabel, and relation displayRationale.
169
169
  - For inferred relations, do not provide relation displayLabel or displayRationale; inferred relations are unconfirmed order notes, not causal/support/dependency judgments.
170
170
  - 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.
171
171
  - Keep source paths, findings files, claim IDs, narrative hash, and numbers unchanged.
172
- - Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, supportRationale, supportedScope, unsupportedScope, objectionsSummary, risksSummary, riskOrGapSummary, researchGapsSummary, relation displayLabel, and relation displayRationale.
172
+ - Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, supportRationale, supportedScope, unsupportedScope, objectionsSummary, risksSummary, riskOrGapSummary, researchGapsSummary, research gap displayQuestion, relation displayLabel, and relation displayRationale.
173
173
  - For every claim in a non-English target language, provide displayTitle so the selected-claim panel does not fall back to canonical English claim text.
174
- - For every selected-claim detail field that has canonical user-facing text, provide the matching localized display field when it exists: supportedScope for supported scope, unsupportedScope for evidence boundaries, supportRationale for why the evidence supports the claim, objectionsSummary for objections, risksSummary for risks, and researchGapsSummary for research gaps.
174
+ - For every evidence/detail field that has canonical user-facing text, provide the matching localized display field when it exists: supportedScope for supported scope, unsupportedScope for evidence boundaries, supportRationale for why the evidence supports the claim, objectionsSummary for objections, risksSummary for risks, and researchGapsSummary for canonical research gaps.
175
175
  - Do not translate claim IDs, relation endpoints, narrative hash, source paths, findings files, URLs, numbers, or quoted/source facts.
176
+ - For every canonical research gap in a non-English target language, provide researchGapCards[].displayQuestion so gap cards do not fall back to canonical English question text. Preserve gapId exactly and do not change the gap meaning.
176
177
  - Use natural business and manufacturing terminology in the target language, not word-by-word machine translation.
177
178
  - If a fact is missing, describe it as missing instead of filling it in.
178
179
  - If vaultDiagnostics.blockers is present in the compact map, keep Story read-only but surface the blocker state in summaryLine and the relevant claim card riskOrGapSummary when applicable. Include diagnostic file/nodeId/code/message/suggestedAction in display copy; do not invent missing evidence, source trace, quotes, or caveats to hide the blocker.
179
180
  - Do not turn Story into a workflow dashboard: diagnostic copy is for reading context only, not command suggestions or mutation planning.
181
+ - Story UI is evidence-first: clicking a claim shows evidence/gap cards; evidence cards must carry the evidence description, why it supports the claim, and source trace; clicking evidence shows only canonical gaps linked to that evidence.
182
+ - The selected-claim panel is only an evidence/research-gap card list. Do not repeat canonical claim text there; the claim itself is already visible in Claim Flow.
183
+ - Claim cards should not explain gaps or risks through riskOrGapSummary when the same topic belongs in evidence/gap cards. Keep claim cards focused on role, narrative job, and evidence summary.
184
+ - Do not make evidence detail a schema dump. Do not default to unsupportedScope, caveat, IDs, target fields, or evidenceBindingIds for evidence cards; evidence cards should explain what the source says, why it supports the claim, and where it came from.
185
+ - Do not invent, infer, or soften new gaps in display copy. Show research gaps only when they exist in compactMap.researchGaps; do not turn unsupportedScope, weak evidence, caveats, objections, risks, or your own judgment into a gap.
186
+ - researchGapsSummary and riskOrGapSummary may only summarize existing canonical research gaps or diagnostics. If no canonical gap exists for the claim, leave gap-specific display copy empty instead of implying a gap.
187
+ - Localize new Story labels when useful: selectedEvidence, evidenceList, gap, gaps, noEvidence, selectEvidencePrompt, sourceTrace, evidenceSource, whyThisSupports, linkedGaps, selectedGap, and noLinkedGaps.
188
+ - Gap detail may only use canonical research gap fields from the compact map: id, targetType, targetId, status, priority, question, findingsFile, evidenceBindingIds, and notes.
180
189
 
181
190
  Chinese localization rules when the target language request is Chinese, zh, zh-CN, --cn, 中文, or Simplified Chinese:
182
191
  - Use natural business/manufacturing Chinese, not word-by-word machine translation.
@@ -94,15 +94,16 @@ Workflow:
94
94
  3. If the read summary returned \`markdownQa.blockers\` or \`vaultDiagnostics.blockers\`, stop before deck planning and report Markdown QA repair cards separately from compile diagnostics with file/node/code/message and smallest repair or suggested next action. If narrative readiness is \`approved\`, continue. If it is \`ready_for_approval\`, ask the user for explicit approval before continuing. If it is blocked, stale, or needs research, stop and report the smallest next action. Do not call \`approveNarrative\` unless the user explicitly approves or requests a render override.
95
95
  4. After approval or explicit render override exists, call \`revela-decks\` action \`compileDeckPlan\`. This projects canonical narrative claims and evidence bindings into compatibility \`slides[]\` and \`slides[].evidence[]\`; it must not write HTML.
96
96
  5. If \`compileDeckPlan\` returns \`skipped\`, stop and report the reason. Do not invent slide specs manually to bypass approval.
97
- 6. Present the compiled deck plan to the user and include a low-fidelity layout sketch for every slide. The plan must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. The sketch is ASCII/text structure only; do not generate visual images or HTML mockups.
98
- 7. Stop after presenting the plan. Ask the user to confirm or request changes. Do not call \`revela-decks review\`, do not fetch design context, and do not write HTML in the same turn unless the user had already explicitly confirmed the current plan before this command.
99
- 8. Only after explicit user confirmation of the current slide plan, call \`revela-decks\` action \`confirmDeckPlan\` with \`approvalBy=user\` and a compact \`approvalNote\`.
100
- 9. After confirmation is recorded, ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
101
- 10. Update only deck/artifact metadata through \`revela-decks upsertDeck\` / \`upsertSlides\` when required by confirmed design/layout choices. Do not change canonical narrative claims unless the user asks to revise the narrative.
102
- 11. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing. If it reports \`slide_plan_unconfirmed\`, stop and ask for explicit deck-plan confirmation.
103
- 12. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
104
- 13. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
105
- 14. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
97
+ 6. Treat each compiled slide's \`visuals[]\` and \`content.data.visualIntent\` as required render instructions, not optional decoration. Do not downgrade a planned chart, metric card, evidence table, comparison grid, risk matrix, steps view, or media brief into generic bullets unless the plan is revised and reconfirmed.
98
+ 7. Present the compiled deck plan to the user and include a low-fidelity layout sketch for every slide. The plan must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. The sketch is ASCII/text structure only; do not generate visual images or HTML mockups.
99
+ 8. Stop after presenting the plan. Ask the user to confirm or request changes. Do not call \`revela-decks review\`, do not fetch design context, and do not write HTML in the same turn unless the user had already explicitly confirmed the current plan before this command.
100
+ 9. Only after explicit user confirmation of the current slide plan, call \`revela-decks\` action \`confirmDeckPlan\` with \`approvalBy=user\` and a compact \`approvalNote\`.
101
+ 10. After confirmation is recorded, ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
102
+ 11. Update only deck/artifact metadata through \`revela-decks upsertDeck\` / \`upsertSlides\` when required by confirmed design/layout choices. Do not change canonical narrative claims unless the user asks to revise the narrative.
103
+ 12. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing. If it reports \`slide_plan_unconfirmed\`, stop and ask for explicit deck-plan confirmation.
104
+ 13. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
105
+ 14. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
106
+ 15. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
106
107
 
107
108
  Deck plan report format:
108
109
  - Start with \`Deck plan: awaiting confirmation\` when a plan was compiled and has not yet been confirmed.
@@ -111,7 +112,7 @@ Deck plan report format:
111
112
  - Include whether \`compileDeckPlan\` compiled or skipped.
112
113
  - Include \`Required structure: Cover + Table of Contents + Closing\` and do not omit any of those slides.
113
114
  - Include a \`Chapters\` section before the slide list. It must list 3-5 TOC headings, their slide ranges, and the non-structural slides assigned to each chapter.
114
- - For every slide, include: slide index, title, purpose, narrative role, low-fidelity layout sketch, layout, components, primary/supporting claim ids, evidence binding ids or source summary, visual intent, and caveats/unsupported scope.
115
+ - For every slide, include: slide index, title, purpose, narrative role, low-fidelity layout sketch, layout, components, primary/supporting claim ids, evidence binding ids or source summary, visual intent from \`content.data.visualIntent\`, visual brief from \`visuals[]\`, and caveats/unsupported scope.
115
116
  - Use this sketch style or similarly simple ASCII boxes:
116
117
 
117
118
  \`\`\`text
@@ -133,6 +134,8 @@ Components:
133
134
  Primary claim:
134
135
  Supporting claims:
135
136
  Evidence bindings:
137
+ Visual intent:
138
+ Visual brief:
136
139
  Caveats / unsupported scope:
137
140
  \`\`\`
138
141
  - End by asking the user to confirm the deck plan or request changes.
@@ -146,6 +149,7 @@ Report format before any HTML write after confirmation:
146
149
 
147
150
  Rules:
148
151
  - \`compileDeckPlan\` is the canonical narrative-to-deck planning path. Do not manually invent slide specs to avoid it.
152
+ - Visual intent is part of the confirmed plan. During HTML generation, satisfy the planned component/visual brief using fetched design components; do not collapse planned visuals into prose-only bullets.
149
153
  - Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
150
154
  - Cover, Table of Contents, and Closing are mandatory deck structure. TOC chapter headings must match the chapter grouping used for generation.
151
155
  - Do not generate the complete deck content in one broad pass after confirmation. Work chapter by chapter while keeping the artifact valid after each write.
@@ -12,6 +12,10 @@ const MIME_TO_EXT: Record<string, string> = {
12
12
 
13
13
  const ALLOWED_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif", ".ico"])
14
14
  const DEFAULT_DOWNLOAD_TIMEOUT_MS = 10_000
15
+ const PRODUCT_USER_AGENT = "Revela/0.17 asset-save"
16
+ const BROWSER_USER_AGENT =
17
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
18
+ "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
15
19
 
16
20
  function normalizeExtension(ext: string): string {
17
21
  const value = ext.toLowerCase()
@@ -47,6 +51,24 @@ export async function downloadImageFromUrl(
47
51
  throw new Error("INVALID_URL")
48
52
  }
49
53
 
54
+ const userAgents = [PRODUCT_USER_AGENT, BROWSER_USER_AGENT]
55
+ let lastError: unknown
56
+ for (const userAgent of userAgents) {
57
+ try {
58
+ return await downloadWithUserAgent(parsed, userAgent, options)
59
+ } catch (error) {
60
+ lastError = error
61
+ }
62
+ }
63
+
64
+ throw lastError instanceof Error ? lastError : new Error(String(lastError))
65
+ }
66
+
67
+ async function downloadWithUserAgent(
68
+ parsed: URL,
69
+ userAgent: string,
70
+ options: { timeoutMs?: number },
71
+ ): Promise<{ buffer: Buffer; contentType: string | null; extension: string }> {
50
72
  const controller = new AbortController()
51
73
  let timedOut = false
52
74
  const timer = setTimeout(() => {
@@ -60,9 +82,7 @@ export async function downloadImageFromUrl(
60
82
  response = await fetch(parsed, {
61
83
  headers: {
62
84
  Accept: "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
63
- "User-Agent":
64
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " +
65
- "(KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
85
+ "User-Agent": userAgent,
66
86
  },
67
87
  signal: controller.signal,
68
88
  })
package/lib/media/save.ts CHANGED
@@ -141,6 +141,7 @@ function saveFailureResult(
141
141
  path: null,
142
142
  manifestPath: relative(workspaceDir, manifestPath),
143
143
  updated: true,
144
+ failureReason,
144
145
  }
145
146
  }
146
147
 
@@ -59,6 +59,7 @@ export type MediaSaveResult =
59
59
  path: string | null
60
60
  manifestPath: string
61
61
  updated: boolean
62
+ failureReason?: string
62
63
  }
63
64
  | {
64
65
  ok: false
@@ -9,6 +9,7 @@ export interface NarrativeDisplayModel {
9
9
  summaryLine?: string
10
10
  labels?: Partial<NarrativeDisplayLabels>
11
11
  claimCards?: NarrativeDisplayClaimCard[]
12
+ researchGapCards?: NarrativeDisplayResearchGapCard[]
12
13
  relations?: NarrativeDisplayRelation[]
13
14
  }
14
15
 
@@ -17,6 +18,18 @@ export interface NarrativeDisplayLabels {
17
18
  claimFlow: string
18
19
  flowNote: string
19
20
  selectedClaim: string
21
+ selectedEvidence: string
22
+ evidenceList: string
23
+ gap: string
24
+ gaps: string
25
+ noEvidence: string
26
+ selectEvidencePrompt: string
27
+ sourceTrace: string
28
+ evidenceSource: string
29
+ whyThisSupports: string
30
+ linkedGaps: string
31
+ selectedGap: string
32
+ noLinkedGaps: string
20
33
  claim: string
21
34
  claimId: string
22
35
  status: string
@@ -48,6 +61,11 @@ export interface NarrativeDisplayClaimCard {
48
61
  researchGapsSummary?: string
49
62
  }
50
63
 
64
+ export interface NarrativeDisplayResearchGapCard {
65
+ gapId: string
66
+ displayQuestion?: string
67
+ }
68
+
51
69
  export interface NarrativeDisplayRelation {
52
70
  fromClaimId: string
53
71
  toClaimId: string
@@ -63,6 +81,7 @@ export interface ValidatedNarrativeDisplayModel {
63
81
  summaryLine?: string
64
82
  labels: NarrativeDisplayLabels
65
83
  claimCards: Map<string, NarrativeDisplayClaimCard>
84
+ researchGapCards: Map<string, NarrativeDisplayResearchGapCard>
66
85
  relations: Map<string, NarrativeDisplayRelation>
67
86
  }
68
87
 
@@ -71,8 +90,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
71
90
  return {
72
91
  eyebrow: "只读主张流",
73
92
  claimFlow: "主张推进",
74
- flowNote: "点击主张查看证据、关系、风险、缺口和已覆盖页面。",
93
+ flowNote: "点击主张查看论据和真实存在的缺口;点击论据查看它关联的缺口。",
75
94
  selectedClaim: "当前主张",
95
+ selectedEvidence: "当前论据",
96
+ evidenceList: "论据",
97
+ gap: "缺口",
98
+ gaps: "缺口",
99
+ noEvidence: "没有绑定论据",
100
+ selectEvidencePrompt: "选择一条论据或缺口查看详情",
101
+ sourceTrace: "来源追踪",
102
+ evidenceSource: "来源",
103
+ whyThisSupports: "为什么支撑论点",
104
+ linkedGaps: "这条论据关联的缺口",
105
+ selectedGap: "当前缺口",
106
+ noLinkedGaps: "这条论据没有关联缺口",
76
107
  claim: "主张",
77
108
  claimId: "主张 ID",
78
109
  status: "状态",
@@ -93,8 +124,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
93
124
  return {
94
125
  eyebrow: "読み取り専用クレームフロー",
95
126
  claimFlow: "クレームフロー",
96
- flowNote: "クレームをクリックすると、根拠、関係、リスク、ギャップ、該当スライドを確認できます。",
127
+ flowNote: "クレームをクリックして根拠と実在するギャップを確認し、根拠をクリックして紐づくギャップを確認します。",
97
128
  selectedClaim: "選択中のクレーム",
129
+ selectedEvidence: "選択中の根拠",
130
+ evidenceList: "根拠",
131
+ gap: "ギャップ",
132
+ gaps: "ギャップ",
133
+ noEvidence: "紐づいた根拠はありません",
134
+ selectEvidencePrompt: "根拠またはギャップを選択して詳細を確認してください",
135
+ sourceTrace: "出典トレース",
136
+ evidenceSource: "出典",
137
+ whyThisSupports: "この根拠がクレームを支える理由",
138
+ linkedGaps: "この根拠に紐づくギャップ",
139
+ selectedGap: "選択中のギャップ",
140
+ noLinkedGaps: "この根拠に紐づくギャップはありません",
98
141
  claim: "クレーム",
99
142
  claimId: "クレーム ID",
100
143
  status: "ステータス",
@@ -114,8 +157,20 @@ export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage):
114
157
  return {
115
158
  eyebrow: "Read-only claim flow board",
116
159
  claimFlow: "Claim Flow",
117
- flowNote: "Click a claim to inspect support, relation context, gaps, and covered slides.",
160
+ flowNote: "Click a claim to read its evidence and real gaps; click evidence to see gaps linked to that evidence.",
118
161
  selectedClaim: "Selected claim",
162
+ selectedEvidence: "Selected evidence",
163
+ evidenceList: "Evidence",
164
+ gap: "Gap",
165
+ gaps: "Gaps",
166
+ noEvidence: "No evidence bound",
167
+ selectEvidencePrompt: "Select evidence or a gap to inspect details.",
168
+ sourceTrace: "Source trace",
169
+ evidenceSource: "Source",
170
+ whyThisSupports: "Why this supports the claim",
171
+ linkedGaps: "Gaps linked to evidence",
172
+ selectedGap: "Selected gap",
173
+ noLinkedGaps: "No gaps linked to this evidence.",
119
174
  claim: "Claim",
120
175
  claimId: "Claim ID",
121
176
  status: "Status",
@@ -140,6 +195,7 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
140
195
  if (input.language !== language) throw new Error(`Narrative display model language must be ${language}.`)
141
196
 
142
197
  const claimIds = new Set(map.claimFlow.map((claim) => claim.id))
198
+ const gapIds = new Set(map.researchGaps.map((gap) => gap.id))
143
199
  const relationByKey = new Map(map.claimRelations.map((relation) => [relationKey(relation), relation]))
144
200
  const claimCards = new Map<string, NarrativeDisplayClaimCard>()
145
201
  for (const card of input.claimCards ?? []) {
@@ -147,6 +203,12 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
147
203
  claimCards.set(card.claimId, cleanClaimCard(card))
148
204
  }
149
205
 
206
+ const researchGapCards = new Map<string, NarrativeDisplayResearchGapCard>()
207
+ for (const card of input.researchGapCards ?? []) {
208
+ if (!gapIds.has(card.gapId)) throw new Error(`Unknown display gapId: ${card.gapId}`)
209
+ researchGapCards.set(card.gapId, cleanResearchGapCard(card))
210
+ }
211
+
150
212
  const relations = new Map<string, NarrativeDisplayRelation>()
151
213
  for (const relation of input.relations ?? []) {
152
214
  const key = relationKey(relation)
@@ -162,12 +224,13 @@ export function validateNarrativeDisplayModel(map: NarrativeMap, input: Narrativ
162
224
  summaryLine: clean(input.summaryLine),
163
225
  labels: mergeLabels(defaults, input.labels),
164
226
  claimCards,
227
+ researchGapCards,
165
228
  relations,
166
229
  }
167
230
  }
168
231
 
169
232
  export function emptyDisplayModel(language: NarrativeViewLanguage, labels = defaultNarrativeDisplayLabels(language)): ValidatedNarrativeDisplayModel {
170
- return { version: 1, language, labels, claimCards: new Map(), relations: new Map() }
233
+ return { version: 1, language, labels, claimCards: new Map(), researchGapCards: new Map(), relations: new Map() }
171
234
  }
172
235
 
173
236
  export function relationKey(relation: Pick<NarrativeDisplayRelation, "fromClaimId" | "toClaimId" | "relation">): string {
@@ -210,6 +273,13 @@ function cleanClaimCard(card: NarrativeDisplayClaimCard): NarrativeDisplayClaimC
210
273
  }
211
274
  }
212
275
 
276
+ function cleanResearchGapCard(card: NarrativeDisplayResearchGapCard): NarrativeDisplayResearchGapCard {
277
+ return {
278
+ gapId: card.gapId,
279
+ displayQuestion: clean(card.displayQuestion),
280
+ }
281
+ }
282
+
213
283
  function cleanRelation(relation: NarrativeDisplayRelation, canonical: NarrativeMapClaimRelation): NarrativeDisplayRelation {
214
284
  const displayLabel = clean(relation.displayLabel)
215
285
  const displayRationale = clean(relation.displayRationale)