@cyber-dash-tech/revela 0.11.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.
Files changed (37) hide show
  1. package/README.md +35 -29
  2. package/README.zh-CN.md +35 -29
  3. package/lib/commands/brief.ts +63 -0
  4. package/lib/commands/designs.ts +2 -2
  5. package/lib/commands/domains.ts +2 -2
  6. package/lib/commands/enable.ts +19 -19
  7. package/lib/commands/help.ts +7 -3
  8. package/lib/commands/init.ts +30 -19
  9. package/lib/commands/narrative.ts +160 -0
  10. package/lib/commands/review.ts +115 -1
  11. package/lib/decks-state.ts +46 -3
  12. package/lib/edit/prompt.ts +3 -0
  13. package/lib/inspection-context/compile.ts +159 -5
  14. package/lib/inspection-context/project.ts +20 -0
  15. package/lib/narrative-state/coverage.ts +100 -0
  16. package/lib/narrative-state/display.ts +219 -0
  17. package/lib/narrative-state/executive-brief.ts +246 -0
  18. package/lib/narrative-state/hash.ts +61 -0
  19. package/lib/narrative-state/map-html.ts +348 -0
  20. package/lib/narrative-state/map.ts +282 -0
  21. package/lib/narrative-state/normalize.ts +361 -0
  22. package/lib/narrative-state/project-compat.ts +14 -0
  23. package/lib/narrative-state/queries.ts +433 -0
  24. package/lib/narrative-state/readiness.ts +359 -0
  25. package/lib/narrative-state/render-plan.ts +250 -0
  26. package/lib/narrative-state/research-gaps.ts +191 -0
  27. package/lib/narrative-state/types.ts +172 -0
  28. package/lib/prompt-builder.ts +59 -26
  29. package/lib/workspace-state/evidence-status.ts +21 -1
  30. package/lib/workspace-state/graph.ts +174 -2
  31. package/lib/workspace-state/types.ts +13 -1
  32. package/package.json +1 -1
  33. package/plugin.ts +58 -2
  34. package/skill/NARRATIVE_SKILL.md +64 -0
  35. package/tools/decks.ts +265 -2
  36. package/tools/narrative-view.ts +84 -0
  37. package/tools/workspace-scan.ts +14 -1
