@cyber-dash-tech/revela 0.10.0 → 0.12.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 +54 -28
- package/README.zh-CN.md +54 -28
- 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 +5 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/inspect.ts +1 -1
- package/lib/commands/pdf.ts +33 -5
- package/lib/commands/pptx.ts +14 -9
- package/lib/commands/refine.ts +1 -1
- package/lib/commands/review.ts +115 -1
- package/lib/deck-html/contract.ts +252 -0
- package/lib/decks-state.ts +111 -28
- package/lib/document-materials/extract.ts +20 -0
- package/lib/edit/resolve-deck.ts +13 -2
- package/lib/inspect/open.ts +3 -1
- package/lib/narrative-state/hash.ts +52 -0
- package/lib/narrative-state/normalize.ts +307 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/readiness.ts +289 -0
- package/lib/narrative-state/render-plan.ts +207 -0
- package/lib/narrative-state/types.ts +139 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/qa/export-gate.ts +8 -1
- package/lib/refine/open.ts +3 -1
- package/lib/workspace-state/actions.ts +71 -0
- package/lib/workspace-state/compat.ts +10 -0
- package/lib/workspace-state/evidence-status.ts +267 -0
- package/lib/workspace-state/graph.ts +544 -0
- package/lib/workspace-state/render-targets.ts +182 -0
- package/lib/workspace-state/rendered-artifacts.ts +43 -0
- package/lib/workspace-state/repository.ts +43 -0
- package/lib/workspace-state/research-attachments.ts +130 -0
- package/lib/workspace-state/review-snapshots.ts +127 -0
- package/lib/workspace-state/types.ts +122 -0
- package/package.json +1 -1
- package/plugin.ts +53 -3
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +233 -6
- package/tools/pdf.ts +9 -1
- package/tools/pptx.ts +10 -0
- package/tools/research-save.ts +15 -0
- package/tools/workspace-scan.ts +29 -1
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec } from "../decks-state"
|
|
2
|
+
import { computeNarrativeHash } from "./hash"
|
|
3
|
+
import { normalizeNarrativeState } from "./normalize"
|
|
4
|
+
import { narrativeToBrief } from "./project-compat"
|
|
5
|
+
import type { NarrativeClaim, NarrativeEvidenceBinding, NarrativeStateV1 } from "./types"
|
|
6
|
+
|
|
7
|
+
export interface CompileDeckPlanOptions {
|
|
8
|
+
now?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CompileDeckPlanResult {
|
|
12
|
+
compiled: boolean
|
|
13
|
+
skipped: boolean
|
|
14
|
+
reason?: string
|
|
15
|
+
narrativeHash: string
|
|
16
|
+
slideCount: number
|
|
17
|
+
slides: SlideSpec[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compileDeckPlanFromNarrative(state: DecksState, options: CompileDeckPlanOptions = {}): { state: DecksState; result: CompileDeckPlanResult } {
|
|
21
|
+
const narrative = normalizeNarrativeState(state)
|
|
22
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
23
|
+
const approval = hasCurrentApprovalOrOverride(narrative, narrativeHash)
|
|
24
|
+
if (!approval) {
|
|
25
|
+
return {
|
|
26
|
+
state: { ...state, narrative },
|
|
27
|
+
result: {
|
|
28
|
+
compiled: false,
|
|
29
|
+
skipped: true,
|
|
30
|
+
reason: "narrative must be approved or explicitly overridden before compiling a deck plan",
|
|
31
|
+
narrativeHash,
|
|
32
|
+
slideCount: 0,
|
|
33
|
+
slides: [],
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const deckKey = state.activeDeck ?? Object.keys(state.decks)[0]
|
|
39
|
+
const deck = deckKey ? state.decks[deckKey] : undefined
|
|
40
|
+
const slug = deck?.slug ?? state.activeDeck ?? "deck"
|
|
41
|
+
const slides = buildSlides(narrative)
|
|
42
|
+
const requiredInputs: Partial<RequiredInputs> = {
|
|
43
|
+
topicClarified: true,
|
|
44
|
+
audienceClarified: Boolean(narrative.audience.primary),
|
|
45
|
+
languageDecided: Boolean(deck?.language),
|
|
46
|
+
sourceMaterialsIdentified: (state.workspace.sourceMaterials ?? []).length > 0 || narrative.evidenceBindings.length > 0,
|
|
47
|
+
researchNeedAssessed: true,
|
|
48
|
+
researchFindingsRead: narrative.evidenceBindings.some((binding) => Boolean(binding.findingsFile)),
|
|
49
|
+
slidePlanConfirmed: false,
|
|
50
|
+
designLayoutsFetched: false,
|
|
51
|
+
}
|
|
52
|
+
let next = upsertDeck({ ...state, narrative }, {
|
|
53
|
+
...deck,
|
|
54
|
+
slug,
|
|
55
|
+
goal: deck?.goal || narrative.thesis?.statement || narrative.decision.action,
|
|
56
|
+
audience: narrative.audience.primary || deck?.audience,
|
|
57
|
+
outputPath: deck?.outputPath,
|
|
58
|
+
narrativeBrief: narrativeToBrief(narrative),
|
|
59
|
+
requiredInputs: {
|
|
60
|
+
...(deck?.requiredInputs ?? {}),
|
|
61
|
+
...requiredInputs,
|
|
62
|
+
} as RequiredInputs,
|
|
63
|
+
writeReadiness: deck?.writeReadiness ?? { status: "blocked" as const, blockers: [] },
|
|
64
|
+
})
|
|
65
|
+
next = upsertSlides(next, slug, slides)
|
|
66
|
+
next.narrative = { ...narrative, updatedAt: options.now ?? narrative.updatedAt }
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
state: next,
|
|
70
|
+
result: {
|
|
71
|
+
compiled: true,
|
|
72
|
+
skipped: false,
|
|
73
|
+
narrativeHash,
|
|
74
|
+
slideCount: slides.length,
|
|
75
|
+
slides,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildSlides(narrative: NarrativeStateV1): SlideSpec[] {
|
|
81
|
+
const slides: SlideSpec[] = []
|
|
82
|
+
const centralClaims = narrative.claims.filter((claim) => claim.importance === "central")
|
|
83
|
+
const supportingClaims = narrative.claims.filter((claim) => claim.importance !== "central")
|
|
84
|
+
const evidenceByClaim = new Map<string, NarrativeEvidenceBinding[]>()
|
|
85
|
+
for (const binding of narrative.evidenceBindings) {
|
|
86
|
+
const list = evidenceByClaim.get(binding.claimId) ?? []
|
|
87
|
+
list.push(binding)
|
|
88
|
+
evidenceByClaim.set(binding.claimId, list)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
slides.push({
|
|
92
|
+
index: slides.length + 1,
|
|
93
|
+
title: "Decision Context",
|
|
94
|
+
purpose: "Frame the audience belief shift and decision required before presenting the recommendation.",
|
|
95
|
+
narrativeRole: "context",
|
|
96
|
+
layout: "cover",
|
|
97
|
+
qa: false,
|
|
98
|
+
components: [],
|
|
99
|
+
content: {
|
|
100
|
+
headline: narrative.thesis?.statement || narrative.decision.action || "Narrative context",
|
|
101
|
+
body: [
|
|
102
|
+
narrative.audience.beliefBefore ? `Before: ${narrative.audience.beliefBefore}` : "Before belief needs confirmation.",
|
|
103
|
+
narrative.audience.beliefAfter ? `After: ${narrative.audience.beliefAfter}` : "After belief needs confirmation.",
|
|
104
|
+
],
|
|
105
|
+
bullets: narrative.decision.action ? [`Decision: ${narrative.decision.action}`] : [],
|
|
106
|
+
},
|
|
107
|
+
evidence: [],
|
|
108
|
+
status: "planned",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
for (const claim of centralClaims) slides.push(claimSlide(slides.length + 1, claim, evidenceByClaim.get(claim.id) ?? []))
|
|
112
|
+
if (supportingClaims.length > 0) {
|
|
113
|
+
slides.push({
|
|
114
|
+
index: slides.length + 1,
|
|
115
|
+
title: "Supporting Logic",
|
|
116
|
+
purpose: "Connect supporting claims to the central recommendation without overloading the main proof slides.",
|
|
117
|
+
narrativeRole: "evidence",
|
|
118
|
+
layout: "card-grid",
|
|
119
|
+
qa: true,
|
|
120
|
+
components: ["card"],
|
|
121
|
+
content: {
|
|
122
|
+
headline: "Supporting claims and boundaries",
|
|
123
|
+
bullets: supportingClaims.slice(0, 5).map((claim) => claim.text),
|
|
124
|
+
},
|
|
125
|
+
evidence: supportingClaims.flatMap((claim) => (evidenceByClaim.get(claim.id) ?? []).map(evidenceRefFromBinding)),
|
|
126
|
+
status: "planned",
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (narrative.risks.length > 0 || narrative.objections.length > 0) {
|
|
131
|
+
slides.push({
|
|
132
|
+
index: slides.length + 1,
|
|
133
|
+
title: "Risks And Objections",
|
|
134
|
+
purpose: "Make caveats and stakeholder objections visible before asking for a decision.",
|
|
135
|
+
narrativeRole: "risk",
|
|
136
|
+
layout: "two-col",
|
|
137
|
+
qa: true,
|
|
138
|
+
components: ["card"],
|
|
139
|
+
content: {
|
|
140
|
+
headline: "What could break the recommendation",
|
|
141
|
+
bullets: [
|
|
142
|
+
...narrative.risks.slice(0, 3).map((risk) => risk.mitigation ? `${risk.text} Mitigation: ${risk.mitigation}` : risk.text),
|
|
143
|
+
...narrative.objections.slice(0, 3).map((objection) => objection.response ? `${objection.text} Response: ${objection.response}` : objection.text),
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
evidence: [],
|
|
147
|
+
status: "planned",
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
slides.push({
|
|
152
|
+
index: slides.length + 1,
|
|
153
|
+
title: "Decision Ask",
|
|
154
|
+
purpose: "Close with the explicit decision or action requested from the audience.",
|
|
155
|
+
narrativeRole: "ask",
|
|
156
|
+
layout: "closing",
|
|
157
|
+
qa: false,
|
|
158
|
+
components: [],
|
|
159
|
+
content: {
|
|
160
|
+
headline: narrative.decision.action || "Confirm the decision",
|
|
161
|
+
bullets: narrative.decision.consequenceOfNoDecision ? [`If no decision: ${narrative.decision.consequenceOfNoDecision}`] : [],
|
|
162
|
+
},
|
|
163
|
+
evidence: [],
|
|
164
|
+
status: "planned",
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return slides
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function claimSlide(index: number, claim: NarrativeClaim, bindings: NarrativeEvidenceBinding[]): SlideSpec {
|
|
171
|
+
return {
|
|
172
|
+
index,
|
|
173
|
+
title: titleFromClaim(claim),
|
|
174
|
+
purpose: `Prove or bound this ${claim.importance} ${claim.kind} claim for the audience.`,
|
|
175
|
+
narrativeRole: claim.kind === "risk" || claim.kind === "assumption" ? "risk" : claim.kind === "ask" ? "ask" : claim.kind === "recommendation" ? "recommendation" : "evidence",
|
|
176
|
+
layout: "two-col",
|
|
177
|
+
qa: true,
|
|
178
|
+
components: ["card"],
|
|
179
|
+
content: {
|
|
180
|
+
headline: claim.text,
|
|
181
|
+
bullets: [claim.supportedScope, claim.unsupportedScope ? `Unsupported scope: ${claim.unsupportedScope}` : undefined, ...(claim.caveats ?? [])].filter((item): item is string => Boolean(item)),
|
|
182
|
+
},
|
|
183
|
+
evidence: bindings.map(evidenceRefFromBinding),
|
|
184
|
+
status: "planned",
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function evidenceRefFromBinding(binding: NarrativeEvidenceBinding): EvidenceRef {
|
|
189
|
+
return {
|
|
190
|
+
source: binding.source,
|
|
191
|
+
quote: binding.quote,
|
|
192
|
+
url: binding.url,
|
|
193
|
+
sourcePath: binding.sourcePath,
|
|
194
|
+
location: binding.location,
|
|
195
|
+
findingsFile: binding.findingsFile,
|
|
196
|
+
caveat: binding.caveat || binding.unsupportedScope,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function titleFromClaim(claim: NarrativeClaim): string {
|
|
201
|
+
const words = claim.text.split(/\s+/).filter(Boolean).slice(0, 6).join(" ")
|
|
202
|
+
return words || claim.kind
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function hasCurrentApprovalOrOverride(narrative: NarrativeStateV1, narrativeHash: string): boolean {
|
|
206
|
+
return narrative.approvals.some((approval) => approval.narrativeHash === narrativeHash && (approval.scope === "narrative" && approval.approvedBy === "user" || approval.scope === "render_override" || approval.approvedBy === "override"))
|
|
207
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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 interface NarrativeStateV1 {
|
|
8
|
+
version: 1
|
|
9
|
+
id: string
|
|
10
|
+
status: NarrativeStatus
|
|
11
|
+
audience: AudienceIntent
|
|
12
|
+
decision: DecisionIntent
|
|
13
|
+
thesis?: NarrativeThesis
|
|
14
|
+
claims: NarrativeClaim[]
|
|
15
|
+
evidenceBindings: NarrativeEvidenceBinding[]
|
|
16
|
+
objections: NarrativeObjection[]
|
|
17
|
+
risks: NarrativeRisk[]
|
|
18
|
+
approvals: NarrativeApproval[]
|
|
19
|
+
updatedAt: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface AudienceIntent {
|
|
23
|
+
primary: string
|
|
24
|
+
secondary?: string[]
|
|
25
|
+
beliefBefore: string
|
|
26
|
+
beliefAfter: string
|
|
27
|
+
decisionContext?: string
|
|
28
|
+
successCriteria?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface DecisionIntent {
|
|
32
|
+
action: string
|
|
33
|
+
owner?: string
|
|
34
|
+
deadline?: string
|
|
35
|
+
decisionType?: "approve" | "invest" | "prioritize" | "align" | "choose" | "understand" | "other"
|
|
36
|
+
consequenceOfNoDecision?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface NarrativeThesis {
|
|
40
|
+
id: string
|
|
41
|
+
statement: string
|
|
42
|
+
confidence: "high" | "medium" | "low"
|
|
43
|
+
caveat?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface NarrativeClaim {
|
|
47
|
+
id: string
|
|
48
|
+
kind: NarrativeClaimKind
|
|
49
|
+
text: string
|
|
50
|
+
importance: "central" | "supporting" | "background"
|
|
51
|
+
evidenceRequired: boolean
|
|
52
|
+
evidenceStatus: NarrativeEvidenceStatus
|
|
53
|
+
supportedScope?: string
|
|
54
|
+
unsupportedScope?: string
|
|
55
|
+
caveats?: string[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NarrativeEvidenceBinding {
|
|
59
|
+
id: string
|
|
60
|
+
claimId: string
|
|
61
|
+
source: string
|
|
62
|
+
sourcePath?: string
|
|
63
|
+
findingsFile?: string
|
|
64
|
+
quote?: string
|
|
65
|
+
location?: string
|
|
66
|
+
url?: string
|
|
67
|
+
caveat?: string
|
|
68
|
+
supportScope?: string
|
|
69
|
+
unsupportedScope?: string
|
|
70
|
+
strength: "strong" | "partial" | "weak"
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface NarrativeObjection {
|
|
74
|
+
id: string
|
|
75
|
+
text: string
|
|
76
|
+
claimId?: string
|
|
77
|
+
priority: "high" | "medium" | "low"
|
|
78
|
+
response?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface NarrativeRisk {
|
|
82
|
+
id: string
|
|
83
|
+
text: string
|
|
84
|
+
claimId?: string
|
|
85
|
+
severity: "high" | "medium" | "low"
|
|
86
|
+
mitigation?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NarrativeApproval {
|
|
90
|
+
id: string
|
|
91
|
+
narrativeHash: string
|
|
92
|
+
approvedAt: string
|
|
93
|
+
approvedBy: "user" | "override"
|
|
94
|
+
scope: "narrative" | "render_override"
|
|
95
|
+
note?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type NarrativeReadinessStatus = "blocked" | "needs_research" | "needs_user_confirmation" | "ready_for_approval" | "approved"
|
|
99
|
+
|
|
100
|
+
export type NarrativeReadinessIssueType =
|
|
101
|
+
| "missing_audience"
|
|
102
|
+
| "missing_belief_shift"
|
|
103
|
+
| "missing_decision"
|
|
104
|
+
| "missing_thesis"
|
|
105
|
+
| "claim_chain_gap"
|
|
106
|
+
| "missing_evidence"
|
|
107
|
+
| "weak_evidence"
|
|
108
|
+
| "unsupported_scope"
|
|
109
|
+
| "unhandled_objection"
|
|
110
|
+
| "missing_risk"
|
|
111
|
+
| "approval_missing"
|
|
112
|
+
| "approval_stale"
|
|
113
|
+
| "artifact_stale"
|
|
114
|
+
| "research_findings_unattached"
|
|
115
|
+
|
|
116
|
+
export interface NarrativeReadinessIssue {
|
|
117
|
+
type: NarrativeReadinessIssueType
|
|
118
|
+
severity: "blocker" | "warning"
|
|
119
|
+
message: string
|
|
120
|
+
suggestedAction: string
|
|
121
|
+
claimId?: string
|
|
122
|
+
claimText?: string
|
|
123
|
+
source?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface NarrativeReadinessResult {
|
|
127
|
+
status: NarrativeReadinessStatus
|
|
128
|
+
narrativeHash: string
|
|
129
|
+
reviewedAt: string
|
|
130
|
+
blockers: string[]
|
|
131
|
+
warnings: string[]
|
|
132
|
+
issues: NarrativeReadinessIssue[]
|
|
133
|
+
approval?: {
|
|
134
|
+
current: boolean
|
|
135
|
+
stale: boolean
|
|
136
|
+
latest?: NarrativeApproval
|
|
137
|
+
}
|
|
138
|
+
nextActions: string[]
|
|
139
|
+
}
|
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
|
}
|
package/lib/qa/export-gate.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { formatReport, runQA } from "./index"
|
|
2
|
+
import { assertDeckHtmlContractValid } from "../deck-html/contract"
|
|
3
|
+
|
|
4
|
+
export interface ExportQAGateOptions {
|
|
5
|
+
workspaceRoot?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function assertExportQAPassed(filePath: string, options: ExportQAGateOptions = {}): Promise<void> {
|
|
9
|
+
if (options.workspaceRoot) assertDeckHtmlContractValid(options.workspaceRoot, filePath)
|
|
2
10
|
|
|
3
|
-
export async function assertExportQAPassed(filePath: string): Promise<void> {
|
|
4
11
|
const report = await runQA(filePath)
|
|
5
12
|
if (report.totalIssues === 0) return
|
|
6
13
|
|
package/lib/refine/open.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "fs"
|
|
|
2
2
|
import { ACTIVE_PROMPT_FILE } from "../config"
|
|
3
3
|
import { ctx } from "../ctx"
|
|
4
4
|
import { seedBuiltinDesigns } from "../design/designs"
|
|
5
|
+
import { assertDeckHtmlContractValid } from "../deck-html/contract"
|
|
5
6
|
import { seedBuiltinDomains } from "../domain/domains"
|
|
6
7
|
import { ensureEditableDeckState } from "../edit/deck-state"
|
|
7
8
|
import { openUrl } from "../edit/open"
|
|
@@ -33,6 +34,7 @@ export interface OpenRefineDeckOptions {
|
|
|
33
34
|
export function openRefineDeck(target: string, options: OpenRefineDeckOptions): OpenRefineDeckResult {
|
|
34
35
|
const deck = resolveEditableDeck(options.workspaceRoot, target)
|
|
35
36
|
const preflight = ensureEditableDeckState(options.workspaceRoot, deck)
|
|
37
|
+
assertDeckHtmlContractValid(options.workspaceRoot, deck.absoluteFile)
|
|
36
38
|
const mode = options.mode ?? "edit"
|
|
37
39
|
|
|
38
40
|
ctx.enabled = true
|
|
@@ -57,7 +59,7 @@ export function openRefineDeck(target: string, options: OpenRefineDeckOptions):
|
|
|
57
59
|
return {
|
|
58
60
|
deck,
|
|
59
61
|
url,
|
|
60
|
-
source: deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path",
|
|
62
|
+
source: deck.source === "render-target" ? "render target" : deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path",
|
|
61
63
|
stateNote: preflight.changed ? "Deck state was prepared in DECKS.json for refinement." : "Deck state already points to this refinement target.",
|
|
62
64
|
preflightChanged: preflight.changed,
|
|
63
65
|
reusedSession: session.reused,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import type { DecksState } from "../decks-state"
|
|
3
|
+
import type { WorkspaceAction, WorkspaceActionType } from "./types"
|
|
4
|
+
|
|
5
|
+
export const MAX_WORKSPACE_ACTIONS = 500
|
|
6
|
+
|
|
7
|
+
export interface WorkspaceActionInput {
|
|
8
|
+
type: WorkspaceActionType
|
|
9
|
+
actor?: string
|
|
10
|
+
inputs?: Record<string, unknown>
|
|
11
|
+
outputs?: Record<string, unknown>
|
|
12
|
+
status?: WorkspaceAction["status"]
|
|
13
|
+
summary?: string
|
|
14
|
+
nodeIds?: string[]
|
|
15
|
+
timestamp?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function recordWorkspaceAction(state: DecksState, input: WorkspaceActionInput): DecksState {
|
|
19
|
+
const actions = state.actions ?? []
|
|
20
|
+
const timestamp = input.timestamp ?? new Date().toISOString()
|
|
21
|
+
const action: WorkspaceAction = {
|
|
22
|
+
id: workspaceActionId(input.type, timestamp, actions.length, input),
|
|
23
|
+
type: input.type,
|
|
24
|
+
timestamp,
|
|
25
|
+
status: input.status ?? "success",
|
|
26
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
27
|
+
...(input.inputs ? { inputs: compactActionPayload(input.inputs) } : {}),
|
|
28
|
+
...(input.outputs ? { outputs: compactActionPayload(input.outputs) } : {}),
|
|
29
|
+
...(input.summary ? { summary: input.summary } : {}),
|
|
30
|
+
...(input.nodeIds && input.nodeIds.length > 0 ? { nodeIds: [...new Set(input.nodeIds)].sort() } : {}),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
state.actions = [...actions, action].slice(-MAX_WORKSPACE_ACTIONS)
|
|
34
|
+
return state
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compactActionPayload(input: Record<string, unknown>): Record<string, unknown> {
|
|
38
|
+
const output: Record<string, unknown> = {}
|
|
39
|
+
for (const [key, value] of Object.entries(input)) {
|
|
40
|
+
const compacted = compactActionValue(value)
|
|
41
|
+
if (compacted !== undefined) output[key] = compacted
|
|
42
|
+
}
|
|
43
|
+
return output
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function workspaceActionId(type: WorkspaceActionType, timestamp: string, sequence: number, input: Omit<WorkspaceActionInput, "timestamp">): string {
|
|
47
|
+
return `action:${timestamp}:${type}:${stableHash(JSON.stringify({ sequence, input: compactActionPayload(input as Record<string, unknown>) }))}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function compactActionValue(value: unknown): unknown {
|
|
51
|
+
if (value === undefined || value === null) return undefined
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
const trimmed = value.trim()
|
|
54
|
+
if (!trimmed) return undefined
|
|
55
|
+
return trimmed.length > 500 ? `${trimmed.slice(0, 500).trimEnd()}... [truncated]` : trimmed
|
|
56
|
+
}
|
|
57
|
+
if (typeof value === "number" || typeof value === "boolean") return value
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
const items = value.map(compactActionValue).filter((item) => item !== undefined)
|
|
60
|
+
return items.length > 0 ? items.slice(0, 50) : undefined
|
|
61
|
+
}
|
|
62
|
+
if (typeof value === "object") {
|
|
63
|
+
const compacted = compactActionPayload(value as Record<string, unknown>)
|
|
64
|
+
return Object.keys(compacted).length > 0 ? compacted : undefined
|
|
65
|
+
}
|
|
66
|
+
return undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function stableHash(value: string): string {
|
|
70
|
+
return createHash("sha1").update(value).digest("hex").slice(0, 10)
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DecksState } from "../decks-state"
|
|
2
|
+
import type { DecksStateV1Projection, WorkspaceState } from "./types"
|
|
3
|
+
|
|
4
|
+
export function isDecksStateV1(state: WorkspaceState): state is DecksState {
|
|
5
|
+
return state.version === 1
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function asDecksStateV1Projection(state: DecksState): DecksStateV1Projection {
|
|
9
|
+
return state
|
|
10
|
+
}
|