@cyber-dash-tech/revela 0.14.0 → 0.15.1
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 +65 -46
- package/README.zh-CN.md +65 -46
- package/designs/starter/DESIGN.md +168 -171
- package/designs/starter/preview.html +2 -2
- package/designs/summit/DESIGN.md +283 -129
- package/lib/command-intent.ts +59 -0
- package/lib/commands/brief.ts +1 -1
- package/lib/commands/designs.ts +1 -1
- package/lib/commands/domains.ts +1 -1
- package/lib/commands/edit.ts +2 -21
- package/lib/commands/enable.ts +6 -6
- package/lib/commands/help.ts +16 -8
- package/lib/commands/init.ts +1 -1
- package/lib/commands/narrative.ts +26 -0
- package/lib/commands/research.ts +66 -0
- package/lib/commands/review.ts +52 -15
- package/lib/decks-state.ts +127 -8
- package/lib/design/designs.ts +1 -2
- package/lib/edit/prompt.ts +6 -5
- package/lib/edit/resolve-deck.ts +1 -1
- package/lib/narrative-state/render-plan.ts +10 -1
- package/lib/qa/artifact.ts +77 -0
- package/lib/qa/checks.ts +100 -10
- package/lib/qa/index.ts +8 -6
- package/lib/qa/measure.ts +85 -0
- package/lib/refine/open.ts +21 -1
- package/lib/refine/server.ts +127 -4
- package/lib/workspace-state/types.ts +1 -0
- package/package.json +1 -1
- package/plugin.ts +283 -178
- package/skill/NARRATIVE_SKILL.md +103 -25
- package/skill/SKILL.md +6 -11
- package/tools/decks.ts +29 -3
- package/tools/narrative-view.ts +1 -1
- package/tools/qa.ts +17 -11
package/lib/decks-state.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./workspace-state/review-snapshots"
|
|
19
19
|
import { WORKSPACE_STATE_FILE, type RenderTarget, type ReviewSnapshot, type WorkspaceAction } from "./workspace-state/types"
|
|
20
20
|
import { normalizeCanonicalNarrativeState, normalizeNarrativeState } from "./narrative-state/normalize"
|
|
21
|
+
import { computeNarrativeHash } from "./narrative-state/hash"
|
|
21
22
|
import type { NarrativeStateV1 } from "./narrative-state/types"
|
|
22
23
|
|
|
23
24
|
export const DECKS_STATE_FILE = WORKSPACE_STATE_FILE
|
|
@@ -91,6 +92,7 @@ export interface DeckSpec {
|
|
|
91
92
|
researchPlan: ResearchAxis[]
|
|
92
93
|
slides: SlideSpec[]
|
|
93
94
|
assets: DeckAsset[]
|
|
95
|
+
planReview?: DeckPlanReview
|
|
94
96
|
writeReadiness: {
|
|
95
97
|
status: WriteReadinessStatus
|
|
96
98
|
blockers: string[]
|
|
@@ -98,6 +100,15 @@ export interface DeckSpec {
|
|
|
98
100
|
}
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
export interface DeckPlanReview {
|
|
104
|
+
status: "pending" | "confirmed"
|
|
105
|
+
narrativeHash: string
|
|
106
|
+
planHash: string
|
|
107
|
+
confirmedAt?: string
|
|
108
|
+
confirmedBy?: "user"
|
|
109
|
+
summary?: string
|
|
110
|
+
}
|
|
111
|
+
|
|
101
112
|
export interface NarrativeBrief {
|
|
102
113
|
audienceBeliefBefore?: string
|
|
103
114
|
audienceBeliefAfter?: string
|
|
@@ -202,6 +213,7 @@ export type ReadinessSeverity = "blocker" | "warning"
|
|
|
202
213
|
export type ReadinessIssueType =
|
|
203
214
|
| "missing_required_input"
|
|
204
215
|
| "missing_slide_spec"
|
|
216
|
+
| "slide_plan_unconfirmed"
|
|
205
217
|
| "research_not_ready"
|
|
206
218
|
| "missing_evidence"
|
|
207
219
|
| "weak_evidence"
|
|
@@ -278,6 +290,22 @@ const SOURCE_TRACE_ACTION = "Add slide evidence with source plus source trace su
|
|
|
278
290
|
|
|
279
291
|
export interface ReviewDeckStateOptions {
|
|
280
292
|
workspaceRoot?: string
|
|
293
|
+
narrativeHash?: string
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface ConfirmDeckPlanOptions {
|
|
297
|
+
approvedBy?: "user"
|
|
298
|
+
note?: string
|
|
299
|
+
now?: string
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export interface ConfirmDeckPlanResult {
|
|
303
|
+
confirmed: boolean
|
|
304
|
+
skipped: boolean
|
|
305
|
+
reason?: string
|
|
306
|
+
slug?: string
|
|
307
|
+
narrativeHash?: string
|
|
308
|
+
planHash?: string
|
|
281
309
|
}
|
|
282
310
|
|
|
283
311
|
export function decksStatePath(workspaceRoot: string): string {
|
|
@@ -357,10 +385,74 @@ export function createDeckSpec(input: Partial<DeckSpec> & { slug: string }): Dec
|
|
|
357
385
|
researchPlan: input.researchPlan ?? [],
|
|
358
386
|
slides: normalizeSlides(input.slides ?? []),
|
|
359
387
|
assets: input.assets ?? [],
|
|
388
|
+
planReview: normalizeDeckPlanReview(input.planReview),
|
|
360
389
|
writeReadiness: input.writeReadiness ?? { status: "blocked", blockers: [] },
|
|
361
390
|
}
|
|
362
391
|
}
|
|
363
392
|
|
|
393
|
+
export function deckPlanHash(slides: SlideSpec[]): string {
|
|
394
|
+
return createHash("sha1")
|
|
395
|
+
.update(JSON.stringify(normalizeSlides(slides).map((slide) => ({
|
|
396
|
+
index: slide.index,
|
|
397
|
+
title: slide.title,
|
|
398
|
+
purpose: slide.purpose,
|
|
399
|
+
narrativeRole: slide.narrativeRole,
|
|
400
|
+
layout: slide.layout,
|
|
401
|
+
components: slide.components,
|
|
402
|
+
claimIds: slide.claimIds ?? [],
|
|
403
|
+
claimRefs: slide.claimRefs ?? [],
|
|
404
|
+
evidenceBindingIds: slide.evidenceBindingIds ?? [],
|
|
405
|
+
content: slide.content,
|
|
406
|
+
evidence: slide.evidence,
|
|
407
|
+
visuals: slide.visuals ?? [],
|
|
408
|
+
}))))
|
|
409
|
+
.digest("hex")
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function currentDeckPlanReviewStatus(deck: DeckSpec, narrativeHash?: string): { current: boolean; stale: boolean; reason?: string; planHash: string } {
|
|
413
|
+
const planHash = deckPlanHash(deck.slides)
|
|
414
|
+
const review = deck.planReview
|
|
415
|
+
if (!review) return { current: false, stale: false, reason: "deck plan has not been shown and confirmed", planHash }
|
|
416
|
+
if (review.status !== "confirmed") return { current: false, stale: false, reason: "deck plan is pending user confirmation", planHash }
|
|
417
|
+
if (narrativeHash && review.narrativeHash !== narrativeHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the narrative hash changed", planHash }
|
|
418
|
+
if (review.planHash !== planHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the slide plan changed", planHash }
|
|
419
|
+
return { current: true, stale: false, planHash }
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptions = {}): { state: DecksState; result: ConfirmDeckPlanResult } {
|
|
423
|
+
const normalized = normalizeDecksStateWithNarrative(state)
|
|
424
|
+
const key = currentDeckKey(normalized)
|
|
425
|
+
const deck = key ? normalized.decks[key] : undefined
|
|
426
|
+
if (!deck) {
|
|
427
|
+
return { state: normalized, result: { confirmed: false, skipped: true, reason: `No active deck exists in ${DECKS_STATE_FILE}.` } }
|
|
428
|
+
}
|
|
429
|
+
if (deck.slides.length === 0) {
|
|
430
|
+
return { state: normalized, result: { confirmed: false, skipped: true, slug: deck.slug, reason: "Cannot confirm a deck plan with no slides." } }
|
|
431
|
+
}
|
|
432
|
+
const narrative = normalizeNarrativeState(normalized)
|
|
433
|
+
const narrativeHash = computeNarrativeHash(narrative)
|
|
434
|
+
const planHash = deckPlanHash(deck.slides)
|
|
435
|
+
const pending = deck.planReview
|
|
436
|
+
if (pending && pending.status === "pending" && (pending.narrativeHash !== narrativeHash || pending.planHash !== planHash)) {
|
|
437
|
+
return { state: normalized, result: { confirmed: false, skipped: true, slug: deck.slug, narrativeHash, planHash, reason: "Cannot confirm because the pending deck plan is stale. Re-run compileDeckPlan first." } }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
deck.planReview = {
|
|
441
|
+
status: "confirmed",
|
|
442
|
+
narrativeHash,
|
|
443
|
+
planHash,
|
|
444
|
+
confirmedAt: options.now ?? new Date().toISOString(),
|
|
445
|
+
confirmedBy: options.approvedBy ?? "user",
|
|
446
|
+
summary: cleanOptionalText(options.note),
|
|
447
|
+
}
|
|
448
|
+
deck.requiredInputs = { ...deck.requiredInputs, slidePlanConfirmed: true }
|
|
449
|
+
deck.writeReadiness = { status: "blocked", blockers: [] }
|
|
450
|
+
normalized.decks[deck.slug] = deck
|
|
451
|
+
normalized.activeDeck = deck.slug
|
|
452
|
+
normalized.narrative = narrative
|
|
453
|
+
return { state: normalized, result: { confirmed: true, skipped: false, slug: deck.slug, narrativeHash, planHash } }
|
|
454
|
+
}
|
|
455
|
+
|
|
364
456
|
export function readDecksState(workspaceRoot: string): DecksState {
|
|
365
457
|
return readWorkspaceState(workspaceRoot, { fileName: DECKS_STATE_FILE, normalize: normalizeDecksStateWithNarrative })
|
|
366
458
|
}
|
|
@@ -487,7 +579,10 @@ export function reviewDeckState(state: DecksState, slug?: string, options: Revie
|
|
|
487
579
|
}
|
|
488
580
|
}
|
|
489
581
|
|
|
490
|
-
const issues = computeDeckReadinessIssues(deck, normalized.workspace,
|
|
582
|
+
const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
|
|
583
|
+
...options,
|
|
584
|
+
narrativeHash: options.narrativeHash ?? computeNarrativeHash(normalizeNarrativeState(normalized)),
|
|
585
|
+
})
|
|
491
586
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
492
587
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
493
588
|
const evidenceCandidates = issues.flatMap((issue) => issue.evidenceCandidates ?? [])
|
|
@@ -544,7 +639,9 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
544
639
|
}
|
|
545
640
|
}
|
|
546
641
|
|
|
547
|
-
const issues = computeDeckReadinessIssues(deck, normalized.workspace
|
|
642
|
+
const issues = computeDeckReadinessIssues(deck, normalized.workspace, {
|
|
643
|
+
narrativeHash: computeNarrativeHash(normalizeNarrativeState(normalized)),
|
|
644
|
+
})
|
|
548
645
|
const blockers = issues.filter((issue) => issue.severity === "blocker").map((issue) => issue.message)
|
|
549
646
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
550
647
|
if (normalizeDeckPath(deck.outputPath) !== targetPath) {
|
|
@@ -564,7 +661,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
564
661
|
type: "missing_slide_spec",
|
|
565
662
|
severity: "blocker",
|
|
566
663
|
message,
|
|
567
|
-
suggestedAction: "Run /revela review and resolve all readiness blockers before writing deck HTML.",
|
|
664
|
+
suggestedAction: "Run /revela make deck --review and resolve all readiness blockers before writing deck HTML.",
|
|
568
665
|
})
|
|
569
666
|
}
|
|
570
667
|
if (deck.writeReadiness.blockers.length > 0) {
|
|
@@ -574,7 +671,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
574
671
|
type: "missing_slide_spec",
|
|
575
672
|
severity: "blocker",
|
|
576
673
|
message,
|
|
577
|
-
suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela review.",
|
|
674
|
+
suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela make deck --review.",
|
|
578
675
|
})
|
|
579
676
|
}
|
|
580
677
|
if (normalized.reviews.length > 0) {
|
|
@@ -587,7 +684,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
587
684
|
type: "missing_slide_spec",
|
|
588
685
|
severity: "blocker",
|
|
589
686
|
message,
|
|
590
|
-
suggestedAction: "Run /revela review so readiness is recorded against the current active render target.",
|
|
687
|
+
suggestedAction: "Run /revela make deck --review so readiness is recorded against the current active render target.",
|
|
591
688
|
})
|
|
592
689
|
} else if (!isReviewSnapshotCurrent(normalized, snapshot, deck.slug)) {
|
|
593
690
|
const message = "Latest review snapshot is stale for the current deck, sources, evidence, narrative state, or render target"
|
|
@@ -596,7 +693,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
596
693
|
type: "missing_slide_spec",
|
|
597
694
|
severity: "blocker",
|
|
598
695
|
message,
|
|
599
|
-
suggestedAction: "Run /revela review again after the latest state changes before writing deck HTML.",
|
|
696
|
+
suggestedAction: "Run /revela make deck --review again after the latest state changes before writing deck HTML.",
|
|
600
697
|
})
|
|
601
698
|
} else if (snapshot.status !== "ready") {
|
|
602
699
|
const message = `Latest review snapshot is ${snapshot.status}, not ready`
|
|
@@ -605,7 +702,7 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
605
702
|
type: "missing_slide_spec",
|
|
606
703
|
severity: "blocker",
|
|
607
704
|
message,
|
|
608
|
-
suggestedAction: "Resolve review blockers and rerun /revela review before writing deck HTML.",
|
|
705
|
+
suggestedAction: "Resolve review blockers and rerun /revela make deck --review before writing deck HTML.",
|
|
609
706
|
})
|
|
610
707
|
}
|
|
611
708
|
}
|
|
@@ -651,7 +748,7 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
|
|
|
651
748
|
}
|
|
652
749
|
let text = JSON.stringify(compact, null, 2)
|
|
653
750
|
if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
|
|
654
|
-
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
|
|
751
|
+
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 for the /revela make deck generation workflow, not a hard blocker for targeted artifact-level HTML fixes.\n- Do not edit ${DECKS_STATE_FILE} directly; use the revela-decks tool.\n- For /revela make deck generated HTML, use the current deck's outputPath and satisfy the deck HTML contract. For targeted artifact-level edits, patch the requested deck HTML directly without treating \`writeReadiness\` or \`planReview\` as a precondition.`
|
|
655
752
|
}
|
|
656
753
|
|
|
657
754
|
function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
|
|
@@ -765,6 +862,18 @@ function normalizeDecksStateWithNarrative(input: DecksState): DecksState {
|
|
|
765
862
|
return state
|
|
766
863
|
}
|
|
767
864
|
|
|
865
|
+
function normalizeDeckPlanReview(input: DeckPlanReview | undefined): DeckPlanReview | undefined {
|
|
866
|
+
if (!input || !input.narrativeHash || !input.planHash) return undefined
|
|
867
|
+
return {
|
|
868
|
+
status: input.status === "confirmed" ? "confirmed" : "pending",
|
|
869
|
+
narrativeHash: input.narrativeHash,
|
|
870
|
+
planHash: input.planHash,
|
|
871
|
+
confirmedAt: cleanOptionalText(input.confirmedAt),
|
|
872
|
+
confirmedBy: input.confirmedBy === "user" ? "user" : undefined,
|
|
873
|
+
summary: cleanOptionalText(input.summary),
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
768
877
|
function currentDeckKey(state: DecksState): string | undefined {
|
|
769
878
|
if (state.activeDeck && state.decks[state.activeDeck]) return state.activeDeck
|
|
770
879
|
const keys = Object.keys(state.decks)
|
|
@@ -800,6 +909,16 @@ function computeDeckReadinessIssues(deck: DeckSpec, workspace: DecksState["works
|
|
|
800
909
|
}
|
|
801
910
|
|
|
802
911
|
if (deck.slides.length === 0) issues.push(blockerIssue("missing_slide_spec", "slides are missing", "Add the confirmed slide plan through revela-decks upsertSlides."))
|
|
912
|
+
if (deck.slides.length > 0) {
|
|
913
|
+
const planReview = currentDeckPlanReviewStatus(deck, options.narrativeHash)
|
|
914
|
+
if (!planReview.current) {
|
|
915
|
+
issues.push(blockerIssue(
|
|
916
|
+
"slide_plan_unconfirmed",
|
|
917
|
+
planReview.stale ? `Deck slide plan confirmation is stale: ${planReview.reason}` : `Deck slide plan is not confirmed: ${planReview.reason}`,
|
|
918
|
+
"Show the compiled deck plan with low-fidelity layout sketches to the user, then call revela-decks confirmDeckPlan only after explicit user confirmation.",
|
|
919
|
+
))
|
|
920
|
+
}
|
|
921
|
+
}
|
|
803
922
|
for (const slide of deck.slides) {
|
|
804
923
|
const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
|
|
805
924
|
if (!slide.title.trim()) issues.push(blockerIssue("missing_slide_spec", `Slide ${slide.index} title is missing`, "Add a slide title to the slide spec.", slideRef))
|
package/lib/design/designs.ts
CHANGED
|
@@ -642,7 +642,6 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
642
642
|
"slide-canvas",
|
|
643
643
|
"visible",
|
|
644
644
|
"reveal",
|
|
645
|
-
"editable",
|
|
646
645
|
"page",
|
|
647
646
|
"bg",
|
|
648
647
|
"fg",
|
|
@@ -655,7 +654,7 @@ const UNIVERSAL_CLASSES = new Set([
|
|
|
655
654
|
* CSS class prefixes that are always exempt from compliance checks.
|
|
656
655
|
* Third-party libraries (icons, charts) generate classes with these prefixes.
|
|
657
656
|
*/
|
|
658
|
-
export const DEFAULT_PREFIX_EXEMPTIONS: string[] = ["lucide-", "echarts-"
|
|
657
|
+
export const DEFAULT_PREFIX_EXEMPTIONS: string[] = ["lucide-", "echarts-"]
|
|
659
658
|
|
|
660
659
|
export interface DesignClassVocabulary {
|
|
661
660
|
/** Complete set of allowed CSS class names. */
|
package/lib/edit/prompt.ts
CHANGED
|
@@ -72,14 +72,15 @@ Instructions:
|
|
|
72
72
|
- If there are multiple comments, apply them as one coherent edit pass and avoid changes from one comment overwriting another.
|
|
73
73
|
- Each comment may reference one or more selected elements. Treat the elements in a single comment as a group.
|
|
74
74
|
- Preserve the narrative boundary: if the requested edit changes audience framing, belief shift, decision/action, thesis, recommendation, claim wording, evidence scope, caveat, risk, objection, or decision ask, do not patch the HTML directly. Explain that the canonical narrative must be updated first through ${"`revela-decks`"} action ${"`upsertNarrative`"}, then reviewed/approved or explicitly overridden before updating the deck projection.
|
|
75
|
-
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, or deck HTML contract fixes may remain an artifact-level edit.
|
|
75
|
+
- Pure artifact polish such as layout, spacing, typography, alignment, color, image crop, animation, export fidelity, runtime JavaScript fixes, or deck HTML contract fixes may remain an artifact-level edit.
|
|
76
76
|
- If the request mixes content meaning and visual polish, treat it as narrative-impacting unless the user clarifies otherwise.
|
|
77
77
|
- Preserve the existing deck structure, active design language, typography, spacing system, animations, and slide count unless the comment explicitly asks otherwise.
|
|
78
78
|
- Do not rewrite unrelated slides or broad sections of the deck.
|
|
79
79
|
- Locate each target primarily with slideIndex, slideTitle, selected text, nearbyText, and outerHTMLExcerpt. Use selector/domPath as hints; they may be approximate.
|
|
80
|
-
-
|
|
81
|
-
-
|
|
82
|
-
-
|
|
83
|
-
-
|
|
80
|
+
- For targeted artifact-level edits, patch ${"`decks/*.html`"} directly. Do not call ${"`revela-decks`"} action ${"`review`"} as a precondition, and do not let ${"`writeReadiness`"}, ${"`planReview`"}, or ${"`slide_plan_unconfirmed`"} block the patch.
|
|
81
|
+
- Do not patch or write ${"`DECKS.json`"} directly. If state must change, use the ${"`revela-decks`"} tool.
|
|
82
|
+
- Apply the edit to ${payload.file} with the smallest targeted HTML patch that satisfies the comment.
|
|
83
|
+
- Artifact QA runs automatically after deck writes/patches/edits. It checks deck HTML contract, design component compliance, exact 1920x1080 slide geometry, scrollbars, element overflow, text clipping, and claim/evidence content-density warnings.
|
|
84
|
+
- If the tool result reports hard QA errors, fix them with the smallest targeted patch and let the post-write QA run again. Refine opens automatically only after hard errors pass; warnings such as thin claim/evidence substance do not block opening.
|
|
84
85
|
- If the comment is ambiguous, ask one concise clarification question instead of guessing.`
|
|
85
86
|
}
|
package/lib/edit/resolve-deck.ts
CHANGED
|
@@ -12,7 +12,7 @@ export interface EditableDeck {
|
|
|
12
12
|
|
|
13
13
|
export function resolveEditableDeck(workspaceRoot: string, input = ""): EditableDeck {
|
|
14
14
|
if (input.trim()) {
|
|
15
|
-
throw new Error("/revela
|
|
15
|
+
throw new Error("/revela refine does not accept a target. It opens the active HTML deck or the only HTML deck in decks/.")
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
if (hasDecksState(workspaceRoot)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec } from "../decks-state"
|
|
1
|
+
import { deckPlanHash, upsertDeck, upsertSlides, type DecksState, type EvidenceRef, type RequiredInputs, type SlideSpec } from "../decks-state"
|
|
2
2
|
import { ensureActiveHtmlDeckRenderTarget } from "../workspace-state/render-targets"
|
|
3
3
|
import { getClaimSlideRefs } from "./queries"
|
|
4
4
|
import { computeNarrativeHash } from "./hash"
|
|
@@ -65,6 +65,15 @@ export function compileDeckPlanFromNarrative(state: DecksState, options: Compile
|
|
|
65
65
|
writeReadiness: deck?.writeReadiness ?? { status: "blocked" as const, blockers: [] },
|
|
66
66
|
})
|
|
67
67
|
next = upsertSlides(next, slug, slides)
|
|
68
|
+
const plannedDeck = next.decks[slug]
|
|
69
|
+
plannedDeck.planReview = {
|
|
70
|
+
status: "pending",
|
|
71
|
+
narrativeHash,
|
|
72
|
+
planHash: deckPlanHash(plannedDeck.slides),
|
|
73
|
+
}
|
|
74
|
+
plannedDeck.requiredInputs = { ...plannedDeck.requiredInputs, slidePlanConfirmed: false }
|
|
75
|
+
plannedDeck.writeReadiness = { status: "blocked", blockers: [] }
|
|
76
|
+
next.decks[slug] = plannedDeck
|
|
68
77
|
next.narrative = { ...narrative, updatedAt: options.now ?? narrative.updatedAt }
|
|
69
78
|
const htmlTarget = ensureActiveHtmlDeckRenderTarget(next)
|
|
70
79
|
if (htmlTarget) {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { formatDeckHtmlContractReport, validateDeckHtmlContract } from "../deck-html/contract"
|
|
2
|
+
import type { DesignClassVocabulary } from "../design/designs"
|
|
3
|
+
import { formatReport, runQA } from "./index"
|
|
4
|
+
import { runComplianceQA } from "./compliance"
|
|
5
|
+
import type { QAReport } from "./checks"
|
|
6
|
+
|
|
7
|
+
export interface ArtifactQAReport {
|
|
8
|
+
file: string
|
|
9
|
+
passed: boolean
|
|
10
|
+
hardErrorCount: number
|
|
11
|
+
warningCount: number
|
|
12
|
+
sections: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function hardErrors(report: QAReport): number {
|
|
16
|
+
return report.slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "error").length, 0)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function warnings(report: QAReport): number {
|
|
20
|
+
return report.slides.reduce((sum, slide) => sum + slide.issues.filter((issue) => issue.severity === "warning").length, 0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runArtifactQA(input: {
|
|
24
|
+
workspaceRoot: string
|
|
25
|
+
filePath: string
|
|
26
|
+
vocabulary?: DesignClassVocabulary
|
|
27
|
+
}): Promise<ArtifactQAReport> {
|
|
28
|
+
const sections: string[] = []
|
|
29
|
+
let hardErrorCount = 0
|
|
30
|
+
let warningCount = 0
|
|
31
|
+
|
|
32
|
+
const contract = validateDeckHtmlContract(input.workspaceRoot, input.filePath)
|
|
33
|
+
if (contract.status === "invalid") {
|
|
34
|
+
hardErrorCount += contract.issues.filter((issue) => issue.severity === "error").length
|
|
35
|
+
warningCount += contract.warnings.length
|
|
36
|
+
sections.push("**[deck HTML contract]**\n\n" + formatDeckHtmlContractReport(contract))
|
|
37
|
+
} else if (contract.warnings.length > 0) {
|
|
38
|
+
warningCount += contract.warnings.length
|
|
39
|
+
sections.push("**[deck HTML contract]**\n\n" + formatDeckHtmlContractReport(contract))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const compliance = runComplianceQA(input.filePath, input.vocabulary)
|
|
43
|
+
const complianceErrors = hardErrors(compliance)
|
|
44
|
+
if (compliance.totalIssues > 0) {
|
|
45
|
+
hardErrorCount += complianceErrors
|
|
46
|
+
warningCount += warnings(compliance)
|
|
47
|
+
sections.push("**[component compliance]**\n\n" + formatReport(compliance))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const browser = await runQA(input.filePath)
|
|
52
|
+
const browserErrors = hardErrors(browser)
|
|
53
|
+
if (browser.totalIssues > 0) {
|
|
54
|
+
hardErrorCount += browserErrors
|
|
55
|
+
warningCount += warnings(browser)
|
|
56
|
+
sections.push("**[browser artifact QA]**\n\n" + formatReport(browser))
|
|
57
|
+
}
|
|
58
|
+
} catch (e) {
|
|
59
|
+
hardErrorCount += 1
|
|
60
|
+
sections.push("**[browser artifact QA]**\n\nError running browser QA: " + (e instanceof Error ? e.message : String(e)))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
file: input.filePath,
|
|
65
|
+
passed: hardErrorCount === 0,
|
|
66
|
+
hardErrorCount,
|
|
67
|
+
warningCount,
|
|
68
|
+
sections,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function formatArtifactQAReport(report: ArtifactQAReport): string {
|
|
73
|
+
const heading = report.passed ? "Artifact QA: PASSED" : "Artifact QA: FAILED"
|
|
74
|
+
const summary = `**File:** \`${report.file}\`\n\n**Hard errors:** ${report.hardErrorCount}\n**Warnings:** ${report.warningCount}`
|
|
75
|
+
if (report.sections.length === 0) return `## ${heading}\n\n${summary}\n\nAll artifact QA checks passed.`
|
|
76
|
+
return `## ${heading}\n\n${summary}\n\n${report.sections.join("\n\n---\n\n")}`
|
|
77
|
+
}
|
package/lib/qa/checks.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/checks.ts
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Browser-measured slide quality checks. The active default path checks hard
|
|
5
|
+
* artifact failures plus a content-substance warning; older soft visual
|
|
6
|
+
* heuristics are kept here for future opt-in use.
|
|
6
7
|
*
|
|
7
|
-
* Dimension 1:
|
|
8
|
-
* Dimension 2:
|
|
9
|
-
* Dimension 3:
|
|
8
|
+
* Dimension 1: Canvas — exact 1920x1080 slide/canvas size
|
|
9
|
+
* Dimension 2: Overflow — scrollbars, element overflow, and text clipping
|
|
10
|
+
* Dimension 3: Density — claim/evidence/source substance warnings
|
|
10
11
|
* Dimension 4: Compliance — CSS classes match the active design's vocabulary
|
|
11
12
|
*
|
|
12
13
|
* All checks operate on SlideMetrics produced by measure.ts.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
14
|
+
* Design component compliance requires an allowedClasses vocabulary from the
|
|
15
|
+
* design system and is run by combined artifact QA.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
18
|
import type { SlideMetrics, ElementInfo, Rect } from "./measure"
|
|
@@ -22,9 +23,10 @@ import { CANVAS_W, CANVAS_H } from "./measure"
|
|
|
22
23
|
export type IssueSeverity = "error" | "warning" | "info"
|
|
23
24
|
|
|
24
25
|
export interface LayoutIssue {
|
|
25
|
-
type: "overflow" | "balance" | "symmetry" | "rhythm" | "compliance"
|
|
26
|
+
type: "canvas" | "scrollbar" | "overflow" | "text_overflow" | "density" | "balance" | "symmetry" | "rhythm" | "compliance"
|
|
26
27
|
/** Sub-category within the dimension */
|
|
27
|
-
sub?: "
|
|
28
|
+
sub?: "size_mismatch" | "page_scroll" | "text_clipped" | "thin_content"
|
|
29
|
+
| "centroid_offset" | "bottom_gap" | "sparse"
|
|
28
30
|
| "height_mismatch" | "density_mismatch"
|
|
29
31
|
| "gap_variance"
|
|
30
32
|
| "unknown_class" | "novel_css_rule"
|
|
@@ -75,6 +77,9 @@ const T = {
|
|
|
75
77
|
GAP_MIN_MEAN: 10,
|
|
76
78
|
// Rhythm — min children count to check gap variance
|
|
77
79
|
GAP_MIN_CHILDREN: 3,
|
|
80
|
+
CANVAS_TOLERANCE: 1,
|
|
81
|
+
DENSITY_MIN_TEXT_POINTS: 70,
|
|
82
|
+
DENSITY_MIN_UNITS: 2,
|
|
78
83
|
}
|
|
79
84
|
|
|
80
85
|
// ── Geometry helpers ──────────────────────────────────────────────────────────
|
|
@@ -172,6 +177,42 @@ function collectLeaves(el: ElementInfo): ElementInfo[] {
|
|
|
172
177
|
|
|
173
178
|
// ── Dimension 1: Overflow ─────────────────────────────────────────────────────
|
|
174
179
|
|
|
180
|
+
function checkCanvas(metrics: SlideMetrics): LayoutIssue[] {
|
|
181
|
+
const issues: LayoutIssue[] = []
|
|
182
|
+
const tol = T.CANVAS_TOLERANCE
|
|
183
|
+
const canvasBad = Math.abs(metrics.canvasRect.width - CANVAS_W) > tol || Math.abs(metrics.canvasRect.height - CANVAS_H) > tol
|
|
184
|
+
const slideBad = Math.abs(metrics.slideRect.width - CANVAS_W) > tol || Math.abs(metrics.slideRect.height - CANVAS_H) > tol
|
|
185
|
+
|
|
186
|
+
if (canvasBad || slideBad) {
|
|
187
|
+
issues.push({
|
|
188
|
+
type: "canvas",
|
|
189
|
+
sub: "size_mismatch",
|
|
190
|
+
severity: "error",
|
|
191
|
+
detail: `Slide and canvas must render exactly ${CANVAS_W}x${CANVAS_H}px. Measured slide ${Math.round(metrics.slideRect.width)}x${Math.round(metrics.slideRect.height)}px, canvas ${Math.round(metrics.canvasRect.width)}x${Math.round(metrics.canvasRect.height)}px.`,
|
|
192
|
+
data: {
|
|
193
|
+
expectedWidth: CANVAS_W,
|
|
194
|
+
expectedHeight: CANVAS_H,
|
|
195
|
+
slideWidth: Math.round(metrics.slideRect.width),
|
|
196
|
+
slideHeight: Math.round(metrics.slideRect.height),
|
|
197
|
+
canvasWidth: Math.round(metrics.canvasRect.width),
|
|
198
|
+
canvasHeight: Math.round(metrics.canvasRect.height),
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return issues
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function checkScrollbars(metrics: SlideMetrics): LayoutIssue[] {
|
|
207
|
+
if (!metrics.hasScrollbars) return []
|
|
208
|
+
return [{
|
|
209
|
+
type: "scrollbar",
|
|
210
|
+
sub: "page_scroll",
|
|
211
|
+
severity: "error",
|
|
212
|
+
detail: "Rendered slide/page has scrollbars at 1920x1080. Deck slides must fit the fixed canvas without document, body, or slide scrolling.",
|
|
213
|
+
}]
|
|
214
|
+
}
|
|
215
|
+
|
|
175
216
|
/**
|
|
176
217
|
* Check 1: Overflow — elements extending beyond canvas boundaries.
|
|
177
218
|
* Hard correctness check; applies to all slide types.
|
|
@@ -205,6 +246,45 @@ function checkOverflow(metrics: SlideMetrics): LayoutIssue[] {
|
|
|
205
246
|
return issues
|
|
206
247
|
}
|
|
207
248
|
|
|
249
|
+
function checkTextOverflow(metrics: SlideMetrics): LayoutIssue[] {
|
|
250
|
+
const issues: LayoutIssue[] = []
|
|
251
|
+
|
|
252
|
+
function walk(els: ElementInfo[]) {
|
|
253
|
+
for (const el of els) {
|
|
254
|
+
if (!el.visible) continue
|
|
255
|
+
if (el.textOverflow) {
|
|
256
|
+
issues.push({
|
|
257
|
+
type: "text_overflow",
|
|
258
|
+
sub: "text_clipped",
|
|
259
|
+
severity: "error",
|
|
260
|
+
detail: `Text appears clipped inside \`${el.selector}\`${el.text ? `: "${el.text}"` : ""}. Increase container size, reduce copy, or adjust font/line-height.`,
|
|
261
|
+
data: { selector: el.selector, text: el.text ?? "" },
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
if (el.children.length > 0) walk(el.children)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
walk(metrics.elements)
|
|
269
|
+
return issues
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function checkContentDensity(metrics: SlideMetrics): LayoutIssue[] {
|
|
273
|
+
if (!metrics.slideQa) return []
|
|
274
|
+
const { bodyTextPoints, contentUnits, supportReferences } = metrics.contentStats
|
|
275
|
+
const thinText = bodyTextPoints < T.DENSITY_MIN_TEXT_POINTS
|
|
276
|
+
const thinUnits = contentUnits < T.DENSITY_MIN_UNITS
|
|
277
|
+
if (!thinText && !thinUnits) return []
|
|
278
|
+
|
|
279
|
+
return [{
|
|
280
|
+
type: "density",
|
|
281
|
+
sub: "thin_content",
|
|
282
|
+
severity: "warning",
|
|
283
|
+
detail: `Content slide may not have enough claim/evidence substance: ${bodyTextPoints} non-title text point(s), ${contentUnits} recognizable content unit(s), ${supportReferences} evidence/source/claim reference(s). Add concrete claim points, evidence, metrics, chart/table support, or source/caveat text if this is not a deliberate focus slide.`,
|
|
284
|
+
data: { bodyTextPoints, contentUnits, supportReferences },
|
|
285
|
+
}]
|
|
286
|
+
}
|
|
287
|
+
|
|
208
288
|
// ── Dimension 2: Balance ──────────────────────────────────────────────────────
|
|
209
289
|
|
|
210
290
|
/**
|
|
@@ -542,7 +622,13 @@ export function runChecks(
|
|
|
542
622
|
const slides: SlideReport[] = []
|
|
543
623
|
|
|
544
624
|
for (const metrics of allMetrics) {
|
|
545
|
-
const issues: LayoutIssue[] = [
|
|
625
|
+
const issues: LayoutIssue[] = [
|
|
626
|
+
...checkCanvas(metrics),
|
|
627
|
+
...checkScrollbars(metrics),
|
|
628
|
+
...checkOverflow(metrics),
|
|
629
|
+
...checkTextOverflow(metrics),
|
|
630
|
+
...checkContentDensity(metrics),
|
|
631
|
+
]
|
|
546
632
|
|
|
547
633
|
slides.push({ index: metrics.index, title: metrics.title, issues })
|
|
548
634
|
}
|
|
@@ -605,7 +691,11 @@ export function formatReport(report: QAReport): string {
|
|
|
605
691
|
`### Action Required`,
|
|
606
692
|
``,
|
|
607
693
|
`Please fix the above hard-error issues in the HTML file. For each issue type:`,
|
|
694
|
+
`- **canvas**: ensure each slide and .slide-canvas render exactly 1920x1080px, not merely any 16:9 size.`,
|
|
695
|
+
`- **scrollbar**: remove document/body/slide scrolling; content must fit inside the fixed 1920x1080 canvas.`,
|
|
608
696
|
`- **overflow**: reduce font size, padding, or content amount for the affected element.`,
|
|
697
|
+
`- **text_overflow**: increase the text container size, reduce copy, or adjust font/line-height so text is not clipped.`,
|
|
698
|
+
`- **density/thin_content**: add concrete claim/evidence points, metrics, chart/table support, or source/caveat text. This is a warning for content substance, not a blank-space failure.`,
|
|
609
699
|
`- **compliance/unknown_class**: an HTML element uses a CSS class not defined in the active design. Replace it with a class from the Component Index or Layout Index. Fetch the component/layout details with the \`revela-designs\` tool if needed.`,
|
|
610
700
|
`- **compliance/novel_css_rule**: \`<style>\` defines a CSS class that is not part of the active design. Remove the custom rule and use the design's existing component styles. For minor spacing/sizing adjustments, use inline \`style=""\` instead.`,
|
|
611
701
|
)
|
package/lib/qa/index.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* lib/qa/index.ts
|
|
3
3
|
*
|
|
4
|
-
* Public entry point for
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Public entry point for browser-rendered slide QA.
|
|
5
|
+
* Combined artifact QA, including contract and component compliance, lives in
|
|
6
|
+
* `lib/qa/artifact.ts`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { measureSlides } from "./measure"
|
|
@@ -18,12 +18,14 @@ export type { RunChecksOptions } from "./checks"
|
|
|
18
18
|
* Run hard-error QA on `htmlFilePath`.
|
|
19
19
|
*
|
|
20
20
|
* 1. Opens the file in headless Chrome (puppeteer-core)
|
|
21
|
-
* 2. Measures each .slide element's geometry
|
|
22
|
-
*
|
|
21
|
+
* 2. Measures each .slide element's geometry, scroll state, text clipping,
|
|
22
|
+
* content-density signals, and CSS class definitions
|
|
23
|
+
* 3. Runs browser QA checks for exact 1920x1080 slides, scrollbars, overflow,
|
|
24
|
+
* text clipping, and claim/evidence density warnings
|
|
23
25
|
* 4. Returns a structured QAReport
|
|
24
26
|
*
|
|
25
27
|
* The optional `vocabulary` argument is retained for backward compatibility;
|
|
26
|
-
* compliance is
|
|
28
|
+
* design compliance is handled by combined artifact QA.
|
|
27
29
|
*
|
|
28
30
|
* Throws if the file cannot be opened or Chrome is not found.
|
|
29
31
|
*/
|