@@ -0,0 +1,191 @@
1
+ import type { DecksState } from "../decks-state"
2
+ import { recordWorkspaceAction } from "../workspace-state/actions"
3
+ import { stableResearchGapId } from "./hash"
4
+ import { normalizeNarrativeState } from "./normalize"
5
+ import { reviewNarrativeState } from "./readiness"
6
+ import type {
7
+ NarrativeReadinessIssue,
8
+ NarrativeResearchGap,
9
+ NarrativeResearchGapStatus,
10
+ NarrativeResearchGapTargetType,
11
+ NarrativeStateV1,
12
+ } from "./types"
13
+
14
+ export interface UpsertResearchGapInput {
15
+ id?: string
16
+ targetType?: NarrativeResearchGapTargetType
17
+ targetId?: string
18
+ question: string
19
+ status?: NarrativeResearchGapStatus
20
+ priority?: "high" | "medium" | "low"
21
+ findingsFile?: string
22
+ evidenceBindingIds?: string[]
23
+ createdFromIssueType?: NarrativeReadinessIssue["type"]
24
+ notes?: string
25
+ }
26
+
27
+ export interface UpdateResearchGapInput {
28
+ id: string
29
+ status?: NarrativeResearchGapStatus
30
+ findingsFile?: string
31
+ evidenceBindingIds?: string[]
32
+ notes?: string
33
+ }
34
+
35
+ export interface ResearchGapMutationResult {
36
+ created: NarrativeResearchGap[]
37
+ updated: NarrativeResearchGap[]
38
+ skipped: Array<{ id?: string; question?: string; reason: string }>
39
+ gaps: NarrativeResearchGap[]
40
+ }
41
+
42
+ export interface CloseResearchGapResult {
43
+ closed: boolean
44
+ skipped: boolean
45
+ reason?: string
46
+ gap?: NarrativeResearchGap
47
+ }
48
+
49
+ export function deriveResearchGapsFromReadiness(state: DecksState, options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
50
+ const reviewed = reviewNarrativeState(state, { now: options.now })
51
+ return upsertResearchGapsInState(reviewed.state, gapsFromIssues(reviewed.state.narrative!, reviewed.result.issues), options)
52
+ }
53
+
54
+ export function upsertResearchGapsInState(state: DecksState, inputs: UpsertResearchGapInput[], options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
55
+ const now = options.now ?? new Date().toISOString()
56
+ const narrative = ensureNarrative(state)
57
+ const existing = new Map((narrative.researchGaps ?? []).map((gap) => [gap.id, gap]))
58
+ const created: NarrativeResearchGap[] = []
59
+ const updated: NarrativeResearchGap[] = []
60
+ const skipped: ResearchGapMutationResult["skipped"] = []
61
+
62
+ for (const input of inputs) {
63
+ const question = clean(input.question)
64
+ if (!question) {
65
+ skipped.push({ reason: "question is required" })
66
+ continue
67
+ }
68
+ const targetType = input.targetType ?? "narrative"
69
+ const targetId = clean(input.targetId)
70
+ const id = input.id?.trim() || stableResearchGapId([targetType, targetId, question].filter(Boolean).join("|"))
71
+ const prior = existing.get(id)
72
+ if (prior?.status === "closed") {
73
+ skipped.push({ id, question, reason: "matching research gap is already closed" })
74
+ continue
75
+ }
76
+
77
+ const next: NarrativeResearchGap = {
78
+ id,
79
+ targetType,
80
+ targetId,
81
+ question,
82
+ status: input.status ?? prior?.status ?? "open",
83
+ priority: input.priority ?? prior?.priority ?? "medium",
84
+ findingsFile: clean(input.findingsFile) || prior?.findingsFile,
85
+ evidenceBindingIds: mergeIds(prior?.evidenceBindingIds, input.evidenceBindingIds),
86
+ createdFromIssueType: input.createdFromIssueType ?? prior?.createdFromIssueType,
87
+ notes: clean(input.notes) || prior?.notes,
88
+ createdAt: prior?.createdAt ?? now,
89
+ updatedAt: now,
90
+ closedAt: input.status === "closed" ? now : prior?.closedAt,
91
+ }
92
+ existing.set(id, next)
93
+ if (prior) updated.push(next)
94
+ else created.push(next)
95
+ }
96
+
97
+ const gaps = [...existing.values()].sort((a, b) => gapSortValue(a) - gapSortValue(b) || a.question.localeCompare(b.question))
98
+ state.narrative = { ...narrative, researchGaps: gaps, updatedAt: now }
99
+ if (created.length > 0) recordResearchGapAction(state, "research.gap_created", created, now)
100
+ if (updated.length > 0) recordResearchGapAction(state, "research.gap_updated", updated, now)
101
+ return { state, result: { created, updated, skipped, gaps } }
102
+ }
103
+
104
+ export function updateResearchGapInState(state: DecksState, input: UpdateResearchGapInput, options: { now?: string } = {}): { state: DecksState; result: ResearchGapMutationResult } {
105
+ const narrative = ensureNarrative(state)
106
+ const gap = (narrative.researchGaps ?? []).find((item) => item.id === input.id)
107
+ if (!gap) return { state, result: { created: [], updated: [], skipped: [{ id: input.id, reason: "research gap not found" }], gaps: narrative.researchGaps ?? [] } }
108
+ return upsertResearchGapsInState(state, [{ ...gap, ...input, question: gap.question, targetType: gap.targetType, targetId: gap.targetId }], options)
109
+ }
110
+
111
+ export function closeResearchGapInState(state: DecksState, id: string, reason?: string, options: { now?: string } = {}): { state: DecksState; result: CloseResearchGapResult } {
112
+ const now = options.now ?? new Date().toISOString()
113
+ const narrative = ensureNarrative(state)
114
+ const gaps = narrative.researchGaps ?? []
115
+ const gap = gaps.find((item) => item.id === id)
116
+ if (!gap) return { state, result: { closed: false, skipped: true, reason: "research gap not found" } }
117
+ const closed: NarrativeResearchGap = { ...gap, status: "closed", notes: clean(reason) || gap.notes, updatedAt: now, closedAt: now }
118
+ state.narrative = { ...narrative, researchGaps: gaps.map((item) => item.id === id ? closed : item), updatedAt: now }
119
+ recordResearchGapAction(state, "research.gap_closed", [closed], now)
120
+ return { state, result: { closed: true, skipped: false, gap: closed } }
121
+ }
122
+
123
+ function gapsFromIssues(narrative: NarrativeStateV1, issues: NarrativeReadinessIssue[]): UpsertResearchGapInput[] {
124
+ return issues.flatMap((issue) => {
125
+ if (!researchableIssue(issue)) return []
126
+ const target = targetForIssue(narrative, issue)
127
+ return [{
128
+ targetType: target.type,
129
+ targetId: target.id,
130
+ question: questionForIssue(issue),
131
+ priority: issue.severity === "blocker" ? "high" : "medium",
132
+ status: "open",
133
+ createdFromIssueType: issue.type,
134
+ notes: issue.suggestedAction,
135
+ findingsFile: issue.source?.startsWith("researches/") ? issue.source : undefined,
136
+ }]
137
+ })
138
+ }
139
+
140
+ function researchableIssue(issue: NarrativeReadinessIssue): boolean {
141
+ return issue.type === "missing_evidence" || issue.type === "weak_evidence" || issue.type === "unsupported_scope" || issue.type === "unhandled_objection" || issue.type === "missing_risk"
142
+ }
143
+
144
+ function targetForIssue(narrative: NarrativeStateV1, issue: NarrativeReadinessIssue): { type: NarrativeResearchGapTargetType; id?: string } {
145
+ if (issue.claimId) return { type: "claim", id: issue.claimId }
146
+ const objection = narrative.objections.find((item) => item.text === issue.claimText)
147
+ if (objection) return { type: "objection", id: objection.id }
148
+ if (issue.type === "missing_risk") return { type: "decision", id: narrative.decision.action ? stableResearchGapId(`decision:${narrative.decision.action}`) : undefined }
149
+ return { type: "narrative", id: narrative.id }
150
+ }
151
+
152
+ function questionForIssue(issue: NarrativeReadinessIssue): string {
153
+ if (issue.claimText && issue.type === "missing_evidence") return `Find evidence for claim: ${issue.claimText}`
154
+ if (issue.claimText && issue.type === "weak_evidence") return `Strengthen evidence for claim: ${issue.claimText}`
155
+ if (issue.claimText && issue.type === "unsupported_scope") return `Resolve unsupported scope for claim: ${issue.claimText}`
156
+ if (issue.type === "unhandled_objection") return `Find response or evidence for objection: ${issue.claimText ?? issue.message}`
157
+ if (issue.type === "missing_risk") return "Identify risk, assumption, caveat, or tradeoff handling for the recommendation."
158
+ return issue.message
159
+ }
160
+
161
+ function ensureNarrative(state: DecksState): NarrativeStateV1 {
162
+ state.narrative = normalizeNarrativeState(state)
163
+ return state.narrative
164
+ }
165
+
166
+ function recordResearchGapAction(state: DecksState, type: "research.gap_created" | "research.gap_updated" | "research.gap_closed", gaps: NarrativeResearchGap[], timestamp: string): void {
167
+ recordWorkspaceAction(state, {
168
+ type,
169
+ actor: "revela-decks",
170
+ timestamp,
171
+ inputs: { narrativeId: state.narrative?.id },
172
+ outputs: { gaps: gaps.map((gap) => ({ id: gap.id, status: gap.status, targetType: gap.targetType, targetId: gap.targetId, findingsFile: gap.findingsFile, evidenceBindingIds: gap.evidenceBindingIds })) },
173
+ status: "success",
174
+ summary: `${type === "research.gap_created" ? "Created" : type === "research.gap_closed" ? "Closed" : "Updated"} ${gaps.length} research gap${gaps.length === 1 ? "" : "s"}.`,
175
+ nodeIds: gaps.map((gap) => gap.id),
176
+ })
177
+ }
178
+
179
+ function gapSortValue(gap: NarrativeResearchGap): number {
180
+ const statusValue: Record<NarrativeResearchGapStatus, number> = { open: 0, in_progress: 1, findings_saved: 2, attached: 3, evidence_bound: 4, closed: 5 }
181
+ const priorityValue = gap.priority === "high" ? 0 : gap.priority === "medium" ? 1 : 2
182
+ return statusValue[gap.status] * 10 + priorityValue
183
+ }
184
+
185
+ function mergeIds(existing: string[] | undefined, next: string[] | undefined): string[] {
186
+ return [...new Set([...(existing ?? []), ...(next ?? [])].map(clean).filter(Boolean))].sort()
187
+ }
188
+
189
+ function clean(value: string | undefined): string {
190
+ return value?.trim() ?? ""
191
+ }
@@ -0,0 +1,172 @@
1
+ export type NarrativeStatus = "draft" | "needs_research" | "needs_user_confirmation" | "ready_for_approval" | "approved"
2
+
3
+ export type NarrativeClaimKind = "context" | "problem" | "opportunity" | "evidence" | "recommendation" | "risk" | "assumption" | "ask"
4
+
5
+ export type NarrativeEvidenceStatus = "supported" | "partial" | "weak" | "missing" | "not_required"
6
+
7
+ export type NarrativeResearchGapStatus = "open" | "in_progress" | "findings_saved" | "attached" | "evidence_bound" | "closed"
8
+
9
+ export type NarrativeResearchGapTargetType = "claim" | "objection" | "risk" | "decision" | "narrative"
10
+
11
+ export type NarrativeClaimRelationType = "leads_to" | "supports" | "depends_on" | "contrasts_with" | "constrains" | "answers"
12
+
13
+ export interface NarrativeStateV1 {
14
+ version: 1
15
+ id: string
16
+ status: NarrativeStatus
17
+ audience: AudienceIntent
18
+ decision: DecisionIntent
19
+ thesis?: NarrativeThesis
20
+ claims: NarrativeClaim[]
21
+ claimRelations?: NarrativeClaimRelation[]
22
+ evidenceBindings: NarrativeEvidenceBinding[]
23
+ objections: NarrativeObjection[]
24
+ risks: NarrativeRisk[]
25
+ researchGaps?: NarrativeResearchGap[]
26
+ approvals: NarrativeApproval[]
27
+ updatedAt: string
28
+ }
29
+
30
+ export interface AudienceIntent {
31
+ primary: string
32
+ secondary?: string[]
33
+ beliefBefore: string
34
+ beliefAfter: string
35
+ decisionContext?: string
36
+ successCriteria?: string[]
37
+ }
38
+
39
+ export interface DecisionIntent {
40
+ action: string
41
+ owner?: string
42
+ deadline?: string
43
+ decisionType?: "approve" | "invest" | "prioritize" | "align" | "choose" | "understand" | "other"
44
+ consequenceOfNoDecision?: string
45
+ }
46
+
47
+ export interface NarrativeThesis {
48
+ id: string
49
+ statement: string
50
+ confidence: "high" | "medium" | "low"
51
+ caveat?: string
52
+ }
53
+
54
+ export interface NarrativeClaim {
55
+ id: string
56
+ kind: NarrativeClaimKind
57
+ text: string
58
+ importance: "central" | "supporting" | "background"
59
+ evidenceRequired: boolean
60
+ evidenceStatus: NarrativeEvidenceStatus
61
+ supportedScope?: string
62
+ unsupportedScope?: string
63
+ caveats?: string[]
64
+ }
65
+
66
+ export interface NarrativeClaimRelation {
67
+ id: string
68
+ fromClaimId: string
69
+ toClaimId: string
70
+ relation: NarrativeClaimRelationType
71
+ rationale?: string
72
+ }
73
+
74
+ export interface NarrativeEvidenceBinding {
75
+ id: string
76
+ claimId: string
77
+ source: string
78
+ sourcePath?: string
79
+ findingsFile?: string
80
+ quote?: string
81
+ location?: string
82
+ url?: string
83
+ caveat?: string
84
+ supportScope?: string
85
+ unsupportedScope?: string
86
+ strength: "strong" | "partial" | "weak"
87
+ }
88
+
89
+ export interface NarrativeObjection {
90
+ id: string
91
+ text: string
92
+ claimId?: string
93
+ priority: "high" | "medium" | "low"
94
+ response?: string
95
+ }
96
+
97
+ export interface NarrativeRisk {
98
+ id: string
99
+ text: string
100
+ claimId?: string
101
+ severity: "high" | "medium" | "low"
102
+ mitigation?: string
103
+ }
104
+
105
+ export interface NarrativeResearchGap {
106
+ id: string
107
+ targetType: NarrativeResearchGapTargetType
108
+ targetId?: string
109
+ question: string
110
+ status: NarrativeResearchGapStatus
111
+ priority: "high" | "medium" | "low"
112
+ findingsFile?: string
113
+ evidenceBindingIds?: string[]
114
+ createdFromIssueType?: NarrativeReadinessIssueType
115
+ notes?: string
116
+ createdAt: string
117
+ updatedAt: string
118
+ closedAt?: string
119
+ }
120
+
121
+ export interface NarrativeApproval {
122
+ id: string
123
+ narrativeHash: string
124
+ approvedAt: string
125
+ approvedBy: "user" | "override"
126
+ scope: "narrative" | "render_override"
127
+ note?: string
128
+ }
129
+
130
+ export type NarrativeReadinessStatus = "blocked" | "needs_research" | "needs_user_confirmation" | "ready_for_approval" | "approved"
131
+
132
+ export type NarrativeReadinessIssueType =
133
+ | "missing_audience"
134
+ | "missing_belief_shift"
135
+ | "missing_decision"
136
+ | "missing_thesis"
137
+ | "claim_chain_gap"
138
+ | "missing_evidence"
139
+ | "weak_evidence"
140
+ | "unsupported_scope"
141
+ | "unhandled_objection"
142
+ | "missing_risk"
143
+ | "approval_missing"
144
+ | "approval_stale"
145
+ | "artifact_stale"
146
+ | "research_findings_unattached"
147
+ | "research_gap_open"
148
+
149
+ export interface NarrativeReadinessIssue {
150
+ type: NarrativeReadinessIssueType
151
+ severity: "blocker" | "warning"
152
+ message: string
153
+ suggestedAction: string
154
+ claimId?: string
155
+ claimText?: string
156
+ source?: string
157
+ }
158
+
159
+ export interface NarrativeReadinessResult {
160
+ status: NarrativeReadinessStatus
161
+ narrativeHash: string
162
+ reviewedAt: string
163
+ blockers: string[]
164
+ warnings: string[]
165
+ issues: NarrativeReadinessIssue[]
166
+ approval?: {
167
+ current: boolean
168
+ stale: boolean
169
+ latest?: NarrativeApproval
170
+ }
171
+ nextActions: string[]
172
+ }
@@ -1,9 +1,14 @@
1
1
  /**
2
- * Prompt builder — assembles the three-layer system prompt.
2
+ * Prompt builder — assembles Revela system prompts.
3
3
  *
4
- * Layer 1: SKILL.md — core protocol (conversation flow, HTML rules, quality)
5
- * Layer 2: DOMAIN.md domain knowledge (report structure, terminology)
6
- * Layer 3: DESIGN.md visual style (colors, fonts, animations, layout)
4
+ * Narrative mode:
5
+ * Layer 1: NARRATIVE_SKILL.md audience/decision/claim/evidence readiness
6
+ * Layer 2: DOMAIN.md domain reasoning guidance when present
7
+ *
8
+ * Deck-render mode:
9
+ * Layer 1: SKILL.md — legacy deck render protocol (HTML rules, quality)
10
+ * Layer 2: DOMAIN.md — domain structure/terminology
11
+ * Layer 3: DESIGN.md — visual style (colors, fonts, animations, layout)
7
12
  *
8
13
  * When the active DESIGN.md has @section markers, only the global section,
9
14
  * layouts section, and a generated component index are injected into the
@@ -37,22 +42,43 @@ import { childLog } from "./log"
37
42
 
38
43
  const promptLog = childLog("prompt-builder")
39
44
 
40
- /** Path to SKILL.md shipped with this package. */
45
+ export type PromptMode = "narrative" | "deck-render"
46
+
47
+ export interface BuildPromptOptions {
48
+ mode?: PromptMode
49
+ designName?: string
50
+ domainName?: string
51
+ }
52
+
53
+ /** Path to deck-render SKILL.md shipped with this package. */
41
54
  const SKILL_MD_PATH = resolve(__dirname, "..", "skill", "SKILL.md")
