@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.
- package/README.md +35 -29
- package/README.zh-CN.md +35 -29
- package/lib/commands/brief.ts +63 -0
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +7 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/narrative.ts +160 -0
- package/lib/commands/review.ts +115 -1
- package/lib/decks-state.ts +46 -3
- 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 +61 -0
- package/lib/narrative-state/map-html.ts +348 -0
- package/lib/narrative-state/map.ts +282 -0
- package/lib/narrative-state/normalize.ts +361 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/queries.ts +433 -0
- package/lib/narrative-state/readiness.ts +359 -0
- package/lib/narrative-state/render-plan.ts +250 -0
- package/lib/narrative-state/research-gaps.ts +191 -0
- package/lib/narrative-state/types.ts +172 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/workspace-state/evidence-status.ts +21 -1
- package/lib/workspace-state/graph.ts +174 -2
- package/lib/workspace-state/types.ts +13 -1
- package/package.json +1 -1
- package/plugin.ts +58 -2
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +265 -2
- package/tools/narrative-view.ts +84 -0
- 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
|
+
}
|
package/lib/prompt-builder.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Prompt builder — assembles
|
|
2
|
+
* Prompt builder — assembles Revela system prompts.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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
|
|
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(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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:
|
|
77
|
-
|
|
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
|
-
`<!--
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|