@cyber-dash-tech/revela 0.10.0 → 0.11.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 +25 -5
- package/README.zh-CN.md +25 -5
- 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/deck-html/contract.ts +252 -0
- package/lib/decks-state.ts +101 -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/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 +426 -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 +119 -0
- package/package.json +1 -1
- package/plugin.ts +26 -1
- package/tools/decks.ts +54 -5
- package/tools/pdf.ts +9 -1
- package/tools/pptx.ts +10 -0
- package/tools/research-save.ts +15 -0
- package/tools/workspace-scan.ts +15 -0
package/lib/decks-state.ts
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
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
|
+
|
|
21
|
+
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
6
22
|
|
|
7
23
|
export type DeckProductionStatus = "planning" | "blocked" | "ready" | "written"
|
|
8
24
|
export type SlideProductionStatus = "planned" | "ready" | "written" | "qa_passed" | "qa_failed"
|
|
@@ -23,6 +39,9 @@ export interface DecksState {
|
|
|
23
39
|
openQuestions: string[]
|
|
24
40
|
}
|
|
25
41
|
decks: Record<string, DeckSpec>
|
|
42
|
+
actions: WorkspaceAction[]
|
|
43
|
+
renderTargets: RenderTarget[]
|
|
44
|
+
reviews: ReviewSnapshot[]
|
|
26
45
|
}
|
|
27
46
|
|
|
28
47
|
export interface SourceMaterial {
|
|
@@ -249,11 +268,11 @@ export interface ReviewDeckStateOptions {
|
|
|
249
268
|
}
|
|
250
269
|
|
|
251
270
|
export function decksStatePath(workspaceRoot: string): string {
|
|
252
|
-
return
|
|
271
|
+
return workspaceStatePath(workspaceRoot, DECKS_STATE_FILE)
|
|
253
272
|
}
|
|
254
273
|
|
|
255
274
|
export function hasDecksState(workspaceRoot: string): boolean {
|
|
256
|
-
return
|
|
275
|
+
return hasWorkspaceState(workspaceRoot, DECKS_STATE_FILE)
|
|
257
276
|
}
|
|
258
277
|
|
|
259
278
|
export function createEmptyDecksState(): DecksState {
|
|
@@ -266,6 +285,9 @@ export function createEmptyDecksState(): DecksState {
|
|
|
266
285
|
openQuestions: [],
|
|
267
286
|
},
|
|
268
287
|
decks: {},
|
|
288
|
+
actions: [],
|
|
289
|
+
renderTargets: [],
|
|
290
|
+
reviews: [],
|
|
269
291
|
}
|
|
270
292
|
}
|
|
271
293
|
|
|
@@ -289,6 +311,7 @@ export function normalizeWorkspaceDeckState(state: DecksState, workspaceRoot: st
|
|
|
289
311
|
delete normalized.decks[existingKey]
|
|
290
312
|
normalized.decks[slug] = deck
|
|
291
313
|
normalized.activeDeck = slug
|
|
314
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
292
315
|
return normalized
|
|
293
316
|
}
|
|
294
317
|
|
|
@@ -326,21 +349,15 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
|
|
|
326
349
|
}
|
|
327
350
|
|
|
328
351
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
329
|
-
|
|
330
|
-
return normalizeDecksState(parsed)
|
|
352
|
+
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
|
|
331
353
|
}
|
|
332
354
|
|
|
333
355
|
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")
|
|
356
|
+
writeWorkspaceState(workspaceRoot, state, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
|
|
337
357
|
}
|
|
338
358
|
|
|
339
359
|
export function readOrCreateDecksState(workspaceRoot: string): DecksState {
|
|
340
|
-
|
|
341
|
-
const state = createEmptyDecksState()
|
|
342
|
-
writeDecksState(workspaceRoot, state)
|
|
343
|
-
return state
|
|
360
|
+
return readOrCreateWorkspaceState(workspaceRoot, createEmptyDecksState, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksState })
|
|
344
361
|
}
|
|
345
362
|
|
|
346
363
|
export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug: string }): DecksState {
|
|
@@ -354,6 +371,7 @@ export function upsertDeck(state: DecksState, input: Partial<DeckSpec> & { slug:
|
|
|
354
371
|
const next = createDeckSpec({ ...existing, ...input, slug })
|
|
355
372
|
normalized.decks[slug] = next
|
|
356
373
|
normalized.activeDeck = slug
|
|
374
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
357
375
|
return normalized
|
|
358
376
|
}
|
|
359
377
|
|
|
@@ -370,6 +388,7 @@ export function upsertSlides(state: DecksState, slug: string, slides: SlideSpec[
|
|
|
370
388
|
deck.slides = [...byIndex.values()].sort((a, b) => a.index - b.index)
|
|
371
389
|
normalized.decks[key] = deck
|
|
372
390
|
normalized.activeDeck = key
|
|
391
|
+
ensureActiveHtmlDeckRenderTarget(normalized)
|
|
373
392
|
return normalized
|
|
374
393
|
}
|
|
375
394
|
|
|
@@ -459,26 +478,29 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
|
|
|
459
478
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
460
479
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
461
480
|
const evidenceCandidates = issues.flatMap((issue) => issue.evidenceCandidates ?? [])
|
|
481
|
+
const reviewedAt = new Date().toISOString()
|
|
462
482
|
deck.writeReadiness = {
|
|
463
483
|
status: blockers.length === 0 ? "ready" : "blocked",
|
|
464
484
|
blockers,
|
|
465
|
-
lastReviewedAt:
|
|
485
|
+
lastReviewedAt: reviewedAt,
|
|
466
486
|
}
|
|
467
487
|
deck.status = blockers.length === 0 ? "ready" : "blocked"
|
|
468
488
|
normalized.decks[deck.slug] = deck
|
|
469
489
|
normalized.activeDeck = deck.slug
|
|
490
|
+
const result: DeckStateReadinessResult = {
|
|
491
|
+
ready: blockers.length === 0,
|
|
492
|
+
slug: deck.slug,
|
|
493
|
+
status: deck.writeReadiness.status,
|
|
494
|
+
blocker: blockers.join("; "),
|
|
495
|
+
blockers,
|
|
496
|
+
warnings,
|
|
497
|
+
issues,
|
|
498
|
+
evidenceCandidates,
|
|
499
|
+
}
|
|
500
|
+
appendReviewSnapshot(normalized, createReviewSnapshot(normalized, { slug: deck.slug, result, reviewedAt }))
|
|
470
501
|
return {
|
|
471
502
|
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
|
-
},
|
|
503
|
+
result,
|
|
482
504
|
}
|
|
483
505
|
}
|
|
484
506
|
|
|
@@ -542,6 +564,38 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
542
564
|
suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
|
|
543
565
|
})
|
|
544
566
|
}
|
|
567
|
+
if (normalized.reviews.length > 0) {
|
|
568
|
+
const targetId = activeReviewTargetId(normalized)
|
|
569
|
+
const snapshot = latestReviewSnapshotForTarget(normalized, targetId)
|
|
570
|
+
if (!snapshot) {
|
|
571
|
+
const message = "No review snapshot exists for the active HTML render target"
|
|
572
|
+
blockers.unshift(message)
|
|
573
|
+
issues.unshift({
|
|
574
|
+
type: "missing_slide_spec",
|
|
575
|
+
severity: "blocker",
|
|
576
|
+
message,
|
|
577
|
+
suggestedAction: "Run /revela review so readiness is recorded against the current active render target.",
|
|
578
|
+
})
|
|
579
|
+
} else if (!isReviewSnapshotCurrent(normalized, snapshot, deck.slug)) {
|
|
580
|
+
const message = "Latest review snapshot is stale for the current deck, sources, evidence, narrative state, or render target"
|
|
581
|
+
blockers.unshift(message)
|
|
582
|
+
issues.unshift({
|
|
583
|
+
type: "missing_slide_spec",
|
|
584
|
+
severity: "blocker",
|
|
585
|
+
message,
|
|
586
|
+
suggestedAction: "Run /revela review again after the latest state changes before writing deck HTML.",
|
|
587
|
+
})
|
|
588
|
+
} else if (snapshot.status !== "ready") {
|
|
589
|
+
const message = `Latest review snapshot is ${snapshot.status}, not ready`
|
|
590
|
+
blockers.unshift(message)
|
|
591
|
+
issues.unshift({
|
|
592
|
+
type: "missing_slide_spec",
|
|
593
|
+
severity: "blocker",
|
|
594
|
+
message,
|
|
595
|
+
suggestedAction: "Resolve review blockers and rerun /revela review before writing deck HTML.",
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
}
|
|
545
599
|
|
|
546
600
|
return {
|
|
547
601
|
ready: blockers.length === 0,
|
|
@@ -579,11 +633,12 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
|
|
|
579
633
|
activeDeck: activeKey,
|
|
580
634
|
workspace: compactWorkspaceForPrompt(state.workspace),
|
|
581
635
|
deck: active ? compactDeckForPrompt(active) : undefined,
|
|
636
|
+
renderTargets: state.renderTargets,
|
|
637
|
+
reviews: compactReviewsForPrompt(state.reviews),
|
|
582
638
|
}
|
|
583
639
|
let text = JSON.stringify(compact, null, 2)
|
|
584
640
|
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.`
|
|
641
|
+
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
642
|
}
|
|
588
643
|
|
|
589
644
|
function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
|
|
@@ -616,6 +671,20 @@ function compactDeckForPrompt(deck: DeckSpec): DeckSpec {
|
|
|
616
671
|
}
|
|
617
672
|
}
|
|
618
673
|
|
|
674
|
+
function compactReviewsForPrompt(reviews: ReviewSnapshot[]): ReviewSnapshot[] {
|
|
675
|
+
return reviews.slice(-5).map((review) => ({
|
|
676
|
+
id: review.id,
|
|
677
|
+
targetId: review.targetId,
|
|
678
|
+
inputHash: review.inputHash,
|
|
679
|
+
status: review.status,
|
|
680
|
+
blockers: review.blockers.slice(0, 5),
|
|
681
|
+
warnings: review.warnings.slice(0, 5),
|
|
682
|
+
issues: review.issues.slice(0, 10),
|
|
683
|
+
evidenceCandidates: review.evidenceCandidates?.slice(0, 10),
|
|
684
|
+
reviewedAt: review.reviewedAt,
|
|
685
|
+
}))
|
|
686
|
+
}
|
|
687
|
+
|
|
619
688
|
function compactNarrativeBriefForPrompt(brief: NarrativeBrief | undefined): NarrativeBrief | undefined {
|
|
620
689
|
if (!brief) return undefined
|
|
621
690
|
return {
|
|
@@ -659,6 +728,9 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
659
728
|
openQuestions: input.workspace?.openQuestions ?? [],
|
|
660
729
|
},
|
|
661
730
|
decks: {},
|
|
731
|
+
actions: input.actions ?? [],
|
|
732
|
+
renderTargets: input.renderTargets ?? [],
|
|
733
|
+
reviews: input.reviews ?? [],
|
|
662
734
|
}
|
|
663
735
|
for (const [slug, deck] of Object.entries(input.decks ?? {})) {
|
|
664
736
|
const normalizedSlug = normalizeSlug(deck.slug || slug)
|
|
@@ -669,6 +741,7 @@ function normalizeDecksState(input: DecksState): DecksState {
|
|
|
669
741
|
const keys = Object.keys(state.decks)
|
|
670
742
|
if (keys.length === 1) state.activeDeck = keys[0]
|
|
671
743
|
}
|
|
744
|
+
ensureActiveHtmlDeckRenderTarget(state)
|
|
672
745
|
return state
|
|
673
746
|
}
|
|
674
747
|
|
|
@@ -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,
|
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
|
+
}
|