55
+ const NARRATIVE_SKILL_MD_PATH = resolve(__dirname, "..", "skill", "NARRATIVE_SKILL.md")
42
56
 
43
57
  /**
44
- * Build the combined system prompt and write it to _active-prompt.md.
58
+ * Build the active system prompt and write it to _active-prompt.md.
59
+ *
60
+ * Backward-compatible call form:
61
+ * - buildPrompt() builds the default narrative prompt.
62
+ * - buildPrompt("aurora", "general") builds the default narrative prompt with active metadata overrides.
63
+ *
64
+ * New call form:
65
+ * - buildPrompt({ mode: "narrative" }) avoids design/HTML instructions.
66
+ * - buildPrompt({ mode: "deck-render" }) preserves the legacy deck render prompt.
45
67
  *
46
- * @param designName - Override design (defaults to active)
47
- * @param domainName - Override domain (defaults to active)
48
68
  * @returns The path to the written file.
49
69
  */
50
- export function buildPrompt(designName?: string, domainName?: string): string {
51
- const design = designName || activeDesign()
52
- const domain = domainName || activeDomain()
53
-
54
- // Layer 1 — SKILL.md
55
- const coreSkill = readFileSync(SKILL_MD_PATH, "utf-8")
70
+ export function buildPrompt(options?: BuildPromptOptions): string
71
+ export function buildPrompt(designName?: string, domainName?: string): string
72
+ export function buildPrompt(optionsOrDesignName?: BuildPromptOptions | string, legacyDomainName?: string): string {
73
+ const options = typeof optionsOrDesignName === "object" && optionsOrDesignName !== null
74
+ ? optionsOrDesignName
75
+ : { designName: optionsOrDesignName, domainName: legacyDomainName }
76
+ const mode: PromptMode = options.mode || "narrative"
77
+ const design = options.designName || activeDesign()
78
+ const domain = options.domainName || activeDomain()
79
+
80
+ // Layer 1 — core skill for the selected prompt mode.
81
+ const coreSkill = readFileSync(mode === "deck-render" ? SKILL_MD_PATH : NARRATIVE_SKILL_MD_PATH, "utf-8")
56
82
 
57
83
  // Check for preview.html
58
84
  const designDir = join(DESIGNS_DIR, design)
@@ -73,30 +99,37 @@ export function buildPrompt(designName?: string, domainName?: string): string {
73
99
  })
74
100
  }
