@cyber-dash-tech/revela 0.12.0 → 0.13.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/lib/commands/brief.ts +63 -0
- package/lib/commands/help.ts +2 -0
- package/lib/commands/narrative.ts +160 -0
- package/lib/decks-state.ts +33 -0
- package/lib/edit/prompt.ts +3 -0
- package/lib/inspection-context/compile.ts +159 -5
- package/lib/inspection-context/project.ts +20 -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 +433 -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/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 +31 -0
- package/tools/decks.ts +86 -1
- package/tools/narrative-view.ts +84 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type DecksState, type SlideClaimRef, type SlideClaimRefRole } from "../decks-state"
|
|
2
|
+
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
3
|
+
import { ensureActiveHtmlDeckRenderTarget } from "../workspace-state/render-targets"
|
|
4
|
+
import { computeNarrativeHash } from "./hash"
|
|
5
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
6
|
+
import { getClaimSlideRefs, type ClaimSlideRef } from "./queries"
|
|
7
|
+
|
|
8
|
+
export interface BackfillSlideClaimRefsResult {
|
|
9
|
+
updated: boolean
|
|
10
|
+
addedCount: number
|
|
11
|
+
slideCount: number
|
|
12
|
+
narrativeHash: string
|
|
13
|
+
refs: ClaimSlideRef[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function backfillSlideClaimRefsFromCoverage(state: DecksState): { state: DecksState; result: BackfillSlideClaimRefsResult } {
|
|
17
|
+
const narrative = normalizeNarrativeState(state)
|
|
18
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
19
|
+
const deckKey = state.activeDeck || Object.keys(state.decks)[0]
|
|
20
|
+
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
21
|
+
if (!deck) {
|
|
22
|
+
return { state: { ...state, narrative }, result: { updated: false, addedCount: 0, slideCount: 0, narrativeHash, refs: [] } }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const refs = getClaimSlideRefs({ ...state, narrative }, deck)
|
|
26
|
+
const refsBySlide = new Map<number, ClaimSlideRef[]>()
|
|
27
|
+
for (const ref of refs) refsBySlide.set(ref.slideIndex, [...(refsBySlide.get(ref.slideIndex) ?? []), ref])
|
|
28
|
+
|
|
29
|
+
let addedCount = 0
|
|
30
|
+
const slides = deck.slides.map((slide) => {
|
|
31
|
+
const existing = [...(slide.claimRefs ?? [])]
|
|
32
|
+
const seen = new Set(existing.map((ref) => `${ref.claimId}:${ref.role}`))
|
|
33
|
+
const additions: SlideClaimRef[] = []
|
|
34
|
+
for (const ref of refsBySlide.get(slide.index) ?? []) {
|
|
35
|
+
const role = backfilledRole(ref.role)
|
|
36
|
+
const key = `${ref.claimId}:${role}`
|
|
37
|
+
if (seen.has(key)) continue
|
|
38
|
+
seen.add(key)
|
|
39
|
+
additions.push({ claimId: ref.claimId, role, note: backfillNote(ref) })
|
|
40
|
+
}
|
|
41
|
+
if (additions.length === 0) return slide
|
|
42
|
+
addedCount += additions.length
|
|
43
|
+
return { ...slide, claimRefs: [...existing, ...additions] }
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const next: DecksState = {
|
|
47
|
+
...state,
|
|
48
|
+
narrative,
|
|
49
|
+
decks: {
|
|
50
|
+
...state.decks,
|
|
51
|
+
[deckKey]: {
|
|
52
|
+
...deck,
|
|
53
|
+
slides,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updatedRefs = getClaimSlideRefs(next, next.decks[deckKey])
|
|
59
|
+
const htmlTarget = ensureActiveHtmlDeckRenderTarget(next)
|
|
60
|
+
if (htmlTarget) {
|
|
61
|
+
htmlTarget.data = {
|
|
62
|
+
...(htmlTarget.data ?? {}),
|
|
63
|
+
narrativeId: narrative.id,
|
|
64
|
+
narrativeHash,
|
|
65
|
+
claimSlideRefs: updatedRefs.map((ref) => ({
|
|
66
|
+
claimId: ref.claimId,
|
|
67
|
+
claimText: ref.claimText,
|
|
68
|
+
slideIndex: ref.slideIndex,
|
|
69
|
+
slideTitle: ref.slideTitle,
|
|
70
|
+
match: ref.match,
|
|
71
|
+
role: ref.role,
|
|
72
|
+
location: ref.location,
|
|
73
|
+
})),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (addedCount > 0) {
|
|
78
|
+
recordWorkspaceAction(next, {
|
|
79
|
+
type: "artifact.coverage_backfilled",
|
|
80
|
+
actor: "revela-decks",
|
|
81
|
+
inputs: { activeDeck: deckKey, narrativeId: narrative.id },
|
|
82
|
+
outputs: { addedCount, slideCount: slides.length, narrativeHash },
|
|
83
|
+
status: "success",
|
|
84
|
+
summary: `Backfilled ${addedCount} slide claim reference${addedCount === 1 ? "" : "s"} from current artifact coverage.`,
|
|
85
|
+
nodeIds: [narrative.id, `artifact:${deck.outputPath ?? deckKey}`],
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { state: next, result: { updated: addedCount > 0, addedCount, slideCount: slides.length, narrativeHash, refs: updatedRefs } }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function backfilledRole(role: SlideClaimRefRole): SlideClaimRefRole {
|
|
93
|
+
return role
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function backfillNote(ref: ClaimSlideRef): string {
|
|
97
|
+
if (ref.match === "metadata") return `Backfilled from ${ref.location}.`
|
|
98
|
+
if (ref.match === "content") return `Backfilled from content match at ${ref.location}.`
|
|
99
|
+
return "Backfilled from slide evidence trace."
|
|
100
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { NarrativeMap, NarrativeMapClaimRelation } from "./map"
|
|
2
|
+
|
|
3
|
+
export type NarrativeViewLanguage = string
|
|
4
|
+
|
|
5
|
+
export interface NarrativeDisplayModel {
|
|
6
|
+
version: 1
|
|
7
|
+
language: NarrativeViewLanguage
|
|
8
|
+
pageTitle?: string
|
|
9
|
+
summaryLine?: string
|
|
10
|
+
labels?: Partial<NarrativeDisplayLabels>
|
|
11
|
+
claimCards?: NarrativeDisplayClaimCard[]
|
|
12
|
+
relations?: NarrativeDisplayRelation[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface NarrativeDisplayLabels {
|
|
16
|
+
eyebrow: string
|
|
17
|
+
claimFlow: string
|
|
18
|
+
flowNote: string
|
|
19
|
+
selectedClaim: string
|
|
20
|
+
claim: string
|
|
21
|
+
claimId: string
|
|
22
|
+
status: string
|
|
23
|
+
supportedScope: string
|
|
24
|
+
unsupportedScope: string
|
|
25
|
+
incomingRelations: string
|
|
26
|
+
outgoingRelations: string
|
|
27
|
+
evidence: string
|
|
28
|
+
objections: string
|
|
29
|
+
risks: string
|
|
30
|
+
researchGaps: string
|
|
31
|
+
coveredSlides: string
|
|
32
|
+
noClaims: string
|
|
33
|
+
none: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface NarrativeDisplayClaimCard {
|
|
37
|
+
claimId: string
|
|
38
|
+
displayTitle?: string
|
|
39
|
+
roleLabel?: string
|
|
40
|
+
narrativeJob?: string
|
|
41
|
+
evidenceSummary?: string
|
|
42
|
+
riskOrGapSummary?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface NarrativeDisplayRelation {
|
|
46
|
+
fromClaimId: string
|
|
47
|
+
toClaimId: string
|
|
48
|
+
relation: NarrativeMapClaimRelation["relation"]
|
|
49
|
+
displayLabel?: string
|
|
50
|
+
displayRationale?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ValidatedNarrativeDisplayModel {
|
|
54
|
+
version: 1
|
|
55
|
+
language: NarrativeViewLanguage
|
|
56
|
+
pageTitle?: string
|
|
57
|
+
summaryLine?: string
|
|
58
|
+
labels: NarrativeDisplayLabels
|
|
59
|
+
claimCards: Map<string, NarrativeDisplayClaimCard>
|
|
60
|
+
relations: Map<string, NarrativeDisplayRelation>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function defaultNarrativeDisplayLabels(language: NarrativeViewLanguage): NarrativeDisplayLabels {
|
|
64
|
+
if (isChineseLanguage(language)) {
|
|
65
|
+
return {
|
|
66
|
+
eyebrow: "只读主张流",
|
|
67
|
+
claimFlow: "主张推进",
|
|
68
|
+
flowNote: "点击主张查看证据、关系、风险、缺口和已覆盖页面。",
|
|
69
|
+
selectedClaim: "当前主张",
|
|
70
|
+
claim: "主张",
|
|
71
|
+
claimId: "主张 ID",
|
|
72
|
+
status: "状态",
|
|
73
|
+
supportedScope: "已支持范围",
|
|
74
|
+
unsupportedScope: "未支持范围",
|
|
75
|
+
incomingRelations: "前置关系",
|
|
76
|
+
outgoingRelations: "后续关系",
|
|
77
|
+
evidence: "证据",
|
|
78
|
+
objections: "反对意见",
|
|
79
|
+
risks: "风险",
|
|
80
|
+
researchGaps: "研究缺口",
|
|
81
|
+
coveredSlides: "已覆盖页面",
|
|
82
|
+
noClaims: "没有记录主张",
|
|
83
|
+
none: "无",
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (isJapaneseLanguage(language)) {
|
|
87
|
+
return {
|
|
88
|
+
eyebrow: "読み取り専用クレームフロー",
|
|
89
|
+
claimFlow: "クレームフロー",
|
|
90
|
+
flowNote: "クレームをクリックすると、根拠、関係、リスク、ギャップ、該当スライドを確認できます。",
|
|
91
|
+
selectedClaim: "選択中のクレーム",
|
|
92
|
+
claim: "クレーム",
|
|
93
|
+
claimId: "クレーム ID",
|
|
94
|
+
status: "ステータス",
|
|
95
|
+
supportedScope: "裏付けられた範囲",
|
|
96
|
+
unsupportedScope: "未裏付けの範囲",
|
|
97
|
+
incomingRelations: "入力関係",
|
|
98
|
+
outgoingRelations: "出力関係",
|
|
99
|
+
evidence: "根拠",
|
|
100
|
+
objections: "反論",
|
|
101
|
+
risks: "リスク",
|
|
102
|
+
researchGaps: "調査ギャップ",
|
|
103
|
+
coveredSlides: "対応スライド",
|
|
104
|
+
noClaims: "クレームは記録されていません",
|
|
105
|
+
none: "なし",
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
eyebrow: "Read-only claim flow board",
|
|
110
|
+
claimFlow: "Claim Flow",
|
|
111
|
+
flowNote: "Click a claim to inspect support, relation context, gaps, and covered slides.",
|
|
112
|
+
selectedClaim: "Selected claim",
|
|
113
|
+
claim: "Claim",
|
|
114
|
+
claimId: "Claim ID",
|
|
115
|
+
status: "Status",
|
|
116
|
+
supportedScope: "Supported scope",
|
|
117
|
+
unsupportedScope: "Unsupported scope",
|
|
118
|
+
incomingRelations: "Incoming relations",
|
|
119
|
+
outgoingRelations: "Outgoing relations",
|
|
120
|
+
evidence: "Evidence",
|
|
121
|
+
objections: "Objections",
|
|
122
|
+
risks: "Risks",
|
|
123
|
+
researchGaps: "Research gaps",
|
|
124
|
+
coveredSlides: "Covered slides",
|
|
125
|
+
noClaims: "No claims recorded",
|
|
126
|
+
none: "None",
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function validateNarrativeDisplayModel(map: NarrativeMap, input: NarrativeDisplayModel | undefined, language: NarrativeViewLanguage): ValidatedNarrativeDisplayModel {
|
|
131
|
+
const defaults = defaultNarrativeDisplayLabels(language)
|
|
132
|
+
if (!input) return emptyDisplayModel(language, defaults)
|
|
133
|
+
if (input.version !== 1) throw new Error("Narrative display model version must be 1.")
|
|
134
|
+
if (input.language !== language) throw new Error(`Narrative display model language must be ${language}.`)
|
|
135
|
+
|
|
136
|
+
const claimIds = new Set(map.claimFlow.map((claim) => claim.id))
|
|
137
|
+
const relationByKey = new Map(map.claimRelations.map((relation) => [relationKey(relation), relation]))
|
|
138
|
+
const claimCards = new Map<string, NarrativeDisplayClaimCard>()
|
|
139
|
+
for (const card of input.claimCards ?? []) {
|
|
140
|
+
if (!claimIds.has(card.claimId)) throw new Error(`Unknown display claimId: ${card.claimId}`)
|
|
141
|
+
claimCards.set(card.claimId, cleanClaimCard(card))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const relations = new Map<string, NarrativeDisplayRelation>()
|
|
145
|
+
for (const relation of input.relations ?? []) {
|
|
146
|
+
const key = relationKey(relation)
|
|
147
|
+
const canonical = relationByKey.get(key)
|
|
148
|
+
if (!canonical) throw new Error(`Display relation is not present in the narrative map: ${key}`)
|
|
149
|
+
relations.set(key, cleanRelation(relation, canonical))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
version: 1,
|
|
154
|
+
language,
|
|
155
|
+
pageTitle: clean(input.pageTitle),
|
|
156
|
+
summaryLine: clean(input.summaryLine),
|
|
157
|
+
labels: mergeLabels(defaults, input.labels),
|
|
158
|
+
claimCards,
|
|
159
|
+
relations,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function emptyDisplayModel(language: NarrativeViewLanguage, labels = defaultNarrativeDisplayLabels(language)): ValidatedNarrativeDisplayModel {
|
|
164
|
+
return { version: 1, language, labels, claimCards: new Map(), relations: new Map() }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function relationKey(relation: Pick<NarrativeDisplayRelation, "fromClaimId" | "toClaimId" | "relation">): string {
|
|
168
|
+
return `${relation.fromClaimId}\u0000${relation.toClaimId}\u0000${relation.relation}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isChineseLanguage(language: string): boolean {
|
|
172
|
+
const normalized = language.trim().toLowerCase()
|
|
173
|
+
return normalized === "zh" || normalized === "zh-cn" || normalized === "cn" || normalized === "chinese" || normalized.includes("中文") || normalized.includes("简体")
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function isJapaneseLanguage(language: string): boolean {
|
|
177
|
+
const normalized = language.trim().toLowerCase()
|
|
178
|
+
return normalized === "ja" || normalized === "ja-jp" || normalized === "jp" || normalized === "japanese" || normalized.includes("日本")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function mergeLabels(defaults: NarrativeDisplayLabels, overrides: Partial<NarrativeDisplayLabels> | undefined): NarrativeDisplayLabels {
|
|
182
|
+
const merged: NarrativeDisplayLabels = { ...defaults }
|
|
183
|
+
if (!overrides) return merged
|
|
184
|
+
for (const key of Object.keys(defaults) as Array<keyof NarrativeDisplayLabels>) {
|
|
185
|
+
merged[key] = clean(overrides[key]) ?? defaults[key]
|
|
186
|
+
}
|
|
187
|
+
return merged
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cleanClaimCard(card: NarrativeDisplayClaimCard): NarrativeDisplayClaimCard {
|
|
191
|
+
return {
|
|
192
|
+
claimId: card.claimId,
|
|
193
|
+
displayTitle: clean(card.displayTitle),
|
|
194
|
+
roleLabel: clean(card.roleLabel),
|
|
195
|
+
narrativeJob: clean(card.narrativeJob),
|
|
196
|
+
evidenceSummary: clean(card.evidenceSummary),
|
|
197
|
+
riskOrGapSummary: clean(card.riskOrGapSummary),
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function cleanRelation(relation: NarrativeDisplayRelation, canonical: NarrativeMapClaimRelation): NarrativeDisplayRelation {
|
|
202
|
+
const displayLabel = clean(relation.displayLabel)
|
|
203
|
+
const displayRationale = clean(relation.displayRationale)
|
|
204
|
+
if (displayLabel && canonical.inferred) throw new Error("Display label cannot replace inferred, non-canonical claim relation status.")
|
|
205
|
+
if (displayRationale && canonical.inferred) throw new Error("Display rationale cannot replace inferred, non-canonical claim relation rationale.")
|
|
206
|
+
if (displayRationale && !canonical.rationale?.trim()) throw new Error("Display rationale requires canonical claim relation rationale.")
|
|
207
|
+
return {
|
|
208
|
+
fromClaimId: relation.fromClaimId,
|
|
209
|
+
toClaimId: relation.toClaimId,
|
|
210
|
+
relation: relation.relation,
|
|
211
|
+
displayLabel,
|
|
212
|
+
displayRationale,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function clean(value: string | undefined): string | undefined {
|
|
217
|
+
const text = value?.trim()
|
|
218
|
+
return text || undefined
|
|
219
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
3
|
+
import {
|
|
4
|
+
artifactNodeIdForRenderTarget,
|
|
5
|
+
normalizeWorkspacePath,
|
|
6
|
+
renderTargetId,
|
|
7
|
+
upsertRenderTarget,
|
|
8
|
+
} from "../workspace-state/render-targets"
|
|
9
|
+
import type { RenderTarget } from "../workspace-state/types"
|
|
10
|
+
import { computeNarrativeHash } from "./hash"
|
|
11
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
12
|
+
import { reviewNarrativeState } from "./readiness"
|
|
13
|
+
import type { NarrativeApproval, NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "./types"
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_EXECUTIVE_BRIEF_PATH = "briefs/executive-brief.md"
|
|
16
|
+
|
|
17
|
+
export interface CompileExecutiveBriefOptions {
|
|
18
|
+
outputPath?: string
|
|
19
|
+
now?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type CompileExecutiveBriefResult =
|
|
23
|
+
| {
|
|
24
|
+
ok: true
|
|
25
|
+
state: DecksState
|
|
26
|
+
outputPath: string
|
|
27
|
+
content: string
|
|
28
|
+
target: RenderTarget
|
|
29
|
+
narrativeHash: string
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
ok: false
|
|
33
|
+
state: DecksState
|
|
34
|
+
reason: string
|
|
35
|
+
narrativeHash?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function compileExecutiveBrief(state: DecksState, options: CompileExecutiveBriefOptions = {}): CompileExecutiveBriefResult {
|
|
39
|
+
const now = options.now ?? new Date().toISOString()
|
|
40
|
+
const reviewed = reviewNarrativeState(state, { now })
|
|
41
|
+
const narrative = reviewed.state.narrative ?? normalizeNarrativeState(reviewed.state)
|
|
42
|
+
const narrativeHash = reviewed.result.narrativeHash || computeNarrativeHash(narrative)
|
|
43
|
+
const allowedApproval = currentNarrativeApprovalOrOverride(narrative, narrativeHash)
|
|
44
|
+
|
|
45
|
+
if (!allowedApproval) {
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
state: reviewed.state,
|
|
49
|
+
reason: "Executive brief rendering requires current narrative approval or an explicit render override.",
|
|
50
|
+
narrativeHash,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const outputPath = normalizeWorkspacePath(options.outputPath || DEFAULT_EXECUTIVE_BRIEF_PATH)
|
|
55
|
+
const content = renderExecutiveBriefMarkdown(narrative, narrativeHash, now, allowedApproval)
|
|
56
|
+
const claimIds = narrative.claims.map((claim) => claim.id).sort()
|
|
57
|
+
const evidenceBindingIds = narrative.evidenceBindings.map((binding) => binding.id).sort()
|
|
58
|
+
const target: RenderTarget = {
|
|
59
|
+
id: renderTargetId("executive_brief", outputPath),
|
|
60
|
+
type: "executive_brief",
|
|
61
|
+
outputPath,
|
|
62
|
+
sourceNodeIds: [narrative.id, ...claimIds, ...evidenceBindingIds],
|
|
63
|
+
artifactVersion: narrativeHash,
|
|
64
|
+
contractStatus: "valid",
|
|
65
|
+
data: {
|
|
66
|
+
narrativeHash,
|
|
67
|
+
generatedAt: now,
|
|
68
|
+
format: "markdown",
|
|
69
|
+
claimIds,
|
|
70
|
+
evidenceBindingIds,
|
|
71
|
+
approvalId: allowedApproval.id,
|
|
72
|
+
approvalScope: allowedApproval.scope,
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const next: DecksState = { ...reviewed.state, narrative }
|
|
77
|
+
upsertRenderTarget(next, target)
|
|
78
|
+
recordWorkspaceAction(next, {
|
|
79
|
+
type: "artifact.rendered",
|
|
80
|
+
actor: "revela-brief",
|
|
81
|
+
inputs: {
|
|
82
|
+
type: "executive_brief",
|
|
83
|
+
narrativeId: narrative.id,
|
|
84
|
+
narrativeHash,
|
|
85
|
+
approvalId: allowedApproval.id,
|
|
86
|
+
},
|
|
87
|
+
outputs: {
|
|
88
|
+
outputPath,
|
|
89
|
+
targetId: target.id,
|
|
90
|
+
claimCount: claimIds.length,
|
|
91
|
+
evidenceBindingCount: evidenceBindingIds.length,
|
|
92
|
+
},
|
|
93
|
+
status: "success",
|
|
94
|
+
summary: "Rendered executive brief from approved narrative state.",
|
|
95
|
+
nodeIds: [target.id, artifactNodeIdForRenderTarget(target), narrative.id, ...claimIds],
|
|
96
|
+
timestamp: now,
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
return { ok: true, state: next, outputPath, content, target, narrativeHash }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function currentNarrativeApprovalOrOverride(narrative: NarrativeStateV1, narrativeHash: string): NarrativeApproval | undefined {
|
|
103
|
+
const approvals = [...(narrative.approvals ?? [])]
|
|
104
|
+
for (let index = approvals.length - 1; index >= 0; index -= 1) {
|
|
105
|
+
const approval = approvals[index]
|
|
106
|
+
if (approval.narrativeHash !== narrativeHash) continue
|
|
107
|
+
if (approval.scope === "narrative" && approval.approvedBy === "user") return approval
|
|
108
|
+
if (approval.scope === "render_override" || approval.approvedBy === "override") return approval
|
|
109
|
+
}
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderExecutiveBriefMarkdown(narrative: NarrativeStateV1, narrativeHash: string, generatedAt: string, approval: NarrativeApproval): string {
|
|
114
|
+
const evidenceByClaim = groupEvidenceByClaim(narrative.evidenceBindings)
|
|
115
|
+
const centralClaims = narrative.claims.filter((claim) => claim.importance === "central")
|
|
116
|
+
const supportingClaims = narrative.claims.filter((claim) => claim.importance !== "central")
|
|
117
|
+
const lines: string[] = []
|
|
118
|
+
|
|
119
|
+
lines.push("# Executive Brief")
|
|
120
|
+
lines.push("")
|
|
121
|
+
lines.push(`Generated: ${generatedAt}`)
|
|
122
|
+
lines.push(`Narrative ID: ${narrative.id}`)
|
|
123
|
+
lines.push(`Narrative hash: ${narrativeHash}`)
|
|
124
|
+
lines.push(`Approval: ${approval.id} (${approval.scope}, ${approval.approvedBy})`)
|
|
125
|
+
lines.push("")
|
|
126
|
+
lines.push("## Decision Context")
|
|
127
|
+
lines.push(`- Audience: ${fallback(narrative.audience.primary)}`)
|
|
128
|
+
lines.push(`- Belief before: ${fallback(narrative.audience.beliefBefore)}`)
|
|
129
|
+
lines.push(`- Belief after: ${fallback(narrative.audience.beliefAfter)}`)
|
|
130
|
+
lines.push(`- Decision/action: ${fallback(narrative.decision.action)}`)
|
|
131
|
+
if (narrative.decision.owner) lines.push(`- Owner: ${narrative.decision.owner}`)
|
|
132
|
+
if (narrative.decision.deadline) lines.push(`- Deadline: ${narrative.decision.deadline}`)
|
|
133
|
+
if (narrative.decision.consequenceOfNoDecision) lines.push(`- Consequence of no decision: ${narrative.decision.consequenceOfNoDecision}`)
|
|
134
|
+
lines.push("")
|
|
135
|
+
lines.push("## Thesis")
|
|
136
|
+
lines.push(narrative.thesis?.statement ? `${narrative.thesis.statement} (${narrative.thesis.confidence} confidence)` : "Not recorded.")
|
|
137
|
+
if (narrative.thesis?.caveat) lines.push(`Caveat: ${narrative.thesis.caveat}`)
|
|
138
|
+
lines.push("")
|
|
139
|
+
|
|
140
|
+
appendClaims(lines, "Central Claims", centralClaims, evidenceByClaim)
|
|
141
|
+
appendClaims(lines, "Supporting Claims", supportingClaims, evidenceByClaim)
|
|
142
|
+
appendObjections(lines, narrative)
|
|
143
|
+
appendRisks(lines, narrative)
|
|
144
|
+
appendResearchGaps(lines, narrative)
|
|
145
|
+
lines.push("## Provenance")
|
|
146
|
+
lines.push(`- Render target: executive_brief`)
|
|
147
|
+
lines.push(`- Source narrative: ${narrative.id}`)
|
|
148
|
+
lines.push(`- Narrative hash: ${narrativeHash}`)
|
|
149
|
+
lines.push(`- Approval id: ${approval.id}`)
|
|
150
|
+
lines.push("- This brief is compiled from canonical narrative state, not from a deck summary.")
|
|
151
|
+
lines.push("")
|
|
152
|
+
|
|
153
|
+
return lines.join("\n")
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function appendClaims(lines: string[], title: string, claims: NarrativeClaim[], evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): void {
|
|
157
|
+
lines.push(`## ${title}`)
|
|
158
|
+
if (claims.length === 0) {
|
|
159
|
+
lines.push("Not recorded.")
|
|
160
|
+
lines.push("")
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const claim of claims) {
|
|
165
|
+
lines.push(`### ${claim.text}`)
|
|
166
|
+
lines.push(`- Claim ID: ${claim.id}`)
|
|
167
|
+
lines.push(`- Kind: ${claim.kind}`)
|
|
168
|
+
lines.push(`- Evidence status: ${claim.evidenceStatus}`)
|
|
169
|
+
if (claim.supportedScope) lines.push(`- Supported scope: ${claim.supportedScope}`)
|
|
170
|
+
if (claim.unsupportedScope) lines.push(`- Unsupported scope: ${claim.unsupportedScope}`)
|
|
171
|
+
for (const caveat of claim.caveats ?? []) lines.push(`- Caveat: ${caveat}`)
|
|
172
|
+
const bindings = evidenceByClaim.get(claim.id) ?? []
|
|
173
|
+
if (bindings.length === 0) lines.push("- Evidence: none bound")
|
|
174
|
+
else {
|
|
175
|
+
lines.push("- Evidence:")
|
|
176
|
+
for (const binding of bindings) appendEvidence(lines, binding)
|
|
177
|
+
}
|
|
178
|
+
lines.push("")
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function appendEvidence(lines: string[], binding: NarrativeEvidenceBinding): void {
|
|
183
|
+
lines.push(` - ${binding.id} (${binding.strength})`)
|
|
184
|
+
lines.push(` - Source: ${binding.source}`)
|
|
185
|
+
if (binding.findingsFile) lines.push(` - Findings file: ${binding.findingsFile}`)
|
|
186
|
+
if (binding.sourcePath) lines.push(` - Source path: ${binding.sourcePath}`)
|
|
187
|
+
if (binding.location) lines.push(` - Location: ${binding.location}`)
|
|
188
|
+
if (binding.url) lines.push(` - URL: ${binding.url}`)
|
|
189
|
+
if (binding.quote) lines.push(` - Quote: ${binding.quote}`)
|
|
190
|
+
if (binding.supportScope) lines.push(` - Support scope: ${binding.supportScope}`)
|
|
191
|
+
if (binding.unsupportedScope) lines.push(` - Unsupported scope: ${binding.unsupportedScope}`)
|
|
192
|
+
if (binding.caveat) lines.push(` - Caveat: ${binding.caveat}`)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function appendObjections(lines: string[], narrative: NarrativeStateV1): void {
|
|
196
|
+
lines.push("## Objections")
|
|
197
|
+
if (narrative.objections.length === 0) lines.push("Not recorded.")
|
|
198
|
+
for (const objection of narrative.objections) {
|
|
199
|
+
lines.push(`- ${objection.text}`)
|
|
200
|
+
lines.push(` - Objection ID: ${objection.id}`)
|
|
201
|
+
if (objection.claimId) lines.push(` - Challenges claim: ${objection.claimId}`)
|
|
202
|
+
lines.push(` - Priority: ${objection.priority}`)
|
|
203
|
+
if (objection.response) lines.push(` - Response: ${objection.response}`)
|
|
204
|
+
}
|
|
205
|
+
lines.push("")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function appendRisks(lines: string[], narrative: NarrativeStateV1): void {
|
|
209
|
+
lines.push("## Risks")
|
|
210
|
+
if (narrative.risks.length === 0) lines.push("Not recorded.")
|
|
211
|
+
for (const risk of narrative.risks) {
|
|
212
|
+
lines.push(`- ${risk.text}`)
|
|
213
|
+
lines.push(` - Risk ID: ${risk.id}`)
|
|
214
|
+
if (risk.claimId) lines.push(` - Constrains claim: ${risk.claimId}`)
|
|
215
|
+
lines.push(` - Severity: ${risk.severity}`)
|
|
216
|
+
if (risk.mitigation) lines.push(` - Mitigation: ${risk.mitigation}`)
|
|
217
|
+
}
|
|
218
|
+
lines.push("")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function appendResearchGaps(lines: string[], narrative: NarrativeStateV1): void {
|
|
222
|
+
lines.push("## Research Gaps")
|
|
223
|
+
const gaps = narrative.researchGaps ?? []
|
|
224
|
+
if (gaps.length === 0) lines.push("Not recorded.")
|
|
225
|
+
for (const gap of gaps) {
|
|
226
|
+
lines.push(`- ${gap.question}`)
|
|
227
|
+
lines.push(` - Gap ID: ${gap.id}`)
|
|
228
|
+
lines.push(` - Status: ${gap.status}`)
|
|
229
|
+
lines.push(` - Priority: ${gap.priority}`)
|
|
230
|
+
if (gap.targetId) lines.push(` - Target: ${gap.targetType}:${gap.targetId}`)
|
|
231
|
+
if (gap.findingsFile) lines.push(` - Findings file: ${gap.findingsFile}`)
|
|
232
|
+
if (gap.evidenceBindingIds?.length) lines.push(` - Evidence bindings: ${gap.evidenceBindingIds.join(", ")}`)
|
|
233
|
+
if (gap.notes) lines.push(` - Notes: ${gap.notes}`)
|
|
234
|
+
}
|
|
235
|
+
lines.push("")
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function groupEvidenceByClaim(bindings: NarrativeEvidenceBinding[]): Map<string, NarrativeEvidenceBinding[]> {
|
|
239
|
+
const grouped = new Map<string, NarrativeEvidenceBinding[]>()
|
|
240
|
+
for (const binding of bindings) grouped.set(binding.claimId, [...(grouped.get(binding.claimId) ?? []), binding])
|
|
241
|
+
return grouped
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function fallback(value: string | undefined): string {
|
|
245
|
+
return value?.trim() || "Not recorded"
|
|
246
|
+
}
|
|
@@ -9,6 +9,10 @@ export function stableClaimId(text: string): string {
|
|
|
9
9
|
return `claim:${stableHash(text)}`
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
export function stableClaimRelationId(fromClaimId: string, toClaimId: string, relation: string): string {
|
|
13
|
+
return `claim-relation:${stableHash(`${fromClaimId}:${toClaimId}:${relation}`)}`
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
export function stableEvidenceId(claimId: string, seed: string): string {
|
|
13
17
|
return `evidence:${claimId}:${stableHash(seed)}`
|
|
14
18
|
}
|
|
@@ -21,6 +25,10 @@ export function stableRiskId(text: string): string {
|
|
|
21
25
|
return `risk:${stableHash(text)}`
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
export function stableResearchGapId(seed: string): string {
|
|
29
|
+
return `research-gap:${stableHash(seed)}`
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
export function computeNarrativeHash(narrative: NarrativeStateV1): string {
|
|
25
33
|
return stableHash(stableStringify({
|
|
26
34
|
version: narrative.version,
|
|
@@ -29,6 +37,7 @@ export function computeNarrativeHash(narrative: NarrativeStateV1): string {
|
|
|
29
37
|
decision: narrative.decision,
|
|
30
38
|
thesis: narrative.thesis,
|
|
31
39
|
claims: narrative.claims,
|
|
40
|
+
claimRelations: narrative.claimRelations ?? [],
|
|
32
41
|
evidenceBindings: narrative.evidenceBindings,
|
|
33
42
|
objections: narrative.objections,
|
|
34
43
|
risks: narrative.risks,
|