@cyber-dash-tech/revela 0.16.3 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +26 -2
- package/lib/commands/research.ts +36 -20
- package/lib/commands/review.ts +21 -18
- 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/narrative-state/render-plan.ts +114 -27
- 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/refine/server.ts +472 -5
- package/lib/refine/visual-targets.ts +295 -0
- 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/tools/decks.ts +363 -67
- package/tools/research-save.ts +3 -0
- package/tools/workspace-scan.ts +1 -0
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
|
+
}
|
|
@@ -17,9 +17,18 @@ export interface CompileDeckPlanResult {
|
|
|
17
17
|
narrativeHash: string
|
|
18
18
|
slideCount: number
|
|
19
19
|
slides: SlideSpec[]
|
|
20
|
+
chapters?: DeckPlanChapter[]
|
|
20
21
|
qualityChecks?: DeckPlanQualityCheck[]
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export interface DeckPlanChapter {
|
|
25
|
+
title: string
|
|
26
|
+
role: "context" | "tension" | "evidence" | "recommendation" | "risk" | "ask"
|
|
27
|
+
slideIndexes: number[]
|
|
28
|
+
claimIds: string[]
|
|
29
|
+
evidenceBindingIds: string[]
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export interface DeckPlanQualityCheck {
|
|
24
33
|
id: string
|
|
25
34
|
status: "pass" | "warning" | "blocker"
|
|
@@ -47,8 +56,9 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
47
56
|
const deckKey = state.activeDeck ?? Object.keys(state.decks)[0]
|
|
48
57
|
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
49
58
|
const slug = deck?.slug ?? state.activeDeck ?? "deck"
|
|
50
|
-
const
|
|
51
|
-
const
|
|
59
|
+
const plan = buildDeckPlan(narrative)
|
|
60
|
+
const { slides, chapters } = plan
|
|
61
|
+
const qualityChecks = checkPlanQuality(narrative, slides, chapters)
|
|
52
62
|
const planCoverage = deckPlanCoverage(narrative, slides)
|
|
53
63
|
const requiredInputs: Partial<RequiredInputs> = {
|
|
54
64
|
topicClarified: true,
|
|
@@ -91,8 +101,9 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
91
101
|
...(htmlTarget.data ?? {}),
|
|
92
102
|
narrativeId: narrative.id,
|
|
93
103
|
narrativeHash,
|
|
94
|
-
|
|
95
|
-
|
|
104
|
+
planQualityChecks: qualityChecks,
|
|
105
|
+
planChapters: chapters,
|
|
106
|
+
requiredClaimIds: planCoverage.requiredClaimIds,
|
|
96
107
|
coveredClaimIds: planCoverage.coveredClaimIds,
|
|
97
108
|
missingClaimIds: planCoverage.missingClaimIds,
|
|
98
109
|
claimSlideRefs: getClaimSlideRefs(next).map((ref) => ({
|
|
@@ -115,33 +126,45 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
115
126
|
narrativeHash,
|
|
116
127
|
slideCount: slides.length,
|
|
117
128
|
slides,
|
|
129
|
+
chapters,
|
|
118
130
|
qualityChecks,
|
|
119
131
|
},
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
134
|
|
|
123
|
-
function
|
|
135
|
+
function buildDeckPlan(narrative: NarrativeStateV1): { slides: SlideSpec[]; chapters: DeckPlanChapter[] } {
|
|
124
136
|
const slides: SlideSpec[] = []
|
|
125
137
|
const evidenceByClaim = evidenceBindingsByClaim(narrative.evidenceBindings)
|
|
126
138
|
const centralClaims = orderedClaims(narrative, (claim) => claim.importance === "central")
|
|
127
139
|
const supportingClaims = orderedClaims(narrative, (claim) => claim.importance !== "central")
|
|
128
|
-
const chapters = deriveChapters(narrative, centralClaims, supportingClaims)
|
|
140
|
+
const chapters = deriveChapters(narrative, centralClaims, supportingClaims).map((chapter) => ({ ...chapter }))
|
|
129
141
|
|
|
130
142
|
slides.push(coverSlide(slides.length + 1, narrative))
|
|
143
|
+
assignSlideToChapter(chapters, "context", slides[slides.length - 1])
|
|
131
144
|
slides.push(tocSlide(slides.length + 1, chapters))
|
|
132
145
|
|
|
133
146
|
for (const claim of centralClaims) {
|
|
134
|
-
|
|
147
|
+
const slide = claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? [])
|
|
148
|
+
slides.push(slide)
|
|
149
|
+
assignSlideToChapter(chapters, chapterRoleForClaim(claim), slide)
|
|
150
|
+
}
|
|
151
|
+
if (supportingClaims.length > 0) {
|
|
152
|
+
const slide = supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim)
|
|
153
|
+
slides.push(slide)
|
|
154
|
+
assignSlideToChapter(chapters, "evidence", slide)
|
|
135
155
|
}
|
|
136
|
-
if (supportingClaims.length > 0) slides.push(supportingLogicSlide(slides.length + 1, supportingClaims, evidenceByClaim))
|
|
137
156
|
|
|
138
157
|
if (narrative.risks.length > 0 || narrative.objections.length > 0) {
|
|
139
|
-
|
|
158
|
+
const slide = riskObjectionSlide(slides.length + 1, narrative, evidenceByClaim)
|
|
159
|
+
slides.push(slide)
|
|
160
|
+
assignSlideToChapter(chapters, "risk", slide)
|
|
140
161
|
}
|
|
141
162
|
|
|
142
|
-
|
|
163
|
+
const decisionSlide = decisionAskSlide(slides.length + 1, narrative)
|
|
164
|
+
slides.push(decisionSlide)
|
|
165
|
+
assignSlideToChapter(chapters, "ask", decisionSlide)
|
|
143
166
|
|
|
144
|
-
return slides
|
|
167
|
+
return { slides, chapters }
|
|
145
168
|
}
|
|
146
169
|
|
|
147
170
|
function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
@@ -166,7 +189,7 @@ function coverSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
|
166
189
|
}
|
|
167
190
|
}
|
|
168
191
|
|
|
169
|
-
function tocSlide(index: number, chapters:
|
|
192
|
+
function tocSlide(index: number, chapters: DeckPlanChapter[]): SlideSpec {
|
|
170
193
|
return {
|
|
171
194
|
index,
|
|
172
195
|
title: "Storyline",
|
|
@@ -177,7 +200,8 @@ function tocSlide(index: number, chapters: string[]): SlideSpec {
|
|
|
177
200
|
components: ["toc", "text-panel"],
|
|
178
201
|
content: {
|
|
179
202
|
headline: "How the decision story is organized",
|
|
180
|
-
bullets: chapters,
|
|
203
|
+
bullets: chapters.map((chapter) => chapter.title),
|
|
204
|
+
data: { chapters: chapters.map((chapter) => ({ title: chapter.title, role: chapter.role })) },
|
|
181
205
|
},
|
|
182
206
|
evidence: [],
|
|
183
207
|
status: "planned",
|
|
@@ -220,19 +244,20 @@ function supportingLogicSlide(index: number, claims: NarrativeClaim[], evidenceB
|
|
|
220
244
|
evidenceBindingIds: supportingBindings.map((binding) => binding.id),
|
|
221
245
|
content: {
|
|
222
246
|
headline: "Supporting claims and boundaries",
|
|
223
|
-
bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim)]).slice(0, 8),
|
|
247
|
+
bullets: claims.slice(0, 5).flatMap((claim) => [claim.text, ...claimBoundaryBullets(claim), evidenceGapBullet(claim, evidenceByClaim.get(claim.id) ?? [])]).filter((item): item is string => Boolean(item)).slice(0, 8),
|
|
224
248
|
},
|
|
225
249
|
evidence: supportingBindings.map(evidenceRefFromBinding),
|
|
226
250
|
status: "planned",
|
|
227
251
|
}
|
|
228
252
|
}
|
|
229
253
|
|
|
230
|
-
function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSpec {
|
|
254
|
+
function riskObjectionSlide(index: number, narrative: NarrativeStateV1, evidenceByClaim: Map<string, NarrativeEvidenceBinding[]>): SlideSpec {
|
|
231
255
|
const challengedClaimRefs = [
|
|
232
256
|
...narrative.risks.map((risk) => risk.claimId ? { claimId: risk.claimId, role: "risk" as const } : undefined).filter((ref): ref is { claimId: string; role: "risk" } => Boolean(ref)),
|
|
233
257
|
...narrative.objections.map((objection) => objection.claimId ? { claimId: objection.claimId, role: "objection" as const } : undefined).filter((ref): ref is { claimId: string; role: "objection" } => Boolean(ref)),
|
|
234
258
|
]
|
|
235
259
|
const challengedClaimIds = [...new Set(challengedClaimRefs.map((ref) => ref.claimId))]
|
|
260
|
+
const challengedBindings = challengedClaimIds.flatMap((claimId) => evidenceByClaim.get(claimId) ?? [])
|
|
236
261
|
return {
|
|
237
262
|
index,
|
|
238
263
|
title: "Risks And Objections",
|
|
@@ -243,6 +268,7 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSp
|
|
|
243
268
|
components: ["box", "text-panel"],
|
|
244
269
|
claimIds: challengedClaimIds,
|
|
245
270
|
claimRefs: dedupeClaimRefs(challengedClaimRefs),
|
|
271
|
+
evidenceBindingIds: challengedBindings.map((binding) => binding.id),
|
|
246
272
|
content: {
|
|
247
273
|
headline: "What could break the recommendation",
|
|
248
274
|
bullets: [
|
|
@@ -250,7 +276,7 @@ function riskObjectionSlide(index: number, narrative: NarrativeStateV1): SlideSp
|
|
|
250
276
|
...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
|
|
251
277
|
],
|
|
252
278
|
},
|
|
253
|
-
evidence:
|
|
279
|
+
evidence: challengedBindings.map(evidenceRefFromBinding),
|
|
254
280
|
status: "planned",
|
|
255
281
|
}
|
|
256
282
|
}
|
|
@@ -302,22 +328,51 @@ function orderedClaims(narrative: NarrativeStateV1, predicate: (claim: Narrative
|
|
|
302
328
|
.sort((a, b) => (relationScore.get(b.id) ?? 0) - (relationScore.get(a.id) ?? 0) || (sourceOrder.get(a.id) ?? 0) - (sourceOrder.get(b.id) ?? 0))
|
|
303
329
|
}
|
|
304
330
|
|
|
305
|
-
function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeClaim[], supportingClaims: NarrativeClaim[]):
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
if (
|
|
311
|
-
if (
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
331
|
+
function deriveChapters(narrative: NarrativeStateV1, centralClaims: NarrativeClaim[], supportingClaims: NarrativeClaim[]): DeckPlanChapter[] {
|
|
332
|
+
const claims = [...centralClaims, ...supportingClaims]
|
|
333
|
+
const chapters: DeckPlanChapter[] = []
|
|
334
|
+
addChapter(chapters, narrative.audience.decisionContext ? "Decision context" : "Context and belief shift", "context")
|
|
335
|
+
if (hasClaimKind(claims, ["problem", "opportunity"])) addChapter(chapters, "Tension and opportunity", "tension")
|
|
336
|
+
if (claims.some((claim) => claim.kind === "evidence") || narrative.evidenceBindings.length > 0) addChapter(chapters, "Evidence and proof", "evidence")
|
|
337
|
+
if (claims.some((claim) => claim.kind === "recommendation" || claim.kind === "ask") || narrative.decision.action) addChapter(chapters, "Recommendation and decision", "recommendation")
|
|
338
|
+
if (narrative.risks.length > 0 || narrative.objections.length > 0 || centralClaims.some((claim) => claim.unsupportedScope || (claim.caveats ?? []).length > 0)) addChapter(chapters, "Risks and boundaries", "risk")
|
|
339
|
+
addChapter(chapters, "Decision ask", "ask")
|
|
340
|
+
if (chapters.length < 3) addChapter(chapters, "Evidence and proof", "evidence")
|
|
341
|
+
while (chapters.length > 5) {
|
|
342
|
+
const tensionIndex = chapters.findIndex((chapter) => chapter.role === "tension")
|
|
343
|
+
if (tensionIndex >= 0) chapters.splice(tensionIndex, 1)
|
|
344
|
+
else chapters.splice(Math.max(1, chapters.length - 2), 1)
|
|
345
|
+
}
|
|
346
|
+
return chapters
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function addChapter(chapters: DeckPlanChapter[], title: string, role: DeckPlanChapter["role"]): void {
|
|
350
|
+
if (chapters.some((chapter) => chapter.role === role || chapter.title === title)) return
|
|
351
|
+
chapters.push({ title, role, slideIndexes: [], claimIds: [], evidenceBindingIds: [] })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function assignSlideToChapter(chapters: DeckPlanChapter[], role: DeckPlanChapter["role"], slide: SlideSpec): void {
|
|
355
|
+
const chapter = chapters.find((item) => item.role === role) ?? chapters.find((item) => item.role === "evidence") ?? chapters[chapters.length - 1]
|
|
356
|
+
if (!chapter) return
|
|
357
|
+
chapter.slideIndexes.push(slide.index)
|
|
358
|
+
for (const claimId of slide.claimIds ?? []) addUnique(chapter.claimIds, claimId)
|
|
359
|
+
for (const ref of slide.claimRefs ?? []) addUnique(chapter.claimIds, ref.claimId)
|
|
360
|
+
for (const bindingId of slide.evidenceBindingIds ?? []) addUnique(chapter.evidenceBindingIds, bindingId)
|
|
315
361
|
}
|
|
316
362
|
|
|
317
363
|
function addUnique(items: string[], item: string): void {
|
|
318
364
|
if (!items.includes(item)) items.push(item)
|
|
319
365
|
}
|
|
320
366
|
|
|
367
|
+
function chapterRoleForClaim(claim: NarrativeClaim): DeckPlanChapter["role"] {
|
|
368
|
+
if (claim.kind === "problem" || claim.kind === "opportunity") return "tension"
|
|
369
|
+
if (claim.kind === "recommendation") return "recommendation"
|
|
370
|
+
if (claim.kind === "ask") return "ask"
|
|
371
|
+
if (claim.kind === "risk" || claim.kind === "assumption") return "risk"
|
|
372
|
+
if (claim.kind === "context") return "context"
|
|
373
|
+
return "evidence"
|
|
374
|
+
}
|
|
375
|
+
|
|
321
376
|
function hasClaimKind(claims: NarrativeClaim[], kinds: NarrativeClaim["kind"][]): boolean {
|
|
322
377
|
return claims.some((claim) => kinds.includes(claim.kind))
|
|
323
378
|
}
|
|
@@ -332,9 +387,15 @@ function claimBullets(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[
|
|
|
332
387
|
return [
|
|
333
388
|
...claimBoundaryBullets(claim),
|
|
334
389
|
...bindings.slice(0, 2).map((binding) => binding.supportScope ? `Evidence supports: ${binding.supportScope}` : undefined),
|
|
390
|
+
evidenceGapBullet(claim, bindings),
|
|
335
391
|
].filter((item): item is string => Boolean(item))
|
|
336
392
|
}
|
|
337
393
|
|
|
394
|
+
function evidenceGapBullet(claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): string | undefined {
|
|
395
|
+
if (!claim.evidenceRequired || bindings.length > 0) return undefined
|
|
396
|
+
return `Evidence gap: ${claim.evidenceStatus === "missing" ? "no binding yet" : "support remains incomplete"}.`
|
|
397
|
+
}
|
|
398
|
+
|
|
338
399
|
function claimBoundaryBullets(claim: NarrativeClaim): string[] {
|
|
339
400
|
return [
|
|
340
401
|
claim.supportedScope ? `Supported scope: ${claim.supportedScope}` : undefined,
|
|
@@ -370,13 +431,29 @@ function evidenceRefFromBinding(binding: NarrativeEvidenceBinding): EvidenceRef
|
|
|
370
431
|
}
|
|
371
432
|
}
|
|
372
433
|
|
|
373
|
-
function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[]): DeckPlanQualityCheck[] {
|
|
434
|
+
function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[], chapters: DeckPlanChapter[]): DeckPlanQualityCheck[] {
|
|
374
435
|
const coverage = deckPlanCoverage(narrative, slides)
|
|
375
436
|
const centralClaimIds = narrative.claims.filter((claim) => claim.importance === "central").map((claim) => claim.id)
|
|
376
437
|
const missingCentralClaims = centralClaimIds.filter((claimId) => coverage.missingClaimIds.includes(claimId))
|
|
377
438
|
const incompatibleComponents = [...new Set(slides.flatMap((slide) => slide.components).filter((component) => component === "card"))]
|
|
439
|
+
const toc = slides.find((slide) => slide.components.includes("toc"))
|
|
440
|
+
const tocBullets = toc?.content.bullets ?? []
|
|
441
|
+
const chapterTitles = chapters.map((chapter) => chapter.title)
|
|
442
|
+
const evidenceRequiredWithoutBindings = narrative.claims.filter((claim) => claim.evidenceRequired && !narrative.evidenceBindings.some((binding) => binding.claimId === claim.id))
|
|
443
|
+
const invisibleEvidenceGaps = evidenceRequiredWithoutBindings.filter((claim) => coverage.missingClaimIds.includes(claim.id))
|
|
444
|
+
const risksOrObjectionsVisible = narrative.risks.length === 0 && narrative.objections.length === 0 || slides.some((slide) => slide.narrativeRole === "risk")
|
|
378
445
|
|
|
379
446
|
return [
|
|
447
|
+
{
|
|
448
|
+
id: "chapter_structure_present",
|
|
449
|
+
status: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? "pass" : "blocker",
|
|
450
|
+
message: chapters.length >= 3 && chapters.length <= 5 && chapters.every((chapter) => chapter.slideIndexes.length > 0) ? `Deck plan includes ${chapters.length} deterministic chapters with slide ranges.` : "Deck plan must include 3-5 deterministic chapters, each mapped to at least one slide.",
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
id: "toc_matches_chapters",
|
|
454
|
+
status: chapterTitles.length > 0 && chapterTitles.every((title) => tocBullets.includes(title)) ? "pass" : "blocker",
|
|
455
|
+
message: chapterTitles.length > 0 && chapterTitles.every((title) => tocBullets.includes(title)) ? "TOC headings match the deterministic chapter plan." : "TOC headings do not match the deterministic chapter plan.",
|
|
456
|
+
},
|
|
380
457
|
{
|
|
381
458
|
id: "toc_present",
|
|
382
459
|
status: slides.some((slide) => slide.components.includes("toc")) ? "pass" : "blocker",
|
|
@@ -397,6 +474,16 @@ function checkPlanQuality(narrative: NarrativeStateV1, slides: SlideSpec[]): Dec
|
|
|
397
474
|
status: narrative.claims.some((claim) => claim.importance === "central" && (claim.unsupportedScope || (claim.caveats ?? []).length > 0)) ? "warning" : "pass",
|
|
398
475
|
message: narrative.claims.some((claim) => claim.importance === "central" && (claim.unsupportedScope || (claim.caveats ?? []).length > 0)) ? "Central claim boundaries are visible and should remain explicit in the rendered artifact." : "No unsupported central claim boundaries were found.",
|
|
399
476
|
},
|
|
477
|
+
{
|
|
478
|
+
id: "evidence_required_claims_have_evidence_or_visible_gap",
|
|
479
|
+
status: invisibleEvidenceGaps.length === 0 ? evidenceRequiredWithoutBindings.length > 0 ? "warning" : "pass" : "blocker",
|
|
480
|
+
message: invisibleEvidenceGaps.length > 0 ? `Evidence-required claims missing from planned slides: ${invisibleEvidenceGaps.map((claim) => claim.id).join(", ")}` : evidenceRequiredWithoutBindings.length > 0 ? `Evidence gaps remain visible for claims: ${evidenceRequiredWithoutBindings.map((claim) => claim.id).join(", ")}` : "Every evidence-required claim has at least one evidence binding.",
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
id: "risk_or_objection_visible",
|
|
484
|
+
status: risksOrObjectionsVisible ? "pass" : "warning",
|
|
485
|
+
message: risksOrObjectionsVisible ? "Risks and objections are visible when present." : "Narrative risks or objections exist but no risk/objection slide is planned.",
|
|
486
|
+
},
|
|
400
487
|
{
|
|
401
488
|
id: "simplified_design_grammar",
|
|
402
489
|
status: incompatibleComponents.length === 0 ? "pass" : "blocker",
|