@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,63 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "fs"
|
|
2
|
+
import { dirname, isAbsolute, join, normalize, resolve } from "path"
|
|
3
|
+
import { readDecksState, writeDecksState } from "../decks-state"
|
|
4
|
+
import { compileExecutiveBrief, DEFAULT_EXECUTIVE_BRIEF_PATH } from "../narrative-state/executive-brief"
|
|
5
|
+
|
|
6
|
+
export interface BriefArgs {
|
|
7
|
+
outputPath?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ParseBriefArgsResult = { ok: true; args: BriefArgs } | { ok: false; error: string }
|
|
11
|
+
|
|
12
|
+
export function parseBriefArgs(input: string): ParseBriefArgsResult {
|
|
13
|
+
const value = input.trim()
|
|
14
|
+
if (!value) return { ok: true, args: {} }
|
|
15
|
+
if (value.startsWith("--")) return { ok: false, error: "Usage: `/revela brief [workspace-relative-output.md]`" }
|
|
16
|
+
if (!value.endsWith(".md")) return { ok: false, error: "Executive brief output must be a Markdown file ending in `.md`." }
|
|
17
|
+
if (isAbsolute(value) || value.split(/[\\/]+/).includes("..")) return { ok: false, error: "Executive brief output must be a safe workspace-relative path." }
|
|
18
|
+
return { ok: true, args: { outputPath: normalize(value).replace(/\\/g, "/") } }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function handleBrief(
|
|
22
|
+
input: { workspaceRoot: string; outputPath?: string },
|
|
23
|
+
send: (text: string) => Promise<void>,
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
const statePath = join(input.workspaceRoot, "DECKS.json")
|
|
26
|
+
if (!existsSync(statePath)) {
|
|
27
|
+
await send("No `DECKS.json` found. Run `/revela init` before rendering an executive brief.")
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const state = readDecksState(input.workspaceRoot)
|
|
32
|
+
const result = compileExecutiveBrief(state, { outputPath: input.outputPath })
|
|
33
|
+
if (!result.ok) {
|
|
34
|
+
await send(
|
|
35
|
+
`**Executive brief not rendered**\n\n${result.reason}\n\n` +
|
|
36
|
+
(result.narrativeHash ? `Narrative hash: \`${result.narrativeHash}\`\n\n` : "") +
|
|
37
|
+
"Run `/revela review` and approve the current narrative, or record an explicit render override before retrying."
|
|
38
|
+
)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const filePath = safeWorkspaceFilePath(input.workspaceRoot, result.outputPath)
|
|
43
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
44
|
+
writeFileSync(filePath, result.content, "utf-8")
|
|
45
|
+
writeDecksState(input.workspaceRoot, result.state)
|
|
46
|
+
|
|
47
|
+
await send(
|
|
48
|
+
`**Executive brief rendered**\n\n` +
|
|
49
|
+
`- Output: \`${result.outputPath}\`\n` +
|
|
50
|
+
`- Render target: \`${result.target.id}\`\n` +
|
|
51
|
+
`- Narrative hash: \`${result.narrativeHash}\`\n\n` +
|
|
52
|
+
"The brief was compiled from canonical narrative state, not from a deck summary."
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeWorkspaceFilePath(workspaceRoot: string, outputPath: string): string {
|
|
57
|
+
const relative = outputPath || DEFAULT_EXECUTIVE_BRIEF_PATH
|
|
58
|
+
if (isAbsolute(relative) || relative.split(/[\\/]+/).includes("..")) throw new Error("Executive brief output must be a safe workspace-relative path.")
|
|
59
|
+
const root = resolve(workspaceRoot)
|
|
60
|
+
const target = resolve(root, relative)
|
|
61
|
+
if (target !== root && !target.startsWith(`${root}/`)) throw new Error("Executive brief output must stay inside the workspace.")
|
|
62
|
+
return target
|
|
63
|
+
}
|
package/lib/commands/help.ts
CHANGED
|
@@ -28,6 +28,8 @@ export async function handleHelp(
|
|
|
28
28
|
`\`/revela disable\` — disable Revela mode\n` +
|
|
29
29
|
`\`/revela init\` — initialize or refresh workspace DECKS.json\n` +
|
|
30
30
|
`\`/revela review\` — review narrative readiness and approval state\n` +
|
|
31
|
+
`\`/revela narrative\` — open read-only narrative workspace map\n` +
|
|
32
|
+
`\`/revela brief [file.md]\` — render executive brief from approved narrative\n` +
|
|
31
33
|
`\`/revela deck\` — start deck handoff from approved narrative\n` +
|
|
32
34
|
`\`/revela deck --review\` — review deck/artifact readiness before writing HTML\n` +
|
|
33
35
|
`\`/revela refine\` — open unified Edit/Inspect refinement workspace\n` +
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "fs"
|
|
2
|
+
import { tmpdir } from "os"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
import { openUrl as defaultOpenUrl } from "../edit/open"
|
|
5
|
+
import { hasDecksState, readDecksState } from "../decks-state"
|
|
6
|
+
import { buildNarrativeMap, formatNarrativeMap } from "../narrative-state/map"
|
|
7
|
+
import { renderNarrativeMapHtmlWithDisplay } from "../narrative-state/map-html"
|
|
8
|
+
import { emptyDisplayModel, type NarrativeViewLanguage, type ValidatedNarrativeDisplayModel } from "../narrative-state/display"
|
|
9
|
+
|
|
10
|
+
export interface NarrativeArgs {
|
|
11
|
+
language: NarrativeViewLanguage
|
|
12
|
+
raw: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ParseNarrativeArgsResult = { ok: true; args: NarrativeArgs } | { ok: false; error: string }
|
|
16
|
+
|
|
17
|
+
export function parseNarrativeArgs(param: string): ParseNarrativeArgsResult {
|
|
18
|
+
const tokens = param.trim().split(/\s+/).filter(Boolean)
|
|
19
|
+
let language: NarrativeViewLanguage = "en"
|
|
20
|
+
let raw = false
|
|
21
|
+
const languageParts: string[] = []
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
const normalized = token.toLowerCase()
|
|
24
|
+
if (normalized === "--raw") {
|
|
25
|
+
raw = true
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (token.startsWith("--") && token.length > 2) {
|
|
29
|
+
language = normalizeLanguageRequest(token.slice(2))
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
languageParts.push(token)
|
|
33
|
+
}
|
|
34
|
+
if (languageParts.length > 0) language = normalizeLanguageRequest(languageParts.join(" "))
|
|
35
|
+
return { ok: true, args: { language, raw } }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeLanguageRequest(value: string): NarrativeViewLanguage {
|
|
39
|
+
const trimmed = value.trim()
|
|
40
|
+
const normalized = trimmed.toLowerCase()
|
|
41
|
+
if (["en", "eng", "english"].includes(normalized)) return "en"
|
|
42
|
+
if (["cn", "zh", "zh-cn", "chinese"].includes(normalized)) return "zh-CN"
|
|
43
|
+
if (["jp", "ja", "ja-jp", "japanese"].includes(normalized)) return "ja-JP"
|
|
44
|
+
return trimmed || "en"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function handleNarrative(
|
|
48
|
+
options: { workspaceRoot: string; openBrowser?: boolean; openUrl?: (url: string) => void; language?: NarrativeViewLanguage; display?: ValidatedNarrativeDisplayModel },
|
|
49
|
+
send: (text: string) => Promise<void>,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
try {
|
|
52
|
+
if (!hasDecksState(options.workspaceRoot)) {
|
|
53
|
+
await send("No `DECKS.json` found. Run `/revela init` first to initialize the narrative workspace.")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const state = readDecksState(options.workspaceRoot)
|
|
58
|
+
const map = buildNarrativeMap(state)
|
|
59
|
+
const markdown = formatNarrativeMap(map)
|
|
60
|
+
|
|
61
|
+
if (options.openBrowser) {
|
|
62
|
+
const htmlPath = writeNarrativeMapHtml(map, options.display ?? emptyDisplayModel(options.language ?? "en"))
|
|
63
|
+
const url = `file://${htmlPath}`
|
|
64
|
+
try {
|
|
65
|
+
;(options.openUrl ?? defaultOpenUrl)(url)
|
|
66
|
+
await send(`Opened read-only narrative workspace: ${url}\n\n${markdown}`)
|
|
67
|
+
} catch (e: any) {
|
|
68
|
+
await send(`Read-only narrative workspace generated but could not open automatically: ${url}\n\n${e.message || String(e)}\n\n${markdown}`)
|
|
69
|
+
}
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await send(markdown)
|
|
74
|
+
} catch (e: any) {
|
|
75
|
+
await send(`**Narrative map failed:** ${e.message || String(e)}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildNarrativeViewPrompt(options: { workspaceRoot: string; language: NarrativeViewLanguage }): string {
|
|
80
|
+
if (!hasDecksState(options.workspaceRoot)) {
|
|
81
|
+
return "No `DECKS.json` found. Tell the user to run `/revela init` before opening the narrative view. Do not call any tool."
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const map = buildNarrativeMap(readDecksState(options.workspaceRoot))
|
|
85
|
+
const projection = {
|
|
86
|
+
narrativeHash: map.snapshot.narrativeHash,
|
|
87
|
+
language: options.language,
|
|
88
|
+
snapshot: map.snapshot,
|
|
89
|
+
claims: map.claimFlow.map((claim) => ({
|
|
90
|
+
id: claim.id,
|
|
91
|
+
kind: claim.kind,
|
|
92
|
+
importance: claim.importance,
|
|
93
|
+
evidenceStatus: claim.evidenceStatus,
|
|
94
|
+
text: claim.text,
|
|
95
|
+
supportedScope: claim.supportedScope,
|
|
96
|
+
unsupportedScope: claim.unsupportedScope,
|
|
97
|
+
evidence: claim.evidence.map((evidence) => ({ source: evidence.source, strength: evidence.strength, findingsFile: evidence.findingsFile, location: evidence.location, quote: evidence.quote, caveat: evidence.caveat, unsupportedScope: evidence.unsupportedScope })),
|
|
98
|
+
})),
|
|
99
|
+
relations: map.claimRelations.map((relation) => ({ id: relation.id, fromClaimId: relation.fromClaimId, toClaimId: relation.toClaimId, relation: relation.relation, rationale: relation.rationale, inferred: relation.inferred })),
|
|
100
|
+
researchGaps: map.researchGaps.map((gap) => ({ id: gap.id, targetType: gap.targetType, targetId: gap.targetId, status: gap.status, priority: gap.priority, question: gap.question })),
|
|
101
|
+
artifactCoverage: map.artifactCoverage.map((artifact) => ({ type: artifact.type, outputPath: artifact.outputPath, stale: artifact.stale, slideRefs: artifact.slideRefs.map((ref) => ({ claimId: ref.claimId, slideIndex: ref.slideIndex, role: ref.role, match: ref.match, location: ref.location })) })),
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return `Prepare the read-only Revela narrative UI display model.
|
|
105
|
+
|
|
106
|
+
Target language request: ${options.language}
|
|
107
|
+
- The language value is passed from the user's /revela narrative arguments. Interpret it as the desired UI/display language.
|
|
108
|
+
- Examples: --cn maps to zh-CN, --jp maps to ja-JP, while --fr, --de, --es, --ko, --Arabic, --Portuguese-BR, or a written language name should be localized normally into that requested language.
|
|
109
|
+
- Default /revela narrative language is en when the user provides no language request.
|
|
110
|
+
|
|
111
|
+
You must call the \`revela-narrative-view\` tool exactly once.
|
|
112
|
+
|
|
113
|
+
Hard rules:
|
|
114
|
+
- Do not mutate DECKS.json, deck HTML, evidence, claims, relations, approvals, or artifacts.
|
|
115
|
+
- Do not invent new claims, evidence, relations, slide coverage, source paths, findings files, quotes, or caveats.
|
|
116
|
+
- Preserve every claimId exactly.
|
|
117
|
+
- Preserve every relation endpoint exactly: fromClaimId, toClaimId, relation.
|
|
118
|
+
- You may only organize and localize display copy for the UI: pageTitle, summaryLine, section labels, claim card displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
|
|
119
|
+
- For inferred relations, do not provide relation displayLabel or displayRationale; inferred relations are unconfirmed order notes, not causal/support/dependency judgments.
|
|
120
|
+
- relation displayRationale may only localize or clarify an existing canonical relation rationale. If relation.rationale is missing or the relation is inferred, do not provide displayRationale; the UI will show the missing or inferred status.
|
|
121
|
+
- Keep source paths, findings files, claim IDs, narrative hash, and numbers unchanged.
|
|
122
|
+
- Translate normal UI/display text into the target language request: pageTitle, summaryLine, labels, claim displayTitle, roleLabel, narrativeJob, evidenceSummary, riskOrGapSummary, relation displayLabel, and relation displayRationale.
|
|
123
|
+
- Do not translate claim IDs, relation endpoints, narrative hash, source paths, findings files, URLs, numbers, or quoted/source facts.
|
|
124
|
+
- Use natural business and manufacturing terminology in the target language, not word-by-word machine translation.
|
|
125
|
+
- If a fact is missing, describe it as missing instead of filling it in.
|
|
126
|
+
|
|
127
|
+
Chinese localization rules when the target language request is Chinese, zh, zh-CN, --cn, 中文, or Simplified Chinese:
|
|
128
|
+
- Use natural business/manufacturing Chinese, not word-by-word machine translation.
|
|
129
|
+
- In manufacturing, industrial AI, automation, and autonomous systems context, translate "autonomy" as "自主化", "自主能力", or "自主系统". Do not translate it as "自治".
|
|
130
|
+
- Translate "autonomous" as "自主的" / "自主化的" where appropriate, not "自治的".
|
|
131
|
+
- Translate "architectural" as "架构层面的", "架构性", or "架构问题" according to context.
|
|
132
|
+
- Slug-like or kebab-case claim text such as "autonomy-is-architectural" should become a readable displayTitle such as "自主化是架构问题" or "自主化必须作为架构问题处理", not a literal token-by-token translation.
|
|
133
|
+
- If the canonical claim text is only a slug, preserve the claimId exactly but write displayTitle as a readable claim title.
|
|
134
|
+
|
|
135
|
+
Call \`revela-narrative-view\` with:
|
|
136
|
+
- language: ${options.language}
|
|
137
|
+
- narrativeHash: ${map.snapshot.narrativeHash}
|
|
138
|
+
- displayModel.version: 1
|
|
139
|
+
- displayModel.language: ${options.language}
|
|
140
|
+
- displayModel.claimCards only for claim IDs listed below
|
|
141
|
+
- displayModel.relations only for relations listed below
|
|
142
|
+
|
|
143
|
+
Compact deterministic narrative map:
|
|
144
|
+
|
|
145
|
+
\`\`\`json
|
|
146
|
+
${JSON.stringify(projection, null, 2)}
|
|
147
|
+
\`\`\``
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function writeNarrativeMapHtml(map: ReturnType<typeof buildNarrativeMap>, display: ValidatedNarrativeDisplayModel = emptyDisplayModel("en")): string {
|
|
151
|
+
const dir = join(tmpdir(), "revela-narrative")
|
|
152
|
+
mkdirSync(dir, { recursive: true })
|
|
153
|
+
const file = join(dir, `${safeFilePart(map.snapshot.narrativeId)}-${map.snapshot.narrativeHash}.html`)
|
|
154
|
+
writeFileSync(file, renderNarrativeMapHtmlWithDisplay(map, display), "utf-8")
|
|
155
|
+
return file
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function safeFilePart(value: string): string {
|
|
159
|
+
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "narrative"
|
|
160
|
+
}
|
package/lib/decks-state.ts
CHANGED
|
@@ -26,6 +26,7 @@ export type DeckProductionStatus = "planning" | "blocked" | "ready" | "written"
|
|
|
26
26
|
export type SlideProductionStatus = "planned" | "ready" | "written" | "qa_passed" | "qa_failed"
|
|
27
27
|
export type WriteReadinessStatus = "blocked" | "ready" | "written"
|
|
28
28
|
export type NarrativeRole = "context" | "tension" | "evidence" | "recommendation" | "risk" | "ask" | "appendix" | "close"
|
|
29
|
+
export type SlideClaimRefRole = "primary" | "supporting" | "evidence" | "risk" | "objection"
|
|
29
30
|
|
|
30
31
|
export interface DecksState {
|
|
31
32
|
version: 1
|
|
@@ -135,6 +136,9 @@ export interface SlideSpec {
|
|
|
135
136
|
layout: string
|
|
136
137
|
qa?: boolean
|
|
137
138
|
components: string[]
|
|
139
|
+
claimIds?: string[]
|
|
140
|
+
claimRefs?: SlideClaimRef[]
|
|
141
|
+
evidenceBindingIds?: string[]
|
|
138
142
|
content: {
|
|
139
143
|
headline?: string
|
|
140
144
|
body?: string[]
|
|
@@ -148,6 +152,12 @@ export interface SlideSpec {
|
|
|
148
152
|
notes?: string
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
export interface SlideClaimRef {
|
|
156
|
+
claimId: string
|
|
157
|
+
role: SlideClaimRefRole
|
|
158
|
+
note?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
151
161
|
export interface EvidenceRef {
|
|
152
162
|
source: string
|
|
153
163
|
quote?: string
|
|
@@ -1478,6 +1488,9 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
|
|
|
1478
1488
|
title: slide.title ?? "",
|
|
1479
1489
|
layout: slide.layout ?? "",
|
|
1480
1490
|
components: slide.components ?? [],
|
|
1491
|
+
claimIds: normalizeTextList(slide.claimIds),
|
|
1492
|
+
claimRefs: normalizeSlideClaimRefs(slide.claimRefs),
|
|
1493
|
+
evidenceBindingIds: normalizeTextList(slide.evidenceBindingIds),
|
|
1481
1494
|
content: slide.content ?? {},
|
|
1482
1495
|
evidence: slide.evidence ?? [],
|
|
1483
1496
|
status: slide.status ?? "planned",
|
|
@@ -1485,6 +1498,26 @@ function normalizeSlides(slides: SlideSpec[]): SlideSpec[] {
|
|
|
1485
1498
|
.sort((a, b) => a.index - b.index)
|
|
1486
1499
|
}
|
|
1487
1500
|
|
|
1501
|
+
function normalizeSlideClaimRefs(refs: SlideClaimRef[] | undefined): SlideClaimRef[] {
|
|
1502
|
+
const seen = new Set<string>()
|
|
1503
|
+
const out: SlideClaimRef[] = []
|
|
1504
|
+
for (const ref of refs ?? []) {
|
|
1505
|
+
const claimId = cleanOptionalText(ref.claimId)
|
|
1506
|
+
if (!claimId) continue
|
|
1507
|
+
const role = isSlideClaimRefRole(ref.role) ? ref.role : "supporting"
|
|
1508
|
+
const key = `${claimId}:${role}`
|
|
1509
|
+
if (seen.has(key)) continue
|
|
1510
|
+
seen.add(key)
|
|
1511
|
+
const note = cleanOptionalText(ref.note)
|
|
1512
|
+
out.push({ claimId, role, ...(note ? { note } : {}) })
|
|
1513
|
+
}
|
|
1514
|
+
return out
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function isSlideClaimRefRole(value: string | undefined): value is SlideClaimRefRole {
|
|
1518
|
+
return value === "primary" || value === "supporting" || value === "evidence" || value === "risk" || value === "objection"
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1488
1521
|
function normalizeNarrativeBrief(brief: NarrativeBrief | undefined): NarrativeBrief | undefined {
|
|
1489
1522
|
if (!brief) return undefined
|
|
1490
1523
|
const normalized: NarrativeBrief = {
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -71,6 +71,9 @@ Instructions:
|
|
|
71
71
|
- Make the smallest targeted change that satisfies the user's comment.
|
|
72
72
|
- If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
|
|
73
73
|
- Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
|
|
74
|
+
- 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`"} action ${"`upsertNarrative`"}, then reviewed/approved or explicitly overridden before updating the deck projection.
|
|
75
|
+
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, or deck HTML contract fixes may remain an artifact-level edit.
|
|
76
|
+
- If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
|
|
74
77
|
- Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
|
|
75
78
|
- Do not rewrite unrelated slides or broad sections of the deck.
|
|
76
79
|
- Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DeckSpec, DecksState, EvidenceRef, NarrativeBrief, NarrativeRole, SlideSpec, SourceMaterial } from "../decks-state"
|
|
2
|
+
import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "../narrative-state/types"
|
|
2
3
|
|
|
3
|
-
export type InspectionClaimOrigin = "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
|
+
export type InspectionClaimOrigin = "narrative" | "title" | "headline" | "body" | "bullet" | "purpose"
|
|
4
5
|
export type InspectionGapType = "missing_evidence" | "weak_evidence"
|
|
5
6
|
export type InspectionEvidenceSupport = "supported" | "weak" | "unknown"
|
|
6
7
|
|
|
@@ -13,6 +14,7 @@ export interface InspectionContext {
|
|
|
13
14
|
outputPath: string
|
|
14
15
|
narrativeBrief?: NarrativeBrief
|
|
15
16
|
sourceMaterials: InspectionSourceMaterial[]
|
|
17
|
+
narrative?: InspectionNarrativeStateContext
|
|
16
18
|
slides: InspectionSlideContext[]
|
|
17
19
|
gaps: InspectionGap[]
|
|
18
20
|
appendixCandidates: InspectionAppendixCandidate[]
|
|
@@ -20,6 +22,12 @@ export interface InspectionContext {
|
|
|
20
22
|
riskContext: InspectionNarrativeContext[]
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
export interface InspectionNarrativeStateContext {
|
|
26
|
+
id: string
|
|
27
|
+
status: string
|
|
28
|
+
claimCount: number
|
|
29
|
+
}
|
|
30
|
+
|
|
23
31
|
export interface InspectionSourceMaterial extends SourceMaterial {
|
|
24
32
|
linkedEvidenceCount: number
|
|
25
33
|
}
|
|
@@ -46,6 +54,7 @@ export interface InspectionSlideText {
|
|
|
46
54
|
|
|
47
55
|
export interface InspectionClaimCandidate {
|
|
48
56
|
id: string
|
|
57
|
+
canonicalClaimId?: string
|
|
49
58
|
slideIndex: number
|
|
50
59
|
slideTitle: string
|
|
51
60
|
origin: InspectionClaimOrigin
|
|
@@ -54,9 +63,18 @@ export interface InspectionClaimCandidate {
|
|
|
54
63
|
evidenceSupport: InspectionEvidenceSupport
|
|
55
64
|
evidence: InspectionEvidenceTrace[]
|
|
56
65
|
gaps: InspectionGap[]
|
|
66
|
+
evidenceBindingIds: string[]
|
|
67
|
+
supportedScope?: string
|
|
68
|
+
unsupportedScope?: string
|
|
69
|
+
caveats: string[]
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
export interface InspectionEvidenceTrace extends EvidenceRef {
|
|
73
|
+
evidenceBindingId?: string
|
|
74
|
+
claimId?: string
|
|
75
|
+
supportScope?: string
|
|
76
|
+
unsupportedScope?: string
|
|
77
|
+
strength?: NarrativeEvidenceBinding["strength"]
|
|
60
78
|
slideIndex: number
|
|
61
79
|
slideTitle: string
|
|
62
80
|
hasDetail: boolean
|
|
@@ -87,12 +105,13 @@ export interface InspectionNarrativeContext {
|
|
|
87
105
|
|
|
88
106
|
export function compileInspectionContext(state: DecksState, slug?: string): InspectionContext {
|
|
89
107
|
const deck = activeDeck(state, slug)
|
|
108
|
+
const narrative = state.narrative
|
|
90
109
|
const evidence = collectEvidence(deck)
|
|
91
110
|
const sourceMaterials = compileSourceMaterials(state.workspace.sourceMaterials ?? [], evidence)
|
|
92
111
|
const slides = deck.slides
|
|
93
112
|
.slice()
|
|
94
113
|
.sort((a, b) => a.index - b.index)
|
|
95
|
-
.map((slide) => compileSlide(slide))
|
|
114
|
+
.map((slide) => compileSlide(slide, narrative))
|
|
96
115
|
const gaps = slides.flatMap((slide) => slide.claims.flatMap((claim) => claim.gaps))
|
|
97
116
|
|
|
98
117
|
return {
|
|
@@ -103,6 +122,7 @@ export function compileInspectionContext(state: DecksState, slug?: string): Insp
|
|
|
103
122
|
language: deck.language,
|
|
104
123
|
outputPath: deck.outputPath,
|
|
105
124
|
narrativeBrief: deck.narrativeBrief,
|
|
125
|
+
narrative: narrative ? { id: narrative.id, status: narrative.status, claimCount: narrative.claims.length } : undefined,
|
|
106
126
|
sourceMaterials,
|
|
107
127
|
slides,
|
|
108
128
|
gaps,
|
|
@@ -118,9 +138,15 @@ function activeDeck(state: DecksState, slug?: string): DeckSpec {
|
|
|
118
138
|
return state.decks[key]
|
|
119
139
|
}
|
|
120
140
|
|
|
121
|
-
function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
141
|
+
function compileSlide(slide: SlideSpec, narrative: NarrativeStateV1 | undefined): InspectionSlideContext {
|
|
122
142
|
const evidence = slide.evidence.map((item) => compileEvidence(slide, item))
|
|
123
|
-
const
|
|
143
|
+
const canonicalClaims = narrative ? canonicalClaimCandidates(slide, narrative, evidence) : []
|
|
144
|
+
const canonicalText = new Set(canonicalClaims.map((claim) => normalizeText(claim.text)))
|
|
145
|
+
const heuristicClaims = claimCandidates(slide)
|
|
146
|
+
.filter((claim) => !canonicalText.has(normalizeText(claim.text)))
|
|
147
|
+
.map((claim, position) => compileClaim(slide, claim, position, evidence))
|
|
148
|
+
const claims = [...canonicalClaims, ...heuristicClaims]
|
|
149
|
+
const claimCaveats = canonicalClaims.flatMap((claim) => claim.caveats)
|
|
124
150
|
return {
|
|
125
151
|
index: slide.index,
|
|
126
152
|
title: slide.title,
|
|
@@ -136,7 +162,61 @@ function compileSlide(slide: SlideSpec): InspectionSlideContext {
|
|
|
136
162
|
},
|
|
137
163
|
claims,
|
|
138
164
|
evidence,
|
|
139
|
-
caveats:
|
|
165
|
+
caveats: dedupeText([
|
|
166
|
+
...evidence.map((item) => item.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
167
|
+
...claimCaveats,
|
|
168
|
+
]),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function canonicalClaimCandidates(slide: SlideSpec, narrative: NarrativeStateV1, slideEvidence: InspectionEvidenceTrace[]): InspectionClaimCandidate[] {
|
|
173
|
+
const claimRefs = slide.claimRefs ?? []
|
|
174
|
+
const metadataClaimIds = new Set([
|
|
175
|
+
...claimRefs.map((ref) => ref.claimId),
|
|
176
|
+
...(slide.claimIds ?? []),
|
|
177
|
+
].filter(Boolean))
|
|
178
|
+
const evidenceBindingIds = new Set(slide.evidenceBindingIds ?? [])
|
|
179
|
+
for (const binding of narrative.evidenceBindings) {
|
|
180
|
+
if (evidenceBindingIds.has(binding.id)) metadataClaimIds.add(binding.claimId)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return narrative.claims
|
|
184
|
+
.filter((claim) => metadataClaimIds.has(claim.id))
|
|
185
|
+
.map((claim) => compileCanonicalClaim(slide, claim, narrative.evidenceBindings, slideEvidence, evidenceBindingIds))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function compileCanonicalClaim(
|
|
189
|
+
slide: SlideSpec,
|
|
190
|
+
claim: NarrativeClaim,
|
|
191
|
+
bindings: NarrativeEvidenceBinding[],
|
|
192
|
+
slideEvidence: InspectionEvidenceTrace[],
|
|
193
|
+
slideEvidenceBindingIds: Set<string>,
|
|
194
|
+
): InspectionClaimCandidate {
|
|
195
|
+
const allClaimBindings = bindings.filter((binding) => binding.claimId === claim.id)
|
|
196
|
+
const selectedBindings = allClaimBindings.filter((binding) => slideEvidenceBindingIds.size === 0 || slideEvidenceBindingIds.has(binding.id))
|
|
197
|
+
const evidenceBindings = selectedBindings.length > 0 ? selectedBindings : allClaimBindings
|
|
198
|
+
const evidence = evidenceBindings.length > 0
|
|
199
|
+
? evidenceBindings.map((binding) => compileEvidenceBinding(slide, binding))
|
|
200
|
+
: slideEvidence
|
|
201
|
+
const gaps = canonicalClaimGaps(slide, claim, evidence)
|
|
202
|
+
return {
|
|
203
|
+
id: claim.id,
|
|
204
|
+
canonicalClaimId: claim.id,
|
|
205
|
+
slideIndex: slide.index,
|
|
206
|
+
slideTitle: slide.title,
|
|
207
|
+
origin: "narrative",
|
|
208
|
+
text: claim.text,
|
|
209
|
+
evidenceSensitive: claim.evidenceRequired || isEvidenceSensitiveClaim(claim.text),
|
|
210
|
+
evidenceSupport: narrativeEvidenceSupport(claim, evidence),
|
|
211
|
+
evidence,
|
|
212
|
+
gaps,
|
|
213
|
+
evidenceBindingIds: evidenceBindings.map((binding) => binding.id),
|
|
214
|
+
supportedScope: claim.supportedScope,
|
|
215
|
+
unsupportedScope: claim.unsupportedScope,
|
|
216
|
+
caveats: dedupeText([
|
|
217
|
+
...(claim.caveats ?? []),
|
|
218
|
+
...evidenceBindings.map((binding) => binding.caveat).filter((item): item is string => Boolean(item?.trim())),
|
|
219
|
+
]),
|
|
140
220
|
}
|
|
141
221
|
}
|
|
142
222
|
|
|
@@ -159,7 +239,34 @@ function compileClaim(
|
|
|
159
239
|
evidenceSupport: evidenceSupport(evidence),
|
|
160
240
|
evidence,
|
|
161
241
|
gaps,
|
|
242
|
+
evidenceBindingIds: [],
|
|
243
|
+
caveats: [],
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function canonicalClaimGaps(slide: SlideSpec, claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
248
|
+
if (!claim.evidenceRequired) return []
|
|
249
|
+
if (claim.evidenceStatus === "missing" || evidence.length === 0) {
|
|
250
|
+
return [{
|
|
251
|
+
type: "missing_evidence",
|
|
252
|
+
slideIndex: slide.index,
|
|
253
|
+
slideTitle: slide.title,
|
|
254
|
+
claimId: claim.id,
|
|
255
|
+
claimText: claim.text,
|
|
256
|
+
message: "Canonical narrative claim requires evidence but has no bound evidence trace.",
|
|
257
|
+
}]
|
|
162
258
|
}
|
|
259
|
+
if (claim.evidenceStatus === "weak" || evidence.some((item) => !item.hasDetail)) {
|
|
260
|
+
return [{
|
|
261
|
+
type: "weak_evidence",
|
|
262
|
+
slideIndex: slide.index,
|
|
263
|
+
slideTitle: slide.title,
|
|
264
|
+
claimId: claim.id,
|
|
265
|
+
claimText: claim.text,
|
|
266
|
+
message: "Canonical narrative claim has weak or source-only evidence trace.",
|
|
267
|
+
}]
|
|
268
|
+
}
|
|
269
|
+
return []
|
|
163
270
|
}
|
|
164
271
|
|
|
165
272
|
function claimGaps(slide: SlideSpec, claimId: string, claimText: string, evidence: InspectionEvidenceTrace[]): InspectionGap[] {
|
|
@@ -192,6 +299,12 @@ function evidenceSupport(evidence: InspectionEvidenceTrace[]): InspectionEvidenc
|
|
|
192
299
|
return "supported"
|
|
193
300
|
}
|
|
194
301
|
|
|
302
|
+
function narrativeEvidenceSupport(claim: NarrativeClaim, evidence: InspectionEvidenceTrace[]): InspectionEvidenceSupport {
|
|
303
|
+
if (claim.evidenceStatus === "supported" || claim.evidenceStatus === "not_required") return "supported"
|
|
304
|
+
if (claim.evidenceStatus === "partial" || claim.evidenceStatus === "weak") return "weak"
|
|
305
|
+
return evidenceSupport(evidence)
|
|
306
|
+
}
|
|
307
|
+
|
|
195
308
|
function claimCandidates(slide: SlideSpec): Array<{ origin: InspectionClaimOrigin; text: string }> {
|
|
196
309
|
const claims: Array<{ origin: InspectionClaimOrigin; text: string }> = []
|
|
197
310
|
pushClaim(claims, "title", slide.title)
|
|
@@ -218,6 +331,29 @@ function compileEvidence(slide: SlideSpec, evidence: EvidenceRef): InspectionEvi
|
|
|
218
331
|
}
|
|
219
332
|
}
|
|
220
333
|
|
|
334
|
+
function compileEvidenceBinding(slide: SlideSpec, binding: NarrativeEvidenceBinding): InspectionEvidenceTrace {
|
|
335
|
+
const evidence: EvidenceRef = {
|
|
336
|
+
source: binding.source,
|
|
337
|
+
sourcePath: binding.sourcePath,
|
|
338
|
+
findingsFile: binding.findingsFile,
|
|
339
|
+
quote: binding.quote,
|
|
340
|
+
location: binding.location,
|
|
341
|
+
url: binding.url,
|
|
342
|
+
caveat: binding.caveat,
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
...evidence,
|
|
346
|
+
evidenceBindingId: binding.id,
|
|
347
|
+
claimId: binding.claimId,
|
|
348
|
+
supportScope: binding.supportScope,
|
|
349
|
+
unsupportedScope: binding.unsupportedScope,
|
|
350
|
+
strength: binding.strength,
|
|
351
|
+
slideIndex: slide.index,
|
|
352
|
+
slideTitle: slide.title,
|
|
353
|
+
hasDetail: hasEvidenceDetail(evidence),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
221
357
|
function collectEvidence(deck: DeckSpec): InspectionEvidenceTrace[] {
|
|
222
358
|
return deck.slides.flatMap((slide) => slide.evidence.map((item) => compileEvidence(slide, item)))
|
|
223
359
|
}
|
|
@@ -303,6 +439,24 @@ function cleanOptionalText(value: string | undefined): string | undefined {
|
|
|
303
439
|
return text || undefined
|
|
304
440
|
}
|
|
305
441
|
|
|
442
|
+
function dedupeText(values: string[]): string[] {
|
|
443
|
+
const seen = new Set<string>()
|
|
444
|
+
const result: string[] = []
|
|
445
|
+
for (const value of values) {
|
|
446
|
+
const cleaned = cleanOptionalText(value)
|
|
447
|
+
if (!cleaned) continue
|
|
448
|
+
const key = normalizeText(cleaned)
|
|
449
|
+
if (seen.has(key)) continue
|
|
450
|
+
seen.add(key)
|
|
451
|
+
result.push(cleaned)
|
|
452
|
+
}
|
|
453
|
+
return result
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function normalizeText(value: string | undefined): string {
|
|
457
|
+
return value?.replace(/\s+/g, " ").trim().toLowerCase() ?? ""
|
|
458
|
+
}
|
|
459
|
+
|
|
306
460
|
const EVIDENCE_SENSITIVE_TERMS = [
|
|
307
461
|
/\bmarket size\b/,
|
|
308
462
|
/\bcagr\b/,
|
|
@@ -50,10 +50,15 @@ export interface InspectionProjectionMatch {
|
|
|
50
50
|
}
|
|
51
51
|
claim?: {
|
|
52
52
|
id: string
|
|
53
|
+
canonicalClaimId?: string
|
|
53
54
|
origin: string
|
|
54
55
|
text: string
|
|
55
56
|
evidenceSensitive: boolean
|
|
56
57
|
evidenceSupport: string
|
|
58
|
+
evidenceBindingIds: string[]
|
|
59
|
+
supportedScope?: string
|
|
60
|
+
unsupportedScope?: string
|
|
61
|
+
caveats: string[]
|
|
57
62
|
}
|
|
58
63
|
}
|
|
59
64
|
|
|
@@ -97,6 +102,8 @@ export interface InspectionAppendixProjection {
|
|
|
97
102
|
|
|
98
103
|
export interface InspectionEvidenceProjectionTrace {
|
|
99
104
|
source: string
|
|
105
|
+
evidenceBindingId?: string
|
|
106
|
+
claimId?: string
|
|
100
107
|
sourcePath?: string
|
|
101
108
|
findingsFile?: string
|
|
102
109
|
location?: string
|
|
@@ -104,6 +111,9 @@ export interface InspectionEvidenceProjectionTrace {
|
|
|
104
111
|
url?: string
|
|
105
112
|
quote?: string
|
|
106
113
|
caveat?: string
|
|
114
|
+
supportScope?: string
|
|
115
|
+
unsupportedScope?: string
|
|
116
|
+
strength?: string
|
|
107
117
|
extractedTextPath?: string
|
|
108
118
|
extractedManifestPath?: string
|
|
109
119
|
hasDetail: boolean
|
|
@@ -163,10 +173,15 @@ export function projectInspectionMatch(
|
|
|
163
173
|
claim: claim
|
|
164
174
|
? {
|
|
165
175
|
id: claim.id,
|
|
176
|
+
canonicalClaimId: claim.canonicalClaimId,
|
|
166
177
|
origin: claim.origin,
|
|
167
178
|
text: truncate(claim.text, 500),
|
|
168
179
|
evidenceSensitive: claim.evidenceSensitive,
|
|
169
180
|
evidenceSupport: claim.evidenceSupport,
|
|
181
|
+
evidenceBindingIds: claim.evidenceBindingIds,
|
|
182
|
+
supportedScope: truncateOptional(claim.supportedScope, 280),
|
|
183
|
+
unsupportedScope: truncateOptional(claim.unsupportedScope, 280),
|
|
184
|
+
caveats: claim.caveats.map((item) => truncate(item, 280)).slice(0, 8),
|
|
170
185
|
}
|
|
171
186
|
: undefined,
|
|
172
187
|
},
|
|
@@ -211,6 +226,8 @@ export function projectInspectionMatch(
|
|
|
211
226
|
function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenceProjectionTrace {
|
|
212
227
|
return {
|
|
213
228
|
source: truncate(trace.source, 180),
|
|
229
|
+
evidenceBindingId: truncateOptional(trace.evidenceBindingId, 160),
|
|
230
|
+
claimId: truncateOptional(trace.claimId, 160),
|
|
214
231
|
sourcePath: truncateOptional(trace.sourcePath, 220),
|
|
215
232
|
findingsFile: truncateOptional(trace.findingsFile, 220),
|
|
216
233
|
location: truncateOptional(trace.location, 120),
|
|
@@ -218,6 +235,9 @@ function projectEvidenceTrace(trace: InspectionEvidenceTrace): InspectionEvidenc
|
|
|
218
235
|
url: truncateOptional(trace.url, 240),
|
|
219
236
|
quote: truncateOptional(trace.quote, 500),
|
|
220
237
|
caveat: truncateOptional(trace.caveat, 280),
|
|
238
|
+
supportScope: truncateOptional(trace.supportScope, 280),
|
|
239
|
+
unsupportedScope: truncateOptional(trace.unsupportedScope, 280),
|
|
240
|
+
strength: trace.strength,
|
|
221
241
|
extractedTextPath: truncateOptional(trace.extractedTextPath, 220),
|
|
222
242
|
extractedManifestPath: truncateOptional(trace.extractedManifestPath, 220),
|
|
223
243
|
hasDetail: trace.hasDetail,
|