75
101
 
76
- // Layer 3 — DESIGN.md: marker-aware or full-text fallback
77
- const designSkill = buildDesignLayer(design)
102
+ // Layer 3 — DESIGN.md: deck-render only. Narrative mode must not inject
103
+ // visual CSS, layout catalogs, component indexes, or HTML skeleton rules.
104
+ const designSkill = mode === "deck-render" ? buildDesignLayer(design) : ""
78
105
 
79
106
  // Assemble header
80
- const header =
81
- `<!-- Active design: ${design} -->\n` +
82
- `<!-- Active domain: ${domain} -->\n` +
83
- `<!-- Design files: ${designDir}/ -->\n` +
84
- `<!-- - DESIGN.md metadata + style instructions (injected below) -->\n` +
85
- `${previewLine}\n\n`
86
-
87
- // Three-layer concatenation: Header SKILL Domain Design
107
+ const header = mode === "deck-render"
108
+ ? `<!-- Revela prompt mode: deck-render -->\n` +
109
+ `<!-- Active design: ${design} -->\n` +
110
+ `<!-- Active domain: ${domain} -->\n` +
111
+ `<!-- Design files: ${designDir}/ -->\n` +
112
+ `<!-- - DESIGN.md — metadata + style instructions (injected below) -->\n` +
113
+ `${previewLine}\n\n`
114
+ : `<!-- Revela prompt mode: narrative -->\n` +
115
+ `<!-- Active domain: ${domain} -->\n` +
116
+ `<!-- Design layer intentionally omitted in narrative mode. Use deck-render mode before writing deck artifacts. -->\n\n`
117
+
118
+ // Concatenation: Header → Skill → Domain → Design (deck-render only)
88
119
  const parts = [header, coreSkill]
89
120
  if (domainSkill) {
90
121
  parts.push(`\n\n---\n\n${domainSkill}`)
91
122
  }
92
- parts.push(`\n\n---\n\n${designSkill}`)
123
+ if (designSkill) {
124
+ parts.push(`\n\n---\n\n${designSkill}`)
125
+ }
93
126
 
94
127
  const prompt = parts.join("")
95
128
 
96
129
  // Write to _active-prompt.md
97
130
  mkdirSync(CONFIG_DIR, { recursive: true })
98
131
  writeFileSync(ACTIVE_PROMPT_FILE, prompt, "utf-8")
99
- promptLog.info("prompt rebuilt", { design, domain, bytes: prompt.length })
132
+ promptLog.info("prompt rebuilt", { mode, design, domain, bytes: prompt.length })
100
133
 
101
134
  return ACTIVE_PROMPT_FILE
102
135
  }
@@ -40,15 +40,25 @@ export interface EvidenceStatusMatch {
40
40
  slideIndex?: number
41
41
  slideTitle?: string
42
42
  claimId?: string
43
+ canonicalClaimId?: string
43
44
  claimText?: string
44
45
  claimEvidenceSensitive?: boolean
45
46
  claimEvidenceSupport?: string
47
+ evidenceBindingIds: string[]
48
+ supportedScope?: string
49
+ unsupportedScope?: string
50
+ caveats: string[]
46
51
  }
47
52
 
48
53
  export interface EvidenceStatusEvidence extends EvidenceRef {
49
54
  slideIndex: number
50
55
  slideTitle: string
51
56
  hasDetail: boolean
57
+ evidenceBindingId?: string
58
+ claimId?: string
59
+ supportScope?: string
60
+ unsupportedScope?: string
61
+ strength?: "strong" | "partial" | "weak"
52
62
  }
53
63
 
54
64
  export interface EvidenceStatusGap {
@@ -159,9 +169,14 @@ function projectMatch(match: InspectionElementMatch): EvidenceStatusMatch {
159
169
  slideIndex: match.slide?.index,
160
170
  slideTitle: match.slide?.title,
161
171
  claimId: match.claim?.id,
172
+ canonicalClaimId: match.claim?.canonicalClaimId,
162
173
  claimText: match.claim?.text,
163
174
  claimEvidenceSensitive: match.claim?.evidenceSensitive,
164
175
  claimEvidenceSupport: match.claim?.evidenceSupport,
176
+ evidenceBindingIds: match.claim?.evidenceBindingIds ?? [],
177
+ supportedScope: match.claim?.supportedScope,
178
+ unsupportedScope: match.claim?.unsupportedScope,
179
+ caveats: match.claim?.caveats ?? [],
165
180
  }
166
181
  }
167
182
 
@@ -235,7 +250,12 @@ function relevantSearchDiagnostics(issues: ReadinessIssue[], match: InspectionEl
235
250
 
236
251
  function actionTraceForMatch(actions: WorkspaceAction[], match: InspectionElementMatch): EvidenceStatusActionTrace[] {
237
252
  const slideNodeId = match.slide ? `slide:${match.slide.index}` : undefined
238
- const evidenceKeys = new Set(match.evidence.flatMap((item) => [item.source, item.sourcePath, item.findingsFile].filter((value): value is string => Boolean(value))))
253
+ const evidenceKeys = new Set([
254
+ match.claim?.id,
255
+ match.claim?.canonicalClaimId,
256
+ ...(match.claim?.evidenceBindingIds ?? []),
257
+ ...match.evidence.flatMap((item) => [item.source, item.sourcePath, item.findingsFile, item.evidenceBindingId, item.claimId]),
258
+ ].filter((value): value is string => Boolean(value)))
239
259
  return actions
240
260
  .filter((action) => actionRelevantToMatch(action, slideNodeId, evidenceKeys))
241
261
  .slice(-12)