@cyber-dash-tech/revela 0.17.1 → 0.17.3
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 +47 -565
- package/README.zh-CN.md +49 -533
- package/designs/monet/DESIGN.md +1 -1
- package/designs/starter/DESIGN.md +1 -1
- package/designs/summit/DESIGN.md +1 -1
- package/lib/commands/help.ts +6 -6
- package/lib/commands/init.ts +19 -9
- package/lib/commands/narrative.ts +40 -19
- package/lib/commands/pdf.ts +24 -15
- package/lib/commands/pptx.ts +2 -16
- package/lib/commands/research.ts +2 -0
- package/lib/commands/review.ts +79 -95
- package/lib/deck-html/contract.ts +26 -10
- package/lib/decks-state.ts +75 -86
- package/lib/edit/deck-state.ts +3 -111
- package/lib/edit/open.ts +2 -2
- package/lib/edit/resolve-deck.ts +14 -24
- package/lib/inspect/open.ts +2 -2
- package/lib/narrative-state/deck-plan-artifact.ts +584 -0
- package/lib/narrative-state/render-plan.ts +527 -38
- package/lib/narrative-state/research-gaps.ts +5 -2
- package/lib/narrative-vault/compile.ts +16 -1
- package/lib/narrative-vault/types.ts +4 -2
- package/lib/refine/open.ts +2 -2
- package/package.json +1 -1
- package/plugin.ts +2 -2
- package/skill/NARRATIVE_SKILL.md +23 -20
- package/skill/SKILL.md +95 -36
- package/tools/decks.ts +83 -51
- package/tools/narrative-view.ts +8 -10
package/lib/decks-state.ts
CHANGED
|
@@ -328,6 +328,8 @@ export interface ConfirmDeckPlanOptions {
|
|
|
328
328
|
approvedBy?: "user"
|
|
329
329
|
note?: string
|
|
330
330
|
now?: string
|
|
331
|
+
approvedAt?: string
|
|
332
|
+
planHash?: string
|
|
331
333
|
}
|
|
332
334
|
|
|
333
335
|
export interface ConfirmDeckPlanResult {
|
|
@@ -441,12 +443,12 @@ export function deckPlanHash(slides: SlideSpec[]): string {
|
|
|
441
443
|
}
|
|
442
444
|
|
|
443
445
|
export function currentDeckPlanReviewStatus(deck: DeckSpec, narrativeHash?: string): { current: boolean; stale: boolean; reason?: string; planHash: string } {
|
|
444
|
-
const planHash = deckPlanHash(deck.slides)
|
|
445
446
|
const review = deck.planReview
|
|
447
|
+
const planHash = deck.slides.length > 0 ? deckPlanHash(deck.slides) : review?.planHash ?? deckPlanHash(deck.slides)
|
|
446
448
|
if (!review) return { current: false, stale: false, reason: "deck plan has not been shown and confirmed", planHash }
|
|
447
449
|
if (review.status !== "confirmed") return { current: false, stale: false, reason: "deck plan is pending user confirmation", planHash }
|
|
448
450
|
if (narrativeHash && review.narrativeHash !== narrativeHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the narrative hash changed", planHash }
|
|
449
|
-
if (review.planHash !== planHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the slide
|
|
451
|
+
if (deck.slides.length > 0 && review.planHash !== planHash) return { current: false, stale: true, reason: "deck plan confirmation is stale because the cached slide projection changed", planHash }
|
|
450
452
|
return { current: true, stale: false, planHash }
|
|
451
453
|
}
|
|
452
454
|
|
|
@@ -457,14 +459,14 @@ export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptio
|
|
|
457
459
|
if (!deck) {
|
|
458
460
|
return { state: normalized, result: { confirmed: false, skipped: true, reason: `No active deck exists in ${DECKS_STATE_FILE}.` } }
|
|
459
461
|
}
|
|
460
|
-
if (deck.slides.length === 0) {
|
|
461
|
-
return { state: normalized, result: { confirmed: false, skipped: true, slug: deck.slug, reason: "Cannot confirm a deck plan with no slides." } }
|
|
462
|
-
}
|
|
463
462
|
const narrative = normalizeNarrativeState(normalized)
|
|
464
463
|
const narrativeHash = computeNarrativeHash(narrative)
|
|
465
|
-
const planHash = deckPlanHash(deck.slides)
|
|
464
|
+
const planHash = options.planHash ?? deckPlanHash(deck.slides)
|
|
466
465
|
const pending = deck.planReview
|
|
467
|
-
if (pending && pending.status === "pending" &&
|
|
466
|
+
if (pending && pending.status === "pending" && pending.narrativeHash !== narrativeHash) {
|
|
467
|
+
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." } }
|
|
468
|
+
}
|
|
469
|
+
if (!options.planHash && pending && pending.status === "pending" && pending.planHash !== planHash) {
|
|
468
470
|
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." } }
|
|
469
471
|
}
|
|
470
472
|
|
|
@@ -472,7 +474,7 @@ export function confirmDeckPlan(state: DecksState, options: ConfirmDeckPlanOptio
|
|
|
472
474
|
status: "confirmed",
|
|
473
475
|
narrativeHash,
|
|
474
476
|
planHash,
|
|
475
|
-
confirmedAt: options.now ?? new Date().toISOString(),
|
|
477
|
+
confirmedAt: options.approvedAt ?? options.now ?? new Date().toISOString(),
|
|
476
478
|
confirmedBy: options.approvedBy ?? "user",
|
|
477
479
|
summary: cleanOptionalText(options.note),
|
|
478
480
|
qualityChecks: pending?.qualityChecks,
|
|
@@ -676,20 +678,32 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
676
678
|
const targetPath = normalizeDeckPath(filePath)
|
|
677
679
|
const targetSlug = deckSlugFromPath(targetPath)
|
|
678
680
|
const normalized = normalizeDecksState(state)
|
|
681
|
+
if (!isDeckHtmlPath(targetPath)) {
|
|
682
|
+
const message = `Deck HTML writes must target decks/*.html, got ${filePath || "missing"}`
|
|
683
|
+
return {
|
|
684
|
+
ready: false,
|
|
685
|
+
slug: targetSlug,
|
|
686
|
+
blocker: message,
|
|
687
|
+
blockers: [message],
|
|
688
|
+
warnings: [],
|
|
689
|
+
issues: [blockerIssue("missing_slide_spec", message, "Write deck artifacts to a workspace-local decks/*.html path.")],
|
|
690
|
+
}
|
|
691
|
+
}
|
|
679
692
|
const key = currentDeckKey(normalized)
|
|
680
693
|
const deck = key ? normalized.decks[key] : undefined
|
|
681
694
|
if (!deck) {
|
|
695
|
+
const warning = currentDeckBlocker(normalized)
|
|
682
696
|
return {
|
|
683
|
-
ready:
|
|
697
|
+
ready: true,
|
|
684
698
|
slug: targetSlug,
|
|
685
|
-
blocker:
|
|
686
|
-
blockers: [
|
|
687
|
-
warnings: [],
|
|
699
|
+
blocker: "",
|
|
700
|
+
blockers: [],
|
|
701
|
+
warnings: [warning],
|
|
688
702
|
issues: [{
|
|
689
703
|
type: "missing_slide_spec",
|
|
690
|
-
severity: "
|
|
691
|
-
message:
|
|
692
|
-
suggestedAction: "
|
|
704
|
+
severity: "warning",
|
|
705
|
+
message: warning,
|
|
706
|
+
suggestedAction: "Proceed from the file-native deck-plan/ projection or explicit user request; DECKS.json deck records are not required for artifact writing.",
|
|
693
707
|
}],
|
|
694
708
|
}
|
|
695
709
|
}
|
|
@@ -701,32 +715,12 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
701
715
|
const warnings = issues.filter((issue) => issue.severity === "warning").map((issue) => issue.message)
|
|
702
716
|
if (normalizeDeckPath(deck.outputPath) !== targetPath) {
|
|
703
717
|
const message = `Deck outputPath is ${deck.outputPath || "missing"}, not ${targetPath}`
|
|
704
|
-
|
|
718
|
+
warnings.unshift(message)
|
|
705
719
|
issues.unshift({
|
|
706
720
|
type: "missing_slide_spec",
|
|
707
|
-
severity: "
|
|
721
|
+
severity: "warning",
|
|
708
722
|
message,
|
|
709
|
-
suggestedAction: "
|
|
710
|
-
})
|
|
711
|
-
}
|
|
712
|
-
if (deck.writeReadiness.status !== "ready") {
|
|
713
|
-
const message = `Deck writeReadiness is ${deck.writeReadiness.status || "missing"}, not ready`
|
|
714
|
-
blockers.unshift(message)
|
|
715
|
-
issues.unshift({
|
|
716
|
-
type: "missing_slide_spec",
|
|
717
|
-
severity: "blocker",
|
|
718
|
-
message,
|
|
719
|
-
suggestedAction: "Run /revela make --deck and resolve all readiness blockers before writing deck HTML.",
|
|
720
|
-
})
|
|
721
|
-
}
|
|
722
|
-
if (deck.writeReadiness.blockers.length > 0) {
|
|
723
|
-
const message = `Deck still has readiness blockers: ${deck.writeReadiness.blockers.join("; ")}`
|
|
724
|
-
blockers.unshift(message)
|
|
725
|
-
issues.unshift({
|
|
726
|
-
type: "missing_slide_spec",
|
|
727
|
-
severity: "blocker",
|
|
728
|
-
message,
|
|
729
|
-
suggestedAction: "Resolve the stored writeReadiness blockers and rerun /revela make --deck.",
|
|
723
|
+
suggestedAction: "Treat cached deck outputPath as diagnostic only; use the explicit artifact path requested by the user.",
|
|
730
724
|
})
|
|
731
725
|
}
|
|
732
726
|
if (normalized.reviews.length > 0) {
|
|
@@ -734,30 +728,30 @@ export function evaluateDeckStateWriteReadiness(state: DecksState, filePath: str
|
|
|
734
728
|
const snapshot = latestReviewSnapshotForTarget(normalized, targetId)
|
|
735
729
|
if (!snapshot) {
|
|
736
730
|
const message = "No review snapshot exists for the active HTML render target"
|
|
737
|
-
|
|
731
|
+
warnings.unshift(message)
|
|
738
732
|
issues.unshift({
|
|
739
733
|
type: "missing_slide_spec",
|
|
740
|
-
severity: "
|
|
734
|
+
severity: "warning",
|
|
741
735
|
message,
|
|
742
|
-
suggestedAction: "
|
|
736
|
+
suggestedAction: "Review snapshots are diagnostic only; proceed if the user explicitly requested this artifact path and HTML contract checks pass.",
|
|
743
737
|
})
|
|
744
738
|
} else if (!isReviewSnapshotCurrent(normalized, snapshot, deck.slug)) {
|
|
745
739
|
const message = "Latest review snapshot is stale for the current deck, sources, evidence, narrative state, or render target"
|
|
746
|
-
|
|
740
|
+
warnings.unshift(message)
|
|
747
741
|
issues.unshift({
|
|
748
742
|
type: "missing_slide_spec",
|
|
749
|
-
severity: "
|
|
743
|
+
severity: "warning",
|
|
750
744
|
message,
|
|
751
|
-
suggestedAction: "
|
|
745
|
+
suggestedAction: "Treat stale review snapshots as diagnostics; user decides whether to continue or refresh.",
|
|
752
746
|
})
|
|
753
747
|
} else if (snapshot.status !== "ready") {
|
|
754
748
|
const message = `Latest review snapshot is ${snapshot.status}, not ready`
|
|
755
|
-
|
|
749
|
+
warnings.unshift(message)
|
|
756
750
|
issues.unshift({
|
|
757
751
|
type: "missing_slide_spec",
|
|
758
|
-
severity: "
|
|
752
|
+
severity: "warning",
|
|
759
753
|
message,
|
|
760
|
-
suggestedAction: "
|
|
754
|
+
suggestedAction: "Treat prior review status as diagnostic only; current artifact validity checks decide whether writing can proceed.",
|
|
761
755
|
})
|
|
762
756
|
}
|
|
763
757
|
}
|
|
@@ -804,7 +798,7 @@ export function buildDecksStatePromptLayer(workspaceRoot: string, maxChars = 140
|
|
|
804
798
|
}
|
|
805
799
|
let text = JSON.stringify(compact, null, 2)
|
|
806
800
|
if (text.length > maxChars) text = text.slice(0, maxChars).trimEnd() + "\n[DECKS.json state truncated for prompt size.]"
|
|
807
|
-
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
|
|
801
|
+
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 compatibility/render state: workspace context, active output path, render targets, reviews, readiness, provenance, artifact coverage, and cached projections.\n- Do not treat ${DECKS_STATE_FILE} \`slides[]\` as the authoritative HTML slide-count, slide-order, or slide-content contract. When \`deck-plan/\` exists, use \`deck-plan/index.md\` and \`deck-plan/slides/*.md\` as the deck execution blueprint for HTML generation/remake.\n- The decks map is compatibility storage; operate only on the current workspace deck.\n- HTML slide identity is artifact self-consistency: each \`<section class="slide">\` needs a positive 1-based \`data-slide-index\`, indexes must be unique and strictly increase in DOM order, and 0-based \`data-index\` is never canonical identity. Cached ${DECKS_STATE_FILE} \`slides[].index\` values are diagnostic context only.\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\` and \`planReview\` are compatibility projections for the /revela make --deck generation workflow, not hard blockers 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, read \`deck-plan/\` when present, and satisfy the deck HTML contract without padding missing chapters just to match cached ${DECKS_STATE_FILE} \`slides[]\`. Deck-plan diagnostics are advisory; for targeted artifact-level edits, patch the requested deck HTML directly without treating \`writeReadiness\` or \`planReview\` as a precondition.`
|
|
808
802
|
}
|
|
809
803
|
|
|
810
804
|
function compactWorkspaceForPrompt(workspace: DecksState["workspace"]): DecksState["workspace"] {
|
|
@@ -969,52 +963,47 @@ function currentDeckBlocker(state: DecksState): string {
|
|
|
969
963
|
function computeDeckReadinessIssues(state: DecksState, deck: DeckSpec, options: ReviewDeckStateOptions = {}): ReadinessIssue[] {
|
|
970
964
|
const issues: ReadinessIssue[] = []
|
|
971
965
|
const workspace = state.workspace
|
|
972
|
-
if (!deck.goal.trim()) issues.push(
|
|
966
|
+
if (!deck.goal.trim()) issues.push(warningIssue("missing_slide_spec", "Deck goal is missing", "Clarify the deck goal if it matters for this artifact pass; do not block execution solely on cached deck metadata."))
|
|
973
967
|
if (!isDeckHtmlPath(deck.outputPath)) {
|
|
974
|
-
issues.push(
|
|
968
|
+
issues.push(warningIssue(
|
|
975
969
|
"missing_slide_spec",
|
|
976
970
|
`outputPath must be decks/*.html, got ${deck.outputPath || "missing"}`,
|
|
977
|
-
"
|
|
971
|
+
"Resolve output path from the user request, deck-plan/index.md, or a deterministic decks/*.html default instead of treating cached state as permission.",
|
|
978
972
|
))
|
|
979
973
|
}
|
|
980
974
|
|
|
981
975
|
for (const [key, value] of Object.entries(deck.requiredInputs) as Array<[keyof RequiredInputs, boolean]>) {
|
|
982
|
-
if (value !== true)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
))
|
|
988
|
-
}
|
|
976
|
+
if (value !== true) issues.push(warningIssue(
|
|
977
|
+
"missing_required_input",
|
|
978
|
+
`Legacy requiredInputs.${key} is not true`,
|
|
979
|
+
"requiredInputs is legacy diagnostic/cache state only; ask the user for missing intent if needed, but do not block execution.",
|
|
980
|
+
))
|
|
989
981
|
}
|
|
990
982
|
|
|
991
|
-
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
"Show the compiled deck plan with low-fidelity layout sketches to the user, then call revela-decks confirmDeckPlan only after explicit user confirmation.",
|
|
999
|
-
))
|
|
1000
|
-
}
|
|
983
|
+
const planReview = currentDeckPlanReviewStatus(deck, options.narrativeHash)
|
|
984
|
+
if (!planReview.current) {
|
|
985
|
+
issues.push(warningIssue(
|
|
986
|
+
"slide_plan_unconfirmed",
|
|
987
|
+
planReview.stale ? `Deck plan confirmation is stale: ${planReview.reason}` : `Deck plan is not confirmed: ${planReview.reason}`,
|
|
988
|
+
"Write or read deck-plan/ projection Markdown if useful, then decide whether to continue. This is advisory and does not block artifact work.",
|
|
989
|
+
))
|
|
1001
990
|
}
|
|
1002
991
|
issues.push(...deckPlanQualityIssues(deck))
|
|
1003
992
|
issues.push(...artifactCoverageIssues(state, deck))
|
|
1004
993
|
for (const slide of deck.slides) {
|
|
1005
994
|
const slideRef = { slideIndex: slide.index, slideTitle: slide.title }
|
|
1006
|
-
if (!slide.title.trim()) issues.push(
|
|
1007
|
-
if (!slide.layout.trim()) issues.push(
|
|
1008
|
-
if (slide.components.length === 0) issues.push(
|
|
1009
|
-
if (!hasSlideContent(slide)) issues.push(
|
|
995
|
+
if (!slide.title.trim()) issues.push(warningIssue("missing_slide_spec", `Cached slide ${slide.index} title is missing`, "Use deck-plan/ and artifact content as source for rendering; cached slide specs are diagnostics only.", slideRef))
|
|
996
|
+
if (!slide.layout.trim()) issues.push(warningIssue("missing_slide_spec", `Cached slide ${slide.index} layout is missing`, "Fetch needed design layouts before writing HTML, but do not block solely on cached slide specs.", slideRef))
|
|
997
|
+
if (slide.components.length === 0) issues.push(warningIssue("missing_slide_spec", `Cached slide ${slide.index} components are missing`, "Fetch needed design components before writing HTML, but do not block solely on cached slide specs.", slideRef))
|
|
998
|
+
if (!hasSlideContent(slide)) issues.push(warningIssue("missing_slide_spec", `Cached slide ${slide.index} content is missing`, "Use deck-plan/ and the artifact request as render guidance; cached slide specs are diagnostics only.", slideRef))
|
|
1010
999
|
|
|
1011
1000
|
const claim = findEvidenceSensitiveClaim(slide)
|
|
1012
1001
|
if (claim && slide.evidence.length === 0 && !isNavigationSlide(slide)) {
|
|
1013
1002
|
const { candidates: evidenceCandidates, search: evidenceCandidateSearch } = findEvidenceBindingCandidates(deck, slide, claim, options)
|
|
1014
|
-
issues.push(
|
|
1003
|
+
issues.push(warningIssue(
|
|
1015
1004
|
"missing_evidence",
|
|
1016
1005
|
`Slide ${slide.index} has an evidence-sensitive claim without evidence: ${claim}`,
|
|
1017
|
-
SOURCE_TRACE_ACTION
|
|
1006
|
+
`${SOURCE_TRACE_ACTION} Missing evidence is diagnostic; keep the boundary visible rather than blocking the user's requested artifact step.`,
|
|
1018
1007
|
{
|
|
1019
1008
|
...slideRef,
|
|
1020
1009
|
claimText: claim,
|
|
@@ -1036,10 +1025,10 @@ function computeDeckReadinessIssues(state: DecksState, deck: DeckSpec, options:
|
|
|
1036
1025
|
|
|
1037
1026
|
for (const axis of deck.researchPlan) {
|
|
1038
1027
|
if (axis.needed && axis.status !== "done" && axis.status !== "read" && axis.status !== "skipped") {
|
|
1039
|
-
issues.push(
|
|
1028
|
+
issues.push(warningIssue(
|
|
1040
1029
|
"research_not_ready",
|
|
1041
1030
|
`Research axis ${axis.axis || "unnamed"} is needed but ${axis.status}`,
|
|
1042
|
-
"
|
|
1031
|
+
"Research status is diagnostic only; user decides whether to continue, run research, or keep the gap visible.",
|
|
1043
1032
|
))
|
|
1044
1033
|
}
|
|
1045
1034
|
}
|
|
@@ -1050,10 +1039,10 @@ function computeDeckReadinessIssues(state: DecksState, deck: DeckSpec, options:
|
|
|
1050
1039
|
if (isIgnorableSourceMaterial(material.path)) continue
|
|
1051
1040
|
const message = `Source material ${material.path} has been identified but not extracted, summarized, or researched`
|
|
1052
1041
|
if (hasNeededResearch) {
|
|
1053
|
-
issues.push(
|
|
1042
|
+
issues.push(warningIssue(
|
|
1054
1043
|
"source_not_processed",
|
|
1055
1044
|
message,
|
|
1056
|
-
"Extract, summarize, research, or
|
|
1045
|
+
"Extract, summarize, research, or exclude this source if it matters; do not block solely on source processing state.",
|
|
1057
1046
|
))
|
|
1058
1047
|
} else {
|
|
1059
1048
|
issues.push(warningIssue(
|
|
@@ -1072,11 +1061,11 @@ function deckPlanQualityIssues(deck: DeckSpec): ReadinessIssue[] {
|
|
|
1072
1061
|
return checks.flatMap((check): ReadinessIssue[] => {
|
|
1073
1062
|
if (check.status === "pass") return []
|
|
1074
1063
|
const suggestedAction = check.status === "blocker"
|
|
1075
|
-
? "
|
|
1064
|
+
? "Revise deck-plan/ if this quality issue matters for the user's goal. It is diagnostic, not a workflow permission blocker."
|
|
1076
1065
|
: "Keep the stated claim boundaries visible in the plan and rendered artifact; do not stretch partial evidence beyond the supported scope."
|
|
1077
1066
|
return [{
|
|
1078
1067
|
type: "plan_quality",
|
|
1079
|
-
severity:
|
|
1068
|
+
severity: "warning",
|
|
1080
1069
|
message: check.message,
|
|
1081
1070
|
suggestedAction,
|
|
1082
1071
|
}]
|
|
@@ -1088,17 +1077,17 @@ function artifactCoverageIssues(state: DecksState, deck: DeckSpec): ReadinessIss
|
|
|
1088
1077
|
if (!coverage) return []
|
|
1089
1078
|
const issues: ReadinessIssue[] = []
|
|
1090
1079
|
if (coverage.missingClaimIds.length > 0) {
|
|
1091
|
-
issues.push(
|
|
1080
|
+
issues.push(warningIssue(
|
|
1092
1081
|
"artifact_coverage",
|
|
1093
1082
|
`Active deck plan is missing required narrative claims: ${coverage.missingClaimIds.join(", ")}`,
|
|
1094
|
-
"
|
|
1083
|
+
"Coverage gaps are diagnostic; revise deck-plan/ or proceed with the visible limitation if the user chooses.",
|
|
1095
1084
|
))
|
|
1096
1085
|
}
|
|
1097
1086
|
if (coverage.coverageStatus === "stale") {
|
|
1098
|
-
issues.push(
|
|
1087
|
+
issues.push(warningIssue(
|
|
1099
1088
|
"artifact_coverage",
|
|
1100
1089
|
`Active deck artifact coverage is stale: ${coverage.staleReasons.join("; ") || "narrative or render target changed"}`,
|
|
1101
|
-
"
|
|
1090
|
+
"Artifact coverage staleness is diagnostic; user decides whether to remake, review, or export the current artifact.",
|
|
1102
1091
|
))
|
|
1103
1092
|
} else if (coverage.coverageStatus === "partial") {
|
|
1104
1093
|
issues.push(warningIssue(
|
|
@@ -1147,8 +1136,8 @@ function readinessNextActions(issues: ReadinessIssue[], coverage?: ArtifactCover
|
|
|
1147
1136
|
const actions = issues
|
|
1148
1137
|
.filter((issue) => issue.severity === "blocker" || issue.type === "plan_quality" || issue.type === "artifact_coverage")
|
|
1149
1138
|
.map((issue) => issue.suggestedAction)
|
|
1150
|
-
if (coverage?.missingClaimIds.length) actions.unshift("Review missingClaimIds in artifactCoverage and
|
|
1151
|
-
if (coverage?.coverageStatus === "stale") actions.unshift("
|
|
1139
|
+
if (coverage?.missingClaimIds.length) actions.unshift("Review missingClaimIds in artifactCoverage and decide whether to revise deck-plan/ or continue with the current artifact.")
|
|
1140
|
+
if (coverage?.coverageStatus === "stale") actions.unshift("Artifact coverage is stale; decide whether to remake, review, or export the current artifact.")
|
|
1152
1141
|
return [...new Set(actions)].slice(0, 5)
|
|
1153
1142
|
}
|
|
1154
1143
|
|
package/lib/edit/deck-state.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
import { readFileSync } from "fs"
|
|
2
|
-
import { activeDesign } from "../design/designs"
|
|
3
|
-
import { activeDomain } from "../domain/domains"
|
|
4
|
-
import {
|
|
5
|
-
defaultRequiredInputs,
|
|
6
|
-
DECKS_STATE_FILE,
|
|
7
|
-
normalizeWorkspaceDeckState,
|
|
8
|
-
readOrCreateDecksState,
|
|
9
|
-
upsertDeck,
|
|
10
|
-
upsertSlides,
|
|
11
|
-
writeDecksState,
|
|
12
|
-
type SlideSpec,
|
|
13
|
-
} from "../decks-state"
|
|
14
1
|
import type { EditableDeck } from "./resolve-deck"
|
|
15
2
|
|
|
16
3
|
export interface EditDeckStatePreflightResult {
|
|
@@ -18,102 +5,7 @@ export interface EditDeckStatePreflightResult {
|
|
|
18
5
|
}
|
|
19
6
|
|
|
20
7
|
export function ensureEditableDeckState(workspaceRoot: string, deck: EditableDeck): EditDeckStatePreflightResult {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
throw new Error(`${DECKS_STATE_FILE} already points to ${active.outputPath}. Revela 0.8 expects one deck per workspace; move extra decks to a separate workspace.`)
|
|
25
|
-
}
|
|
26
|
-
const existing = state.decks[deck.slug]
|
|
27
|
-
let changed = !existing || existing.outputPath !== deck.file
|
|
28
|
-
|
|
29
|
-
state = upsertDeck(state, {
|
|
30
|
-
...existing,
|
|
31
|
-
slug: deck.slug,
|
|
32
|
-
goal: existing?.goal || `Edit existing Revela deck ${deck.slug}.`,
|
|
33
|
-
audience: existing?.audience || "Existing deck viewers",
|
|
34
|
-
language: existing?.language || "en",
|
|
35
|
-
outputPath: deck.file,
|
|
36
|
-
theme: {
|
|
37
|
-
design: existing?.theme?.design || safeActiveDesign(),
|
|
38
|
-
domain: existing?.theme?.domain || safeActiveDomain(),
|
|
39
|
-
},
|
|
40
|
-
requiredInputs: defaultRequiredInputs({
|
|
41
|
-
...existing?.requiredInputs,
|
|
42
|
-
topicClarified: true,
|
|
43
|
-
audienceClarified: true,
|
|
44
|
-
languageDecided: true,
|
|
45
|
-
visualStyleSelected: true,
|
|
46
|
-
sourceMaterialsIdentified: true,
|
|
47
|
-
researchNeedAssessed: true,
|
|
48
|
-
researchFindingsRead: true,
|
|
49
|
-
slidePlanConfirmed: true,
|
|
50
|
-
designLayoutsFetched: true,
|
|
51
|
-
}),
|
|
52
|
-
researchPlan: existing?.researchPlan || [],
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
const current = state.decks[deck.slug]
|
|
56
|
-
if (current.slides.length === 0) {
|
|
57
|
-
state = upsertSlides(state, deck.slug, inferSlides(deck.absoluteFile))
|
|
58
|
-
changed = true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
writeDecksState(workspaceRoot, state)
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
changed,
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function inferSlides(filePath: string): SlideSpec[] {
|
|
69
|
-
const html = readFileSync(filePath, "utf-8")
|
|
70
|
-
const chunks = html.match(/<section\b[\s\S]*?<\/section>/gi) || [html]
|
|
71
|
-
return chunks.map((chunk, index) => {
|
|
72
|
-
const title = extractTitle(chunk) || `Slide ${index + 1}`
|
|
73
|
-
return {
|
|
74
|
-
index: index + 1,
|
|
75
|
-
title,
|
|
76
|
-
purpose: "Existing HTML slide prepared for targeted visual edits.",
|
|
77
|
-
layout: "existing-html",
|
|
78
|
-
qa: /slide-qa=["']true["']/i.test(chunk),
|
|
79
|
-
components: ["existing-html"],
|
|
80
|
-
content: {
|
|
81
|
-
headline: title,
|
|
82
|
-
body: [extractText(chunk) || "Existing HTML slide content."],
|
|
83
|
-
},
|
|
84
|
-
evidence: [],
|
|
85
|
-
visuals: [],
|
|
86
|
-
status: "ready",
|
|
87
|
-
notes: "Inferred automatically by /revela review --deck preflight.",
|
|
88
|
-
}
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function extractTitle(html: string): string {
|
|
93
|
-
const match = /<(?:h1|h2|h3|title)\b[^>]*>([\s\S]*?)<\/(?:h1|h2|h3|title)>/i.exec(html)
|
|
94
|
-
return normalizeText(match?.[1] || "").slice(0, 160)
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function extractText(html: string): string {
|
|
98
|
-
return normalizeText(html.replace(/<script\b[\s\S]*?<\/script>/gi, " ").replace(/<style\b[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ")).slice(0, 600)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function normalizeText(value: string): string {
|
|
102
|
-
return value.replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\s+/g, " ").trim()
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function safeActiveDesign(): string {
|
|
106
|
-
try {
|
|
107
|
-
return activeDesign()
|
|
108
|
-
} catch {
|
|
109
|
-
return "aurora"
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function safeActiveDomain(): string {
|
|
114
|
-
try {
|
|
115
|
-
return activeDomain()
|
|
116
|
-
} catch {
|
|
117
|
-
return "general"
|
|
118
|
-
}
|
|
8
|
+
void workspaceRoot
|
|
9
|
+
void deck
|
|
10
|
+
return { changed: false }
|
|
119
11
|
}
|
package/lib/edit/open.ts
CHANGED
|
@@ -85,8 +85,8 @@ function openEditableDeckInternal(
|
|
|
85
85
|
const shouldOpen = options.openBrowser !== false && !(behavior.skipLiveSession && session.live)
|
|
86
86
|
if (shouldOpen) (options.openUrl ?? openUrl)(url)
|
|
87
87
|
|
|
88
|
-
const source = deck.source === "
|
|
89
|
-
const stateNote = preflight.changed ? "Deck
|
|
88
|
+
const source = deck.source === "file-path" ? "file path" : "discovered deck file"
|
|
89
|
+
const stateNote = preflight.changed ? "Deck file preflight updated runtime state." : "Deck visual edit uses the selected HTML artifact directly."
|
|
90
90
|
|
|
91
91
|
return {
|
|
92
92
|
deck,
|
package/lib/edit/resolve-deck.ts
CHANGED
|
@@ -1,39 +1,27 @@
|
|
|
1
|
-
import { existsSync, readdirSync } from "fs"
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "fs"
|
|
2
2
|
import { relative, resolve, sep } from "path"
|
|
3
|
-
import {
|
|
4
|
-
import { resolveActiveHtmlDeckPath } from "../workspace-state/render-targets"
|
|
3
|
+
import { isDeckHtmlPath, workspaceDeckSlug } from "../decks-state"
|
|
5
4
|
|
|
6
5
|
export interface EditableDeck {
|
|
7
6
|
slug: string
|
|
8
7
|
file: string
|
|
9
8
|
absoluteFile: string
|
|
10
|
-
source: "
|
|
9
|
+
source: "discovered" | "file-path"
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function resolveEditableDeck(workspaceRoot: string, input = ""): EditableDeck {
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
}
|
|
13
|
+
const explicit = input.trim()
|
|
14
|
+
if (explicit) return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), explicit, "file-path")
|
|
27
15
|
|
|
28
16
|
const htmlFiles = listDeckHtmlFiles(workspaceRoot)
|
|
29
17
|
if (htmlFiles.length === 0) {
|
|
30
|
-
throw new Error("No deck HTML found in decks/.
|
|
18
|
+
throw new Error("No deck HTML found in decks/. Pass a deck path or generate a deck first.")
|
|
31
19
|
}
|
|
32
20
|
if (htmlFiles.length > 1) {
|
|
33
|
-
throw new Error(
|
|
21
|
+
throw new Error(`Multiple deck HTML files found in decks/: ${htmlFiles.join(", ")}. Pass the deck path explicitly.`)
|
|
34
22
|
}
|
|
35
23
|
|
|
36
|
-
return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), htmlFiles[0], "
|
|
24
|
+
return resolveDeckFile(workspaceRoot, workspaceDeckSlug(workspaceRoot), htmlFiles[0], "discovered")
|
|
37
25
|
}
|
|
38
26
|
|
|
39
27
|
function listDeckHtmlFiles(workspaceRoot: string): string[] {
|
|
@@ -51,18 +39,20 @@ function resolveDeckFile(
|
|
|
51
39
|
file: string,
|
|
52
40
|
source: EditableDeck["source"],
|
|
53
41
|
): EditableDeck {
|
|
54
|
-
if (!isDeckHtmlPath(file)) {
|
|
55
|
-
throw new Error(`${DECKS_STATE_FILE} deck outputPath must be decks/*.html, got ${file || "missing"}.`)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
42
|
const root = resolve(workspaceRoot)
|
|
59
43
|
const absoluteFile = resolve(root, file)
|
|
60
44
|
if (!isInside(root, absoluteFile)) {
|
|
61
45
|
throw new Error(`Resolved deck file is outside the workspace: ${file}`)
|
|
62
46
|
}
|
|
47
|
+
if (!isDeckHtmlPath(workspaceRelative(root, absoluteFile)) && !/\.html?$/i.test(absoluteFile)) {
|
|
48
|
+
throw new Error(`Deck path must be an HTML file: ${file || "missing"}.`)
|
|
49
|
+
}
|
|
63
50
|
if (!existsSync(absoluteFile)) {
|
|
64
51
|
throw new Error(`Deck HTML not found: ${workspaceRelative(root, absoluteFile)}`)
|
|
65
52
|
}
|
|
53
|
+
if (!statSync(absoluteFile).isFile()) {
|
|
54
|
+
throw new Error(`Deck path is not a file: ${workspaceRelative(root, absoluteFile)}`)
|
|
55
|
+
}
|
|
66
56
|
|
|
67
57
|
return {
|
|
68
58
|
slug,
|
package/lib/inspect/open.ts
CHANGED
|
@@ -54,8 +54,8 @@ export function openInspectDeck(target: string, options: OpenInspectDeckOptions)
|
|
|
54
54
|
return {
|
|
55
55
|
deck,
|
|
56
56
|
url,
|
|
57
|
-
source: deck.source === "
|
|
58
|
-
stateNote: preflight.changed ? "Deck
|
|
57
|
+
source: deck.source === "file-path" ? "file path" : "discovered deck file",
|
|
58
|
+
stateNote: preflight.changed ? "Deck file preflight updated runtime state." : "Deck inspection uses the selected HTML artifact directly.",
|
|
59
59
|
preflightChanged: preflight.changed,
|
|
60
60
|
reusedSession: session.reused,
|
|
61
61
|
openedBrowser: shouldOpen,
|