@cyber-dash-tech/revela 0.16.4 → 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.
- package/README.md +7 -5
- package/README.zh-CN.md +7 -5
- package/lib/commands/brief.ts +9 -0
- package/lib/commands/help.ts +5 -2
- package/lib/commands/init.ts +42 -27
- package/lib/commands/narrative.ts +39 -6
- package/lib/commands/research.ts +36 -20
- package/lib/commands/review.ts +35 -28
- package/lib/ctx.ts +1 -1
- package/lib/decks-state.ts +38 -4
- package/lib/edit/prompt.ts +1 -1
- package/lib/hook-notifications.ts +53 -0
- package/lib/media/download.ts +23 -3
- package/lib/media/save.ts +1 -0
- package/lib/media/types.ts +1 -0
- package/lib/narrative-state/display.ts +74 -4
- package/lib/narrative-state/map-html.ts +242 -107
- package/lib/narrative-state/render-plan.ts +238 -35
- package/lib/narrative-state/research-binding-eval.ts +260 -0
- package/lib/narrative-state/research-gaps.ts +2 -88
- package/lib/narrative-vault/authoring-contract.ts +127 -0
- package/lib/narrative-vault/authoring-guard.ts +122 -0
- package/lib/narrative-vault/auto-compile.ts +134 -0
- package/lib/narrative-vault/bootstrap.ts +63 -0
- package/lib/narrative-vault/cache.ts +14 -0
- package/lib/narrative-vault/compile-mirror.ts +45 -0
- package/lib/narrative-vault/compile.ts +350 -0
- package/lib/narrative-vault/constants.ts +6 -0
- package/lib/narrative-vault/diagnostic-report.ts +117 -0
- package/lib/narrative-vault/export.ts +71 -0
- package/lib/narrative-vault/frontmatter.ts +41 -0
- package/lib/narrative-vault/hook-targets.ts +40 -0
- package/lib/narrative-vault/index.ts +18 -0
- package/lib/narrative-vault/inventory.ts +392 -0
- package/lib/narrative-vault/markdown-qa.ts +237 -0
- package/lib/narrative-vault/markdown.ts +34 -0
- package/lib/narrative-vault/migration.ts +52 -0
- package/lib/narrative-vault/mutate.ts +361 -0
- package/lib/narrative-vault/paths.ts +19 -0
- package/lib/narrative-vault/read.ts +52 -0
- package/lib/narrative-vault/relations.ts +32 -0
- package/lib/narrative-vault/source-loader.ts +19 -0
- package/lib/narrative-vault/timestamp.ts +32 -0
- package/lib/narrative-vault/types.ts +44 -0
- package/lib/qa/checks.ts +206 -5
- package/lib/qa/measure.ts +63 -1
- package/lib/refine/server.ts +157 -20
- package/lib/source-materials.ts +98 -0
- package/lib/tool-result.ts +34 -0
- package/package.json +2 -2
- package/plugin.ts +60 -22
- package/skill/NARRATIVE_SKILL.md +25 -10
- package/skill/SKILL.md +6 -1
- package/tools/decks.ts +363 -67
- package/tools/narrative-view.ts +16 -0
- package/tools/research-save.ts +3 -0
- package/tools/workspace-scan.ts +1 -0
package/lib/commands/review.ts
CHANGED
|
@@ -35,10 +35,10 @@ Workspace boundary rules:
|
|
|
35
35
|
- For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
|
|
36
36
|
|
|
37
37
|
Workflow:
|
|
38
|
-
1. Call \`revela-decks\` with action \`read\` to inspect the current workspace state.
|
|
38
|
+
1. Call \`revela-decks\` with action \`read\` and \`summary: true\` to inspect the current workspace state, \`vaultDiagnostics\`, \`markdownQa\`, and \`narrativeInventory\` when a vault exists.
|
|
39
39
|
2. If ${DECKS_STATE_FILE} is missing or empty, do not invent a deck plan, slide count, design, output path, or visual style. Report the smallest narrative inputs needed, usually audience, belief-before, belief-after, decision/action, thesis, central claims, evidence availability, objections, and risks.
|
|
40
40
|
3. If legacy deck state exists, let the tool-normalized canonical narrative derived from \`narrativeBrief\`, slide roles, slide content, and slide evidence be reviewed. Do not assume old deck readiness means approval.
|
|
41
|
-
4. Call \`revela-decks\` action \`reviewNarrative\`. Use its returned \`status\`, \`blockers\`, \`warnings\`, \`issues\`, \`narrativeHash\`, \`approval\`, and \`nextActions\` as authoritative.
|
|
41
|
+
4. Call \`revela-decks\` action \`reviewNarrative\`. Use its returned \`status\`, \`blockers\`, \`warnings\`, \`issues\`, \`narrativeHash\`, \`approval\`, and \`nextActions\` as authoritative. If the read summary returned \`markdownQa.repairCards\` or \`vaultDiagnostics\`, report Markdown QA repair cards and compile diagnostics before readiness blockers and include file/node/code/message plus smallest repair or suggested next action.
|
|
42
42
|
5. If research findings have been saved but not attached or evidence-bound, report them as unattached research state, not proof.
|
|
43
43
|
6. If central claims lack required evidence, report the named claim and the exact next action: attach findings, bind evidence, run targeted research, narrow unsupported scope, or rewrite the claim.
|
|
44
44
|
7. If approval is missing or stale, clearly distinguish \`ready_for_approval\`, \`approved\`, and render override.
|
|
@@ -46,7 +46,7 @@ Workflow:
|
|
|
46
46
|
Report format:
|
|
47
47
|
- Start with \`Narrative readiness: <status>\`.
|
|
48
48
|
- Include \`Narrative hash: <hash>\` when returned.
|
|
49
|
-
- If blocked or needs research, list each blocker with issue type, claim text when available, and suggested next action.
|
|
49
|
+
- If blocked or needs research, list each blocker with issue type, claim text when available, and suggested next action. Keep Markdown QA repair cards separate from compiler diagnostics and narrative readiness blockers.
|
|
50
50
|
- If warnings exist, list them after blockers as residual risks.
|
|
51
51
|
- If approval is missing, ask whether the user wants to approve the narrative or revise it.
|
|
52
52
|
- If approval is stale, say the prior approval no longer matches the current narrative hash.
|
|
@@ -89,28 +89,30 @@ Current state:
|
|
|
89
89
|
${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
|
|
90
90
|
|
|
91
91
|
Workflow:
|
|
92
|
-
1. Call \`revela-decks\` action \`read\`.
|
|
92
|
+
1. Call \`revela-decks\` action \`read\` with \`summary: true\`.
|
|
93
93
|
2. Call \`revela-decks\` action \`reviewNarrative\` before planning deck slides.
|
|
94
|
-
3. 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.
|
|
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.
|
|
98
|
-
7.
|
|
99
|
-
8.
|
|
100
|
-
9.
|
|
101
|
-
10.
|
|
102
|
-
11.
|
|
103
|
-
12.
|
|
104
|
-
13.
|
|
105
|
-
14.
|
|
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.
|
|
109
110
|
- Include narrative readiness status and narrative hash when available.
|
|
111
|
+
- Include Markdown QA repair cards and vault diagnostic blockers or warnings when returned by \`read(summary: true)\`; blockers prevent deck planning until fixed.
|
|
110
112
|
- Include whether \`compileDeckPlan\` compiled or skipped.
|
|
111
113
|
- Include \`Required structure: Cover + Table of Contents + Closing\` and do not omit any of those slides.
|
|
112
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.
|
|
113
|
-
- 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
|
|
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.
|
|
114
116
|
- Use this sketch style or similarly simple ASCII boxes:
|
|
115
117
|
|
|
116
118
|
\`\`\`text
|
|
@@ -132,6 +134,8 @@ Components:
|
|
|
132
134
|
Primary claim:
|
|
133
135
|
Supporting claims:
|
|
134
136
|
Evidence bindings:
|
|
137
|
+
Visual intent:
|
|
138
|
+
Visual brief:
|
|
135
139
|
Caveats / unsupported scope:
|
|
136
140
|
\`\`\`
|
|
137
141
|
- End by asking the user to confirm the deck plan or request changes.
|
|
@@ -145,6 +149,7 @@ Report format before any HTML write after confirmation:
|
|
|
145
149
|
|
|
146
150
|
Rules:
|
|
147
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.
|
|
148
153
|
- Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
|
|
149
154
|
- Cover, Table of Contents, and Closing are mandatory deck structure. TOC chapter headings must match the chapter grouping used for generation.
|
|
150
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.
|
|
@@ -181,7 +186,7 @@ Goal:
|
|
|
181
186
|
- Treat \`revela-narrative-reviewer\` findings as advisory critique only. Do not represent them as \`revela-decks\` readiness issues, blockers, or authoritative \`writeReadiness\`.
|
|
182
187
|
- Treat source trace mapping as part of evidence readiness: when research findings have been read, relevant findings should appear in slide-level \`slides[].evidence[]\` records rather than only in raw research files.
|
|
183
188
|
- When \`revela-decks review\` returns \`evidenceCandidates\`, treat them as conservative binding candidates only. They are not proof that the full slide is supported, and they are not automatically applied to \`slides[].evidence[]\`. If a candidate has \`sourceKind: "researchesFallback"\`, say it was discovered from workspace \`researches/\` files that are not currently referenced by \`researchPlan\`.
|
|
184
|
-
- When an evidence candidate includes \`evidenceDraft\`, report it as a proposed slide evidence record with its \`candidateId\`; it still requires explicit user/agent confirmation before
|
|
189
|
+
- When an evidence candidate includes \`evidenceDraft\`, report it as a proposed slide evidence record with its \`candidateId\`; it still requires explicit user/agent confirmation before binding. Binding canonical evidence means using \`initNarrativeVault\` if needed, writing \`revela-narrative/evidence/*.md\` with explicit source trace, and running \`compileNarrativeVault\`. Also report \`unsupportedScope\` and \`recommendedRewrite\` so partial evidence is not stretched to future-state claims.
|
|
185
190
|
- When a missing-evidence issue has \`evidenceCandidateSearch\`, use it to explain search coverage: which \`researchPlan\` findings were searched, which fallback \`researches/**/*.md\` files were searched, and any near misses that were below binding threshold.
|
|
186
191
|
|
|
187
192
|
Current state:
|
|
@@ -195,17 +200,18 @@ Workspace boundary rules:
|
|
|
195
200
|
- For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
|
|
196
201
|
|
|
197
202
|
Workflow:
|
|
198
|
-
1. Call \`revela-decks\` with action \`read\` for the current workspace deck
|
|
199
|
-
2. If
|
|
200
|
-
3. If
|
|
201
|
-
4. If
|
|
202
|
-
5.
|
|
203
|
-
6.
|
|
204
|
-
7.
|
|
205
|
-
8.
|
|
206
|
-
9. For substantial decision decks,
|
|
207
|
-
10.
|
|
208
|
-
11.
|
|
203
|
+
1. Call \`revela-decks\` with action \`read\` and \`summary: true\` for the current workspace deck and any \`markdownQa\`, \`vaultDiagnostics\`, and \`narrativeInventory\`.
|
|
204
|
+
2. If the read summary returned \`markdownQa.blockers\` or \`vaultDiagnostics.blockers\`, report Markdown QA repair cards separately from compile diagnostics before artifact readiness with file/node/code/message and smallestRepair/suggestedAction; stop before \`revela-decks review\`, deck HTML writes, or export guidance until the blocker is fixed.
|
|
205
|
+
3. If no current deck exists but the conversation contains enough deck context, call \`revela-decks\` action \`upsertDeck\` with goal, outputPath, theme, requiredInputs, researchPlan, and narrativeBrief if the story intent is clear. Do not invent or ask for a deck key; the tool uses the workspace folder name internally.
|
|
206
|
+
4. If \`researchPlan[].status\` is \`done\` or \`read\` and \`researchPlan[].findingsFile\` exists, verify that evidence-sensitive slide claims are backed by compact \`slides[].evidence[]\` records that reference the relevant findings file or source material where known. The review tool may surface conservative \`evidenceCandidates\` for missing evidence by matching slide text against those findings files, and may fall back to bounded workspace \`researches/**/*.md\` discovery when the research plan has no matching findings file; report these as candidate bindings, not as already-bound evidence.
|
|
207
|
+
5. If a user-confirmed slide plan is available, call \`revela-decks\` action \`upsertSlides\` with every slide's title, purpose, narrativeRole, layout, components, structured content, evidence, visuals, and status. Use only lightweight narrativeRole values that are clear from the plan: \`context\`, \`tension\`, \`evidence\`, \`recommendation\`, \`risk\`, \`ask\`, \`appendix\`, or \`close\`.
|
|
208
|
+
6. Prefer evidence records with \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, \`caveat\`, \`extractedTextPath\`, or \`extractedManifestPath\` when those fields are known from research files or extracted workspace materials.
|
|
209
|
+
7. Do not invent quotes, page references, locations, URLs, caveats, or extraction paths. If source trace is missing, preserve the blocker or warning and report exactly what trace is needed.
|
|
210
|
+
8. Only set requiredInputs fields true when explicit conversation state, files read, research findings read, selected design, fetched layouts/components, or user confirmation supports them. Do not infer completion.
|
|
211
|
+
9. 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.
|
|
212
|
+
10. 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.
|
|
213
|
+
11. Call \`revela-decks\` action \`review\`. The tool computes and writes \`writeReadiness\` plus structured readiness issues for the current workspace deck.
|
|
214
|
+
12. 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 read summary or review result includes \`markdownQa\`, \`vaultDiagnostics\`, or \`diagnosticReport\`, include \`Markdown QA\` and \`Vault diagnostics\` sections before artifact readiness with file/node/code/message and smallestRepair/suggestedAction. 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
215
|
|
|
210
216
|
Minimum conditions for \`ready\`:
|
|
211
217
|
- Topic, audience, slide count, language, and visual style/design are decided.
|
|
@@ -231,8 +237,9 @@ Report format:
|
|
|
231
237
|
- 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.
|
|
232
238
|
- When reporting weak evidence, say whether the missing trace is \`findingsFile\`, \`sourcePath\`, \`location\`, \`quote\`, \`url\`, or \`caveat\` if that is clear from the reviewed materials.
|
|
233
239
|
- When reporting candidate evidence bindings, distinguish partial support from full-slide support. Never say a candidate supports unrelated future-state, recommendation, roadmap, or product-vision claims unless the candidate explicitly supports those claims.
|
|
234
|
-
- Treat \`evidenceDraft\` as a proposed record, not a mutation. Do not call \`upsertSlides\` to bind it.
|
|
240
|
+
- Treat \`evidenceDraft\` as a proposed record, not a mutation. Do not call \`upsertSlides\` to bind it. If the user asks to apply candidate bindings, use \`initNarrativeVault\` if needed, write \`revela-narrative/evidence/*.md\` directly with explicit source trace, then run \`compileNarrativeVault\`. Use \`upsertVaultEvidence\` only as a fallback helper when direct Markdown editing is unavailable or unsafe.
|
|
235
241
|
- When reporting candidate search diagnostics, do not present near misses as evidence. Say they are below binding threshold and use them only to explain why no candidate was returned.
|
|
242
|
+
- When reporting vault diagnostics, do not fill missing evidence, source trace, quotes, URLs, page references, or caveats from model memory. Preserve the blocker until the Markdown source is fixed and compiled.
|
|
236
243
|
|
|
237
244
|
Rules:
|
|
238
245
|
- Do not write or overwrite \`decks/*.html\` during review.
|
package/lib/ctx.ts
CHANGED
package/lib/decks-state.ts
CHANGED
|
@@ -20,7 +20,8 @@ import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type Work
|
|
|
20
20
|
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
|
|
21
21
|
import { computeNarrativeHash } from "./narrative-state/hash"
|
|
22
22
|
import { getArtifactClaimRefs } from "./narrative-state/queries"
|
|
23
|
-
import type { NarrativeStateV1 } from "./narrative-state/types"
|
|
23
|
+
import type { NarrativeApproval, NarrativeStateV1 } from "./narrative-state/types"
|
|
24
|
+
import { hasNarrativeVault, loadNarrativeFromPreferredSource } from "./narrative-vault"
|
|
24
25
|
|
|
25
26
|
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
26
27
|
|
|
@@ -34,6 +35,7 @@ export interface DecksState {
|
|
|
34
35
|
version: 1
|
|
35
36
|
activeDeck?: string
|
|
36
37
|
narrative?: NarrativeStateV1
|
|
38
|
+
narrativeApprovals?: NarrativeApproval[]
|
|
37
39
|
workspace: {
|
|
38
40
|
brief?: string
|
|
39
41
|
sourceMaterials: SourceMaterial[]
|
|
@@ -55,6 +57,7 @@ export interface SourceMaterial {
|
|
|
55
57
|
type?: string
|
|
56
58
|
size?: number
|
|
57
59
|
fingerprint?: string
|
|
60
|
+
lastModified?: string
|
|
58
61
|
status?: "discovered" | "extracted" | "summarized" | "researched"
|
|
59
62
|
extraction?: {
|
|
60
63
|
manifestPath?: string
|
|
@@ -483,15 +486,37 @@ export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptio
|
|
|
483
486
|
}
|
|
484
487
|
|
|
485
488
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
486
|
-
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
489
|
+
return applyPreferredNarrativeSource(workspaceRoot, readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative }))
|
|
487
490
|
}
|
|
488
491
|
|
|
489
492
|
export function writeDecksState(workspaceRoot: string, state: DecksState): void {
|
|
490
|
-
|
|
493
|
+
const vault = hasNarrativeVault(workspaceRoot)
|
|
494
|
+
writeWorkspaceState(workspaceRoot, prepareStateForWrite(workspaceRoot, state), { fileName: DECKS_STATE_FILE, normalize: vault ? normalizeDecksState : normalizeDecksStateWithNarrative })
|
|
491
495
|
}
|
|
492
496
|
|
|
493
497
|
export function readOrCreateDecksState(workspaceRoot: string): DecksState {
|
|
494
|
-
return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
498
|
+
return applyPreferredNarrativeSource(workspaceRoot, readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative }))
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function applyPreferredNarrativeSource(workspaceRoot: string, state: DecksState): DecksState {
|
|
502
|
+
const normalized = normalizeDecksStateWithNarrative(state)
|
|
503
|
+
const loaded = loadNarrativeFromPreferredSource(workspaceRoot, normalized.narrative, narrativeApprovalsForHydration(normalized))
|
|
504
|
+
if (loaded.source !== "vault" || !loaded.narrative) return normalized
|
|
505
|
+
return normalizeDecksStateWithNarrative({ ...normalized, narrative: loaded.narrative, narrativeApprovals: loaded.narrative.approvals })
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function prepareStateForWrite(workspaceRoot: string, state: DecksState): DecksState {
|
|
509
|
+
const normalized = normalizeDecksStateWithNarrative(state)
|
|
510
|
+
if (!hasNarrativeVault(workspaceRoot)) return normalized
|
|
511
|
+
const loaded = loadNarrativeFromPreferredSource(workspaceRoot, normalized.narrative, narrativeApprovalsForHydration(normalized))
|
|
512
|
+
const narrativeApprovals = loaded.narrative?.approvals ?? narrativeApprovalsForHydration(normalized)
|
|
513
|
+
const prepared = normalizeDecksStateWithNarrative({ ...normalized, narrative: loaded.narrative ?? normalized.narrative, narrativeApprovals })
|
|
514
|
+
const { narrative: _narrative, ...withoutNarrative } = prepared
|
|
515
|
+
return withoutNarrative as DecksState
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function narrativeApprovalsForHydration(state: DecksState): NarrativeApproval[] {
|
|
519
|
+
return state.narrativeApprovals ?? state.narrative?.approvals ?? []
|
|
495
520
|
}
|
|
496
521
|
|
|
497
522
|
export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
|
|
@@ -859,6 +884,7 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
859
884
|
version: 1,
|
|
860
885
|
activeDeck: input.activeDeck ? normalizeSlug(input.activeDeck) : undefined,
|
|
861
886
|
narrative: normalizeCanonicalNarrativeState(input.narrative, input.activeDeck || "workspace"),
|
|
887
|
+
narrativeApprovals: normalizeNarrativeApprovals([...(input.narrativeApprovals ?? []), ...(input.narrative?.approvals ?? [])]),
|
|
862
888
|
workspace: {
|
|
863
889
|
brief: input.workspace?.brief,
|
|
864
890
|
sourceMaterials: input.workspace?.sourceMaterials ?? [],
|
|
@@ -890,9 +916,17 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
890
916
|
function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
|
|
891
917
|
const state = normalizeDecksState(input)
|
|
892
918
|
if (!state.narrative && currentDeckKey(state)) state.narrative = normalizeNarrativeState(state)
|
|
919
|
+
if (state.narrative && state.narrativeApprovals && state.narrativeApprovals.length > 0) {
|
|
920
|
+
state.narrative = { ...state.narrative, approvals: normalizeNarrativeApprovals([...state.narrative.approvals, ...state.narrativeApprovals]) ?? [] }
|
|
921
|
+
}
|
|
893
922
|
return state
|
|
894
923
|
}
|
|
895
924
|
|
|
925
|
+
function normalizeNarrativeApprovals(approvals: NarrativeApproval[]): NarrativeApproval[] | undefined {
|
|
926
|
+
const normalized = [...new Map(approvals.filter((approval) => approval?.id).map((approval) => [approval.id, approval])).values()]
|
|
927
|
+
return normalized.length > 0 ? normalized : undefined
|
|
928
|
+
}
|
|
929
|
+
|
|
896
930
|
function normalizeDeckPlanReview(input: DeckPlanReview | undefined): DeckPlanReview | undefined {
|
|
897
931
|
if (!input || !input.narrativeHash || !input.planHash) return undefined
|
|
898
932
|
return {
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -75,7 +75,7 @@ Instructions:
|
|
|
75
75
|
- Make the smallest targeted change that satisfies the user's comment.
|
|
76
76
|
- If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
|
|
77
77
|
- Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
|
|
78
|
-
- Preserve the narrative boundary: if the requested edit changes audience framing, belief shift, decision/action, thesis, recommendation, claim wording, evidence scope, caveat, risk, objection, or decision ask, do not patch the HTML directly. Explain that the canonical narrative must be updated first through ${"`revela-decks`"}
|
|
78
|
+
- Preserve the narrative boundary: if the requested edit changes audience framing, belief shift, decision/action, thesis, recommendation, claim wording, evidence scope, caveat, risk, objection, or decision ask, do not patch the HTML directly. Explain that the canonical narrative must be updated first through targeted ${"`revela-decks`"} vault actions (${"`initNarrativeVault`"} if needed, then ${"`updateVaultCoreNarrative`"}, ${"`upsertVaultClaim`"}, ${"`upsertVaultEvidence`"}, ${"`upsertVaultObjection`"}, or ${"`upsertVaultRisk`"}), with manual Markdown edits plus ${"`compileNarrativeVault`"} only for unsupported node changes. Then the narrative must be reviewed/approved or explicitly overridden before updating the deck projection.
|
|
79
79
|
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, runtime JavaScript fixes, or deck HTML contract fixes may remain an artifact-level edit.
|
|
80
80
|
- If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
|
|
81
81
|
- Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ArtifactQAReport } from "./qa/artifact"
|
|
2
|
+
import type { AutoCompileNarrativeVaultResult } from "./narrative-vault/auto-compile"
|
|
3
|
+
|
|
4
|
+
export function formatMarkdownQaUserNotice(result: AutoCompileNarrativeVaultResult): string | undefined {
|
|
5
|
+
if (result.ok) return undefined
|
|
6
|
+
|
|
7
|
+
const lines = ["**Markdown QA blocked**"]
|
|
8
|
+
lines.push(`Touched: ${result.touched.length > 0 ? result.touched.map((file) => `\`${file}\``).join(", ") : "unknown"}`)
|
|
9
|
+
|
|
10
|
+
const blockers = result.markdownQa?.blockers ?? []
|
|
11
|
+
if (blockers.length > 0) {
|
|
12
|
+
lines.push("Top repair(s):")
|
|
13
|
+
for (const card of blockers.slice(0, 3)) {
|
|
14
|
+
const location = [card.file, card.nodeId].filter(Boolean).join(" / ")
|
|
15
|
+
lines.push(`- \`${card.issueCode}\`${location ? ` (${location})` : ""}: ${card.smallestRepair}`)
|
|
16
|
+
}
|
|
17
|
+
if (blockers.length > 3) lines.push(`- ... ${blockers.length - 3} more`)
|
|
18
|
+
} else if (result.error) {
|
|
19
|
+
lines.push(`Hook error: ${result.error}`)
|
|
20
|
+
} else {
|
|
21
|
+
lines.push("Compile diagnostics are blocking the vault. See the tool output for details.")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return lines.join("\n")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function formatArtifactQaUserNotice(report: ArtifactQAReport): string | undefined {
|
|
28
|
+
if (report.passed) return undefined
|
|
29
|
+
|
|
30
|
+
const lines = ["**Artifact QA failed**"]
|
|
31
|
+
lines.push(`File: \`${report.file}\``)
|
|
32
|
+
lines.push(`Hard errors: ${report.hardErrorCount}; warnings: ${report.warningCount}`)
|
|
33
|
+
if (report.sections.length > 0) {
|
|
34
|
+
lines.push("Top issue area(s):")
|
|
35
|
+
for (const section of report.sections.slice(0, 3)) lines.push(`- ${firstLine(section)}`)
|
|
36
|
+
if (report.sections.length > 3) lines.push(`- ... ${report.sections.length - 3} more`)
|
|
37
|
+
}
|
|
38
|
+
lines.push("Fix the reported artifact issues before treating the deck as ready.")
|
|
39
|
+
return lines.join("\n")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function formatStateGateUserNotice(kind: "write" | "patch", reason: string): string {
|
|
43
|
+
return [
|
|
44
|
+
"**Revela state gate blocked a direct DECKS.json edit**",
|
|
45
|
+
`Operation: ${kind}`,
|
|
46
|
+
`Reason: ${reason}`,
|
|
47
|
+
"Use the `revela-decks` tool for controlled workspace state changes.",
|
|
48
|
+
].join("\n")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function firstLine(text: string): string {
|
|
52
|
+
return text.split(/\r?\n/).map((line) => line.trim()).find(Boolean)?.replace(/^#+\s*/, "") ?? "See report details."
|
|
53
|
+
}
|
package/lib/media/download.ts
CHANGED
|
@@ -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
package/lib/media/types.ts
CHANGED
|
@@ -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
|
|
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)
|