@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
package/lib/decks-state.ts
CHANGED
|
@@ -1,8 +1,26 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "fs"
|
|
2
2
|
import { createHash } from "crypto"
|
|
3
|
-
import { basename,
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { basename, join, resolve } from "path"
|
|
4
|
+
import {
|
|
5
|
+
hasWorkspaceState,
|
|
6
|
+
readOrCreateWorkspaceState,
|
|
7
|
+
readWorkspaceState,
|
|
8
|
+
workspaceStatePath,
|
|
9
|
+
writeWorkspaceState,
|
|
10
|
+
} from "./workspace-state/repository"
|
|
11
|
+
import { ensureActiveHtmlDeckRenderTarget } from "./workspace-state/render-targets"
|
|
12
|
+
import {
|
|
13
|
+
activeReviewTargetId,
|
|
14
|
+
appendReviewSnapshot,
|
|
15
|
+
createReviewSnapshot,
|
|
16
|
+
isReviewSnapshotCurrent,
|
|
17
|
+
latestReviewSnapshotForTarget,
|
|
18
|
+
} from "./workspace-state/review-snapshots"
|
|
19
|
+
import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
|
|
20
|
+
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
|
|
21
|
+
import type { NarrativeStateV1 } from "./narrative-state/types"
|
|
22
|
+
|
|
23
|
+
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
6
24
|
|
|
7
25
|
export type DeckProductionStatus = "planning" | "blocked" | "ready" | "written"
|
|
8
26
|
export type SlideProductionStatus = "planned" | "ready" | "written" | "qa_passed" | "qa_failed"
|
|
@@ -12,6 +30,7 @@ export type NarrativeRole = "context" | "tension" | "evidence" | "recommendation
|
|
|
12
30
|
export interface DecksState {
|
|
13
31
|
version: 1
|
|
14
32
|
activeDeck?: string
|
|
33
|
+
narrative?: NarrativeStateV1
|
|
15
34
|
workspace: {
|
|
16
35
|
brief?: string
|
|
17
36
|
sourceMaterials: SourceMaterial[]
|
|
@@ -23,6 +42,9 @@ export interface DecksState {
|
|
|
23
42
|
openQuestions: string[]
|
|
24
43
|
}
|
|
25
44
|
decks: Record<string, DeckSpec>
|
|
45
|
+
actions: WorkspaceAction[]
|
|
46
|
+
renderTargets: RenderTarget[]
|
|
47
|
+
reviews: ReviewSnapshot[]
|
|
26
48
|
}
|
|
27
49
|
|
|
28
50
|
export interface SourceMaterial {
|
|
@@ -249,11 +271,11 @@ export interface ReviewDeckStateOptions {
|
|
|
249
271
|
}
|
|
250
272
|
|
|
251
273
|
export function decksStatePath(workspaceRoot: string): string {
|
|
252
|
-
return
|
|
274
|
+
return workspaceStatePath(workspaceRoot, DECKS_STATE_FILE)
|
|
253
275
|
}
|
|
254
276
|
|
|
255
277
|
export function hasDecksState(workspaceRoot: string): boolean {
|
|
256
|
-
return
|
|
278
|
+
return hasWorkspaceState(workspaceRoot, DECKS_STATE_FILE)
|
|
257
279
|
}
|
|
258
280
|
|
|
259
281
|
export function createEmptyDecksState(): DecksState {
|
|
@@ -266,6 +288,9 @@ export function createEmptyDecksState(): DecksState {
|
|
|
266
288
|
openQuestions: [],
|
|
267
289
|
},
|
|
268
290
|
decks: {},
|
|
291
|
+
actions: [],
|
|
292
|
+
renderTargets: [],
|
|
293
|
+
reviews: [],
|
|
269
294
|
}
|
|
270
295
|
}
|
|
271
296
|
|
|
@@ -289,6 +314,7 @@ export function normalizeWorkspaceDeckState(state: DecksState, workspaceRoot: st
|
|
|
289
314
|
delete normalized.decks[existingKey]
|
|
290
315
|
normalized.decks[slug] = deck
|
|
291
316
|
normalized.activeDeck = slug
|
|
317
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
292
318
|
return normalized
|
|
293
319
|
}
|
|
294
320
|
|
|
@@ -326,21 +352,15 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
|
|
|
326
352
|
}
|
|
327
353
|
|
|
328
354
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
329
|
-
|
|
330
|
-
return normalizeDecksState(parsed)
|
|
355
|
+
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
331
356
|
}
|
|
332
357
|
|
|
333
358
|
export function writeDecksState(workspaceRoot: string, state: DecksState): void {
|
|
334
|
-
|
|
335
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
336
|
-
writeFileSync(filePath, JSON.stringify(normalizeDecksState(state), null, 2) + "\n", "utf-8")
|
|
359
|
+
writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
337
360
|
}
|
|
338
361
|
|
|
339
362
|
export function readOrCreateDecksState(workspaceRoot: string): DecksState {
|
|
340
|
-
|
|
341
|
-
const state = createEmptyDecksState()
|
|
342
|
-
writeDecksState(workspaceRoot, state)
|
|
343
|
-
return state
|
|
363
|
+
return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
|
|
@@ -354,6 +374,7 @@ export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug:
|
|
|
354
374
|
const next = createDeckSpec({ ...existing, ...input, slug })
|
|
355
375
|
normalized.decks[slug] = next
|
|
356
376
|
normalized.activeDeck = slug
|
|
377
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
357
378
|
return normalized
|
|
358
379
|
}
|
|
359
380
|
|
|
@@ -370,6 +391,7 @@ export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[
|
|
|
370
391
|
deck.slides = [...byIndex.values()].sort((a, b) => a.index - b.index)
|
|
371
392
|
normalized.decks[key] = deck
|
|
372
393
|
normalized.activeDeck = key
|
|
394
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
373
395
|
return normalized
|
|
374
396
|
}
|
|
375
397
|
|
|
@@ -459,26 +481,29 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
|
|
|
459
481
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
460
482
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
461
483
|
const evidenceCandidates = issues.flatMap((issue) => issue.evidenceCandidates ?? [])
|
|
484
|
+
const reviewedAt = new Date().toISOString()
|
|
462
485
|
deck.writeReadiness = {
|
|
463
486
|
status: blockers.length === 0 ? "ready" : "blocked",
|
|
464
487
|
blockers,
|
|
465
|
-
lastReviewedAt:
|
|
488
|
+
lastReviewedAt: reviewedAt,
|
|
466
489
|
}
|
|
467
490
|
deck.status = blockers.length === 0 ? "ready" : "blocked"
|
|
468
491
|
normalized.decks[deck.slug] = deck
|
|
469
492
|
normalized.activeDeck = deck.slug
|
|
493
|
+
const result: DeckStateReadinessResult = {
|
|
494
|
+
ready: blockers.length === 0,
|
|
495
|
+
slug: deck.slug,
|
|
496
|
+
status: deck.writeReadiness.status,
|
|
497
|
+
blocker: blockers.join("; "),
|
|
498
|
+
blockers,
|
|
499
|
+
warnings,
|
|
500
|
+
issues,
|
|
501
|
+
evidenceCandidates,
|
|
502
|
+
}
|
|
503
|
+
appendReviewSnapshot(normalized, createReviewSnapshot(normalized, { slug: deck.slug, result, reviewedAt }))
|
|
470
504
|
return {
|
|
471
505
|
state: normalized,
|
|
472
|
-
result
|
|
473
|
-
ready: blockers.length === 0,
|
|
474
|
-
slug: deck.slug,
|
|
475
|
-
status: deck.writeReadiness.status,
|
|
476
|
-
blocker: blockers.join("; "),
|
|
477
|
-
blockers,
|
|
478
|
-
warnings,
|
|
479
|
-
issues,
|
|
480
|
-
evidenceCandidates,
|
|
481
|
-
},
|
|
506
|
+
result,
|
|
482
507
|
}
|
|
483
508
|
}
|
|
484
509
|
|
|
@@ -542,6 +567,38 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
542
567
|
suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
|
|
543
568
|
})
|
|
544
569
|
}
|
|
570
|
+
if (normalized.reviews.length > 0) {
|
|
571
|
+
const targetId = activeReviewTargetId(normalized)
|
|
572
|
+
const snapshot = latestReviewSnapshotForTarget(normalized, targetId)
|
|
573
|
+
if (!snapshot) {
|
|
574
|
+
const message = "No review snapshot exists for the active HTML render target"
|
|
575
|
+
blockers.unshift(message)
|
|
576
|
+
issues.unshift({
|
|
577
|
+
type: "missing_slide_spec",
|
|
578
|
+
severity: "blocker",
|
|
579
|
+
message,
|
|
580
|
+
suggestedAction: "Run /revela review so readiness is recorded against the current active render target.",
|
|
581
|
+
})
|
|
582
|
+
} else if (!isReviewSnapshotCurrent(normalized, snapshot, deck.slug)) {
|
|
583
|
+
const message = "Latest review snapshot is stale for the current deck, sources, evidence, narrative state, or render target"
|
|
584
|
+
blockers.unshift(message)
|
|
585
|
+
issues.unshift({
|
|
586
|
+
type: "missing_slide_spec",
|
|
587
|
+
severity: "blocker",
|
|
588
|
+
message,
|
|
589
|
+
suggestedAction: "Run /revela review again after the latest state changes before writing deck HTML.",
|
|
590
|
+
})
|
|
591
|
+
} else if (snapshot.status !== "ready") {
|
|
592
|
+
const message = `Latest review snapshot is ${snapshot.status}, not ready`
|
|
593
|
+
blockers.unshift(message)
|
|
594
|
+
issues.unshift({
|
|
595
|
+
type: "missing_slide_spec",
|
|
596
|
+
severity: "blocker",
|
|
597
|
+
message,
|
|
598
|
+
suggestedAction: "Resolve review blockers and rerun /revela review before writing deck HTML.",
|
|
599
|
+
})
|
|
600
|
+
}
|
|
601
|
+
}
|
|
545
602
|
|
|
546
603
|
return {
|
|
547
604
|
ready: blockers.length === 0,
|
|
@@ -579,11 +636,12 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
|
|
|
579
636
|
activeDeck: activeKey,
|
|
580
637
|
workspace: compactWorkspaceForPrompt(state.workspace),
|
|
581
638
|
deck: active ? compactDeckForPrompt(active) : undefined,
|
|
639
|
+
renderTargets: state.renderTargets,
|
|
640
|
+
reviews: compactReviewsForPrompt(state.reviews),
|
|
582
641
|
}
|
|
583
642
|
let text = JSON.stringify(compact, null, 2)
|
|
584
643
|
if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
|
|
585
|
-
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
|
|
586
|
-
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, evidence, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- ${DECKS_STATE_FILE} deck slides use 1-based \`slides[].index\` values. Render every HTML \`<section class="slide">\` with a matching 1-based \`data-slide-index\` attribute, and do not use 0-based \`data-index\` as slide identity.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
|
|
644
|
+
return `---\n\n# Revela Workspace State From ${DECKS_STATE_FILE}\n\n\`\`\`json\n${text}\n\`\`\`\n\nRules for this state layer:\n- Treat ${DECKS_STATE_FILE} as the source of truth for the single current deck's specs, slide plan, evidence, render targets, and write readiness.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- ${DECKS_STATE_FILE} deck slides use 1-based \`slides[].index\` values. Render every HTML \`<section class="slide">\` with a matching 1-based \`data-slide-index\` attribute, and do not use 0-based \`data-index\` as slide identity.\n- The active HTML deck is represented as a \`renderTarget\` of type \`html_deck\`; PDF/PPTX exports should be recorded as derived render targets, not as separate deck specs.\n- \`writeReadiness\` is a compatibility projection. When review snapshots exist, deck HTML writes require a current non-stale ready review snapshot for the active HTML render target.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- Before writing decks/*.html, the current deck must have writeReadiness.status=ready and a complete slide spec, and its outputPath must match the target file.`
|
|
587
645
|
}
|
|
588
646
|
|
|
589
647
|
function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
|
|
@@ -616,6 +674,20 @@ function compactDeckForPrompt(deck: DeckSpec): DeckSpec {
|
|
|
616
674
|
}
|
|
617
675
|
}
|
|
618
676
|
|
|
677
|
+
function compactReviewsForPrompt(reviews: ReviewSnapshot[]): ReviewSnapshot[] {
|
|
678
|
+
return reviews.slice(-5).map((review) => ({
|
|
679
|
+
id: review.id,
|
|
680
|
+
targetId: review.targetId,
|
|
681
|
+
inputHash: review.inputHash,
|
|
682
|
+
status: review.status,
|
|
683
|
+
blockers: review.blockers.slice(0, 5),
|
|
684
|
+
warnings: review.warnings.slice(0, 5),
|
|
685
|
+
issues: review.issues.slice(0, 10),
|
|
686
|
+
evidenceCandidates: review.evidenceCandidates?.slice(0, 10),
|
|
687
|
+
reviewedAt: review.reviewedAt,
|
|
688
|
+
}))
|
|
689
|
+
}
|
|
690
|
+
|
|
619
691
|
function compactNarrativeBriefForPrompt(brief: NarrativeBrief | undefined): NarrativeBrief | undefined {
|
|
620
692
|
if (!brief) return undefined
|
|
621
693
|
return {
|
|
@@ -648,6 +720,7 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
648
720
|
const state: DecksState = {
|
|
649
721
|
version: 1,
|
|
650
722
|
activeDeck: input.activeDeck ? normalizeSlug(input.activeDeck) : undefined,
|
|
723
|
+
narrative: normalizeCanonicalNarrativeState(input.narrative, input.activeDeck || "workspace"),
|
|
651
724
|
workspace: {
|
|
652
725
|
brief: input.workspace?.brief,
|
|
653
726
|
sourceMaterials: input.workspace?.sourceMaterials ?? [],
|
|
@@ -659,6 +732,9 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
659
732
|
openQuestions: input.workspace?.openQuestions ?? [],
|
|
660
733
|
},
|
|
661
734
|
decks: {},
|
|
735
|
+
actions: input.actions ?? [],
|
|
736
|
+
renderTargets: input.renderTargets ?? [],
|
|
737
|
+
reviews: input.reviews ?? [],
|
|
662
738
|
}
|
|
663
739
|
for (const [slug, deck] of Object.entries(input.decks ?? {})) {
|
|
664
740
|
const normalizedSlug = normalizeSlug(deck.slug || slug)
|
|
@@ -669,6 +745,13 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
669
745
|
const keys = Object.keys(state.decks)
|
|
670
746
|
if (keys.length === 1) state.activeDeck = keys[0]
|
|
671
747
|
}
|
|
748
|
+
ensureActiveHtmlDeckRenderTarget(state)
|
|
749
|
+
return state
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
|
|
753
|
+
const state = normalizeDecksState(input)
|
|
754
|
+
if (!state.narrative && currentDeckKey(state)) state.narrative = normalizeNarrativeState(state)
|
|
672
755
|
return state
|
|
673
756
|
}
|
|
674
757
|
|
|
@@ -10,6 +10,7 @@ import { extractPptx } from "../read-hooks/extractors/pptx"
|
|
|
10
10
|
import { extractXlsx } from "../read-hooks/extractors/xlsx"
|
|
11
11
|
import { hasDecksState, readDecksState, writeDecksState } from "../decks-state"
|
|
12
12
|
import { computeSourceFingerprint, sourceMaterialMetadata, upsertSourceMaterial } from "../source-materials"
|
|
13
|
+
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
13
14
|
|
|
14
15
|
export type DocumentMaterial = {
|
|
15
16
|
path: string
|
|
@@ -180,6 +181,25 @@ function updateDecksSourceMaterialIndex(
|
|
|
180
181
|
: undefined,
|
|
181
182
|
lastExtracted: extracted ? (result.cache_status === "hit" ? existing?.lastExtracted ?? now : now) : undefined,
|
|
182
183
|
}, extracted ? "extracted" : "discovered")
|
|
184
|
+
recordWorkspaceAction(state, {
|
|
185
|
+
type: "source.extracted",
|
|
186
|
+
actor: "revela-extract-document-materials",
|
|
187
|
+
inputs: { file: base.path, type: base.type, fingerprint: base.fingerprint },
|
|
188
|
+
outputs: {
|
|
189
|
+
status: result.status,
|
|
190
|
+
cacheStatus: result.cache_status,
|
|
191
|
+
source: result.source,
|
|
192
|
+
manifestPath: result.manifest_path,
|
|
193
|
+
textPath: result.text_path,
|
|
194
|
+
cacheDir: result.cache_dir,
|
|
195
|
+
reason: result.reason,
|
|
196
|
+
imageCount: result.images?.length ?? 0,
|
|
197
|
+
tableCount: result.tables?.length ?? 0,
|
|
198
|
+
},
|
|
199
|
+
status: result.status === "failed" ? "failed" : result.status === "skipped" ? "skipped" : "success",
|
|
200
|
+
summary: extracted ? `Extracted reusable materials from ${base.path}.` : `Registered ${base.path} without reusable extraction output.`,
|
|
201
|
+
nodeIds: [`source:${base.path}`],
|
|
202
|
+
})
|
|
183
203
|
writeDecksState(workspaceDir, state)
|
|
184
204
|
}
|
|
185
205
|
|
package/lib/edit/resolve-deck.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readdirSync } from "fs"
|
|
2
2
|
import { relative, resolve, sep } from "path"
|
|
3
|
-
import { DECKS_STATE_FILE, isDeckHtmlPath, workspaceDeckSlug } from "../decks-state"
|
|
3
|
+
import { DECKS_STATE_FILE, hasDecksState, isDeckHtmlPath, readDecksState, workspaceDeckSlug } from "../decks-state"
|
|
4
|
+
import { resolveActiveHtmlDeckPath } from "../workspace-state/render-targets"
|
|
4
5
|
|
|
5
6
|
export interface EditableDeck {
|
|
6
7
|
slug: string
|
|
7
8
|
file: string
|
|
8
9
|
absoluteFile: string
|
|
9
|
-
source: "decks-state" | "fallback" | "file-path"
|
|
10
|
+
source: "render-target" | "decks-state" | "fallback" | "file-path"
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function resolveEditableDeck(workspaceRoot: string, input = ""): EditableDeck {
|
|
@@ -14,6 +15,16 @@ export function resolveEditableDeck(workspaceRoot: string, input = ""): Editable
|
|
|
14
15
|
throw new Error("/revela edit no longer accepts a target. It opens the only HTML deck in decks/.")
|
|
15
16
|
}
|
|
16
17
|
|
|
18
|
+
if (hasDecksState(workspaceRoot)) {
|
|
19
|
+
const state = readDecksState(workspaceRoot)
|
|
20
|
+
const deckPath = resolveActiveHtmlDeckPath(state)
|
|
21
|
+
const source = state.renderTargets.some((target) => target.type === "html_deck" && target.outputPath === deckPath) ? "render-target" : "decks-state"
|
|
22
|
+
if (deckPath && isDeckHtmlPath(deckPath)) {
|
|
23
|
+
const absoluteFile = resolve(workspaceRoot, deckPath)
|
|
24
|
+
if (existsSync(absoluteFile)) return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), deckPath, source)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
const htmlFiles = listDeckHtmlFiles(workspaceRoot)
|
|
18
29
|
if (htmlFiles.length === 0) {
|
|
19
30
|
throw new Error("No deck HTML found in decks/. Generate a deck first.")
|
package/lib/inspect/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"
|
|
@@ -30,6 +31,7 @@ export interface OpenInspectDeckOptions {
|
|
|
30
31
|
export function openInspectDeck(target: string, options: OpenInspectDeckOptions): OpenInspectDeckResult {
|
|
31
32
|
const deck = resolveEditableDeck(options.workspaceRoot, target)
|
|
32
33
|
const preflight = ensureEditableDeckState(options.workspaceRoot, deck)
|
|
34
|
+
assertDeckHtmlContractValid(options.workspaceRoot, deck.absoluteFile)
|
|
33
35
|
|
|
34
36
|
ctx.enabled = true
|
|
35
37
|
if (!existsSync(ACTIVE_PROMPT_FILE)) {
|
|
@@ -52,7 +54,7 @@ export function openInspectDeck(target: string, options: OpenInspectDeckOptions)
|
|
|
52
54
|
return {
|
|
53
55
|
deck,
|
|
54
56
|
url,
|
|
55
|
-
source: deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path",
|
|
57
|
+
source: deck.source === "render-target" ? "render target" : deck.source === "decks-state" ? "DECKS.json" : deck.source === "file-path" ? "file path" : "fallback path",
|
|
56
58
|
stateNote: preflight.changed ? "Deck state was prepared in DECKS.json for inspection." : "Deck state already points to this inspection target.",
|
|
57
59
|
preflightChanged: preflight.changed,
|
|
58
60
|
reusedSession: session.reused,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "crypto"
|
|
2
|
+
import type { NarrativeStateV1 } from "./types"
|
|
3
|
+
|
|
4
|
+
export function stableNarrativeId(seed: string): string {
|
|
5
|
+
return `narrative:${stableHash(seed || "workspace")}`
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function stableClaimId(text: string): string {
|
|
9
|
+
return `claim:${stableHash(text)}`
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function stableEvidenceId(claimId: string, seed: string): string {
|
|
13
|
+
return `evidence:${claimId}:${stableHash(seed)}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stableObjectionId(text: string): string {
|
|
17
|
+
return `objection:${stableHash(text)}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function stableRiskId(text: string): string {
|
|
21
|
+
return `risk:${stableHash(text)}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function computeNarrativeHash(narrative: NarrativeStateV1): string {
|
|
25
|
+
return stableHash(stableStringify({
|
|
26
|
+
version: narrative.version,
|
|
27
|
+
id: narrative.id,
|
|
28
|
+
audience: narrative.audience,
|
|
29
|
+
decision: narrative.decision,
|
|
30
|
+
thesis: narrative.thesis,
|
|
31
|
+
claims: narrative.claims,
|
|
32
|
+
evidenceBindings: narrative.evidenceBindings,
|
|
33
|
+
objections: narrative.objections,
|
|
34
|
+
risks: narrative.risks,
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function stableHash(input: unknown): string {
|
|
39
|
+
const text = typeof input === "string" ? input : stableStringify(input)
|
|
40
|
+
return createHash("sha1").update(text).digest("hex").slice(0, 12)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableStringify(value: unknown): string {
|
|
44
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`
|
|
45
|
+
if (value && typeof value === "object") {
|
|
46
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
47
|
+
.filter(([, item]) => item !== undefined)
|
|
48
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
49
|
+
return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`).join(",")}}`
|
|
50
|
+
}
|
|
51
|
+
return JSON.stringify(value)
|
|
52
|
+
}
|