@cyber-dash-tech/revela 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -28
- package/README.zh-CN.md +54 -28
- package/lib/commands/designs.ts +2 -2
- package/lib/commands/domains.ts +2 -2
- package/lib/commands/enable.ts +19 -19
- package/lib/commands/help.ts +5 -3
- package/lib/commands/init.ts +30 -19
- package/lib/commands/inspect.ts +1 -1
- package/lib/commands/pdf.ts +33 -5
- package/lib/commands/pptx.ts +14 -9
- package/lib/commands/refine.ts +1 -1
- package/lib/commands/review.ts +115 -1
- package/lib/deck-html/contract.ts +252 -0
- package/lib/decks-state.ts +111 -28
- package/lib/document-materials/extract.ts +20 -0
- package/lib/edit/resolve-deck.ts +13 -2
- package/lib/inspect/open.ts +3 -1
- package/lib/narrative-state/hash.ts +52 -0
- package/lib/narrative-state/normalize.ts +307 -0
- package/lib/narrative-state/project-compat.ts +14 -0
- package/lib/narrative-state/readiness.ts +289 -0
- package/lib/narrative-state/render-plan.ts +207 -0
- package/lib/narrative-state/types.ts +139 -0
- package/lib/prompt-builder.ts +59 -26
- package/lib/qa/export-gate.ts +8 -1
- package/lib/refine/open.ts +3 -1
- package/lib/workspace-state/actions.ts +71 -0
- package/lib/workspace-state/compat.ts +10 -0
- package/lib/workspace-state/evidence-status.ts +267 -0
- package/lib/workspace-state/graph.ts +544 -0
- package/lib/workspace-state/render-targets.ts +182 -0
- package/lib/workspace-state/rendered-artifacts.ts +43 -0
- package/lib/workspace-state/repository.ts +43 -0
- package/lib/workspace-state/research-attachments.ts +130 -0
- package/lib/workspace-state/review-snapshots.ts +127 -0
- package/lib/workspace-state/types.ts +122 -0
- package/package.json +1 -1
- package/plugin.ts +53 -3
- package/skill/NARRATIVE_SKILL.md +64 -0
- package/tools/decks.ts +233 -6
- package/tools/pdf.ts +9 -1
- package/tools/pptx.ts +10 -0
- package/tools/research-save.ts +15 -0
- package/tools/workspace-scan.ts +29 -1
package/lib/commands/pptx.ts
CHANGED
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
import { existsSync, readdirSync } from "fs"
|
|
11
11
|
import { relative, resolve, sep } from "path"
|
|
12
12
|
import { hasDecksState, isDeckHtmlPath, readDecksState } from "../decks-state"
|
|
13
|
+
import { assertDeckHtmlContractValid } from "../deck-html/contract"
|
|
13
14
|
import { exportToPptx } from "../pptx/export"
|
|
15
|
+
import { recordRenderedArtifact } from "../workspace-state/rendered-artifacts"
|
|
16
|
+
import { resolveActiveHtmlDeckPath } from "../workspace-state/render-targets"
|
|
14
17
|
|
|
15
18
|
export interface PptxArgs {
|
|
16
19
|
filePath: string
|
|
@@ -20,7 +23,7 @@ export interface PptxArgs {
|
|
|
20
23
|
export interface ResolvedPptxDeck {
|
|
21
24
|
file: string
|
|
22
25
|
absoluteFile: string
|
|
23
|
-
source: "decks-state" | "fallback" | "file-path"
|
|
26
|
+
source: "render-target" | "decks-state" | "fallback" | "file-path"
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
function formatSecs(ms: number): string {
|
|
@@ -46,12 +49,12 @@ export function resolvePptxDeck(workspaceRoot: string, filePath = ""): ResolvedP
|
|
|
46
49
|
|
|
47
50
|
if (hasDecksState(root)) {
|
|
48
51
|
const state = readDecksState(root)
|
|
49
|
-
const
|
|
50
|
-
const outputPath = key ? state.decks[key]?.outputPath : undefined
|
|
52
|
+
const outputPath = resolveActiveHtmlDeckPath(state)
|
|
51
53
|
if (outputPath && isDeckHtmlPath(outputPath)) {
|
|
52
54
|
const absoluteFile = resolve(root, outputPath)
|
|
53
55
|
if (existsSync(absoluteFile)) {
|
|
54
|
-
|
|
56
|
+
const source = state.renderTargets.some((target) => target.type === "html_deck" && target.outputPath === outputPath) ? "render-target" : "decks-state"
|
|
57
|
+
return { file: workspaceRelative(root, absoluteFile), absoluteFile, source }
|
|
55
58
|
}
|
|
56
59
|
}
|
|
57
60
|
}
|
|
@@ -114,6 +117,7 @@ export async function handlePptx(
|
|
|
114
117
|
const deck = resolvePptxDeck(workspaceRoot, args.filePath)
|
|
115
118
|
const abs = deck.absoluteFile
|
|
116
119
|
|
|
120
|
+
assertDeckHtmlContractValid(workspaceRoot, abs)
|
|
117
121
|
await send(`Exporting \`${abs}\` to PPTX...`)
|
|
118
122
|
let lastSlideUpdate = 0
|
|
119
123
|
let longDeckThreshold: number | null = null
|
|
@@ -141,6 +145,12 @@ export async function handlePptx(
|
|
|
141
145
|
await send(`Editable export progress: slide ${current}/${total}`)
|
|
142
146
|
},
|
|
143
147
|
})
|
|
148
|
+
recordRenderedArtifact(workspaceRoot, {
|
|
149
|
+
sourceHtmlPath: deck.file,
|
|
150
|
+
outputPath: result.outputPath,
|
|
151
|
+
type: "pptx",
|
|
152
|
+
actor: "revela-pptx",
|
|
153
|
+
})
|
|
144
154
|
|
|
145
155
|
await send(
|
|
146
156
|
`**PPTX exported successfully**\n\n` +
|
|
@@ -159,11 +169,6 @@ export async function handlePptx(
|
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
function singleDeckKey(decks: Record<string, unknown>): string | undefined {
|
|
163
|
-
const keys = Object.keys(decks)
|
|
164
|
-
return keys.length === 1 ? keys[0] : undefined
|
|
165
|
-
}
|
|
166
|
-
|
|
167
172
|
function listDeckHtmlFiles(workspaceRoot: string): string[] {
|
|
168
173
|
const dir = resolve(workspaceRoot, "decks")
|
|
169
174
|
if (!existsSync(dir)) return []
|
package/lib/commands/refine.ts
CHANGED
|
@@ -14,7 +14,7 @@ export async function handleRefine(
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
await send(
|
|
17
|
-
`Opened Revela Refine for the
|
|
17
|
+
`Opened Revela Refine for the active HTML deck.\n` +
|
|
18
18
|
`File: \`${result.deck.file}\`\n` +
|
|
19
19
|
`${result.stateNote}\n` +
|
|
20
20
|
`URL: ${result.url}\n\n` +
|
package/lib/commands/review.ts
CHANGED
|
@@ -6,15 +6,129 @@ export function buildReviewPrompt({
|
|
|
6
6
|
}: {
|
|
7
7
|
exists: boolean
|
|
8
8
|
workspaceRoot?: string
|
|
9
|
+
}): string {
|
|
10
|
+
const state = exists
|
|
11
|
+
? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
|
|
12
|
+
: `${DECKS_STATE_FILE} does not exist yet. Create or normalize it through the revela-decks tool only if there is enough workspace narrative context.`
|
|
13
|
+
|
|
14
|
+
return `Review Revela narrative readiness.
|
|
15
|
+
|
|
16
|
+
Goal:
|
|
17
|
+
- Use ${DECKS_STATE_FILE} as the compatibility workspace-state file, but review the canonical narrative state first: audience, belief shift, decision/action, thesis, central claims, evidence boundaries, objections, risks, and approval state.
|
|
18
|
+
- Treat this as a narrative readiness review, not a deck HTML write-readiness review.
|
|
19
|
+
- Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
|
|
20
|
+
- Call \`revela-decks\` action \`reviewNarrative\` as the authoritative deterministic readiness engine.
|
|
21
|
+
- Do not call \`revela-decks\` action \`review\` here. That action is the deck/artifact gate and belongs to \`/revela deck --review\`.
|
|
22
|
+
- Do not treat legacy \`writeReadiness.status\`, old review snapshots, or an existing HTML deck as narrative approval.
|
|
23
|
+
- Do not write or overwrite \`decks/*.html\` during narrative review.
|
|
24
|
+
- If the narrative is \`ready_for_approval\`, ask whether the user wants to approve it or revise it. Do not approve automatically.
|
|
25
|
+
- Only call \`revela-decks\` action \`approveNarrative\` when the user explicitly asks to approve or override.
|
|
26
|
+
|
|
27
|
+
Current state:
|
|
28
|
+
- ${state}
|
|
29
|
+
${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
|
|
30
|
+
|
|
31
|
+
Workspace boundary rules:
|
|
32
|
+
- Stay strictly inside the current workspace root for every scan, glob, read, and write.
|
|
33
|
+
- Do not search parent directories, home directories, or unrelated absolute directories.
|
|
34
|
+
- Do not use \`~\`, \`..\`, or parent-directory traversal to discover files.
|
|
35
|
+
- For Glob/file searches, use the current workspace as the search root. Do not set the search root to a parent directory or home directory.
|
|
36
|
+
|
|
37
|
+
Workflow:
|
|
38
|
+
1. Call \`revela-decks\` with action \`read\` to inspect the current workspace state.
|
|
39
|
+
2. If ${DECKS_STATE_FILE} is missing or empty, do not invent a deck plan, slide count, design, output path, or visual style. Report the smallest narrative inputs needed, usually audience, belief-before, belief-after, decision/action, thesis, central claims, evidence availability, objections, and risks.
|
|
40
|
+
3. If legacy deck state exists, let the tool-normalized canonical narrative derived from \`narrativeBrief\`, slide roles, slide content, and slide evidence be reviewed. Do not assume old deck readiness means approval.
|
|
41
|
+
4. Call \`revela-decks\` action \`reviewNarrative\`. Use its returned \`status\`, \`blockers\`, \`warnings\`, \`issues\`, \`narrativeHash\`, \`approval\`, and \`nextActions\` as authoritative.
|
|
42
|
+
5. If research findings have been saved but not attached or evidence-bound, report them as unattached research state, not proof.
|
|
43
|
+
6. If central claims lack required evidence, report the named claim and the exact next action: attach findings, bind evidence, run targeted research, narrow unsupported scope, or rewrite the claim.
|
|
44
|
+
7. If approval is missing or stale, clearly distinguish \`ready_for_approval\`, \`approved\`, and render override.
|
|
45
|
+
|
|
46
|
+
Report format:
|
|
47
|
+
- Start with \`Narrative readiness: <status>\`.
|
|
48
|
+
- Include \`Narrative hash: <hash>\` when returned.
|
|
49
|
+
- If blocked or needs research, list each blocker with issue type, claim text when available, and suggested next action.
|
|
50
|
+
- If warnings exist, list them after blockers as residual risks.
|
|
51
|
+
- If approval is missing, ask whether the user wants to approve the narrative or revise it.
|
|
52
|
+
- If approval is stale, say the prior approval no longer matches the current narrative hash.
|
|
53
|
+
- Keep deck/artifact readiness separate. If the user wants to review slide-writing readiness, tell them to run \`/revela deck --review\`.
|
|
54
|
+
|
|
55
|
+
Rules:
|
|
56
|
+
- Do not write or overwrite \`decks/*.html\` during narrative review.
|
|
57
|
+
- Do not call \`revela-decks review\` during narrative review.
|
|
58
|
+
- Do not apply evidence candidates, bind evidence, or rewrite slide text unless the user explicitly asks.
|
|
59
|
+
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
60
|
+
- Do not add inferred user preferences to long-term preference state.
|
|
61
|
+
|
|
62
|
+
Start now by reading ${DECKS_STATE_FILE} through \`revela-decks\`, then call \`revela-decks\` action \`reviewNarrative\`.`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function buildDeckPrompt({
|
|
66
|
+
exists,
|
|
67
|
+
workspaceRoot,
|
|
68
|
+
}: {
|
|
69
|
+
exists: boolean
|
|
70
|
+
workspaceRoot?: string
|
|
71
|
+
}): string {
|
|
72
|
+
const state = exists
|
|
73
|
+
? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
|
|
74
|
+
: `${DECKS_STATE_FILE} does not exist yet. Do not invent a deck; initialize narrative state first with /revela init.`
|
|
75
|
+
|
|
76
|
+
return `Begin Revela deck render handoff.
|
|
77
|
+
|
|
78
|
+
Goal:
|
|
79
|
+
- Treat this as the explicit transition from approved narrative state to deck render planning.
|
|
80
|
+
- Use the deck-render prompt mode for design, layout, component, HTML, QA, and deck artifact rules.
|
|
81
|
+
- Do not write or overwrite \`decks/*.html\` until the narrative handoff and deck/artifact gate are both satisfied.
|
|
82
|
+
- Do not treat legacy \`writeReadiness.status\`, old review snapshots, or existing HTML decks as narrative approval.
|
|
83
|
+
- Do not bypass the deck HTML contract, review snapshot freshness, source-trace expectations, or export preflight protections.
|
|
84
|
+
|
|
85
|
+
Current state:
|
|
86
|
+
- ${state}
|
|
87
|
+
${workspaceRoot ? `- Current workspace root: \`${workspaceRoot}\`` : ""}
|
|
88
|
+
|
|
89
|
+
Workflow:
|
|
90
|
+
1. Call \`revela-decks\` action \`read\`.
|
|
91
|
+
2. Call \`revela-decks\` action \`reviewNarrative\` before planning deck slides.
|
|
92
|
+
3. If narrative readiness is \`approved\`, continue. If it is \`ready_for_approval\`, ask the user for explicit approval before continuing. If it is blocked, stale, or needs research, stop and report the smallest next action. Do not call \`approveNarrative\` unless the user explicitly approves or requests a render override.
|
|
93
|
+
4. After approval or explicit render override exists, call \`revela-decks\` action \`compileDeckPlan\`. This projects canonical narrative claims and evidence bindings into compatibility \`slides[]\` and \`slides[].evidence[]\`; it must not write HTML.
|
|
94
|
+
5. If \`compileDeckPlan\` returns \`skipped\`, stop and report the reason. Do not invent slide specs manually to bypass approval.
|
|
95
|
+
6. Ask for or confirm visual design only after the narrative deck plan exists. Fetch required design layouts/components with \`revela-designs read\` as needed.
|
|
96
|
+
7. Update only deck/artifact metadata through \`revela-decks upsertDeck\` / \`upsertSlides\` when required by confirmed design/layout choices. Do not change canonical narrative claims unless the user asks to revise the narrative.
|
|
97
|
+
8. Call \`revela-decks\` action \`review\` as the artifact gate. It computes \`writeReadiness\` and review snapshots for deck HTML writing.
|
|
98
|
+
9. Write \`decks/*.html\` only if the deck/artifact gate is ready and all deck HTML contract requirements can be satisfied. If not ready, report blockers and stop.
|
|
99
|
+
|
|
100
|
+
Report format before any HTML write:
|
|
101
|
+
- Start with \`Deck handoff: <status>\`.
|
|
102
|
+
- Include narrative readiness status and narrative hash when available.
|
|
103
|
+
- Include whether \`compileDeckPlan\` compiled or skipped.
|
|
104
|
+
- If deck/artifact review is blocked, list blockers separately from narrative blockers.
|
|
105
|
+
- If proceeding to HTML writing, state which approved narrative hash and deck review snapshot authorized the artifact work.
|
|
106
|
+
|
|
107
|
+
Rules:
|
|
108
|
+
- \`compileDeckPlan\` is the canonical narrative-to-deck planning path. Do not manually invent slide specs to avoid it.
|
|
109
|
+
- Deck slide specs are render-target projections. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, risks, and approval.
|
|
110
|
+
- Applying evidence candidates, rewriting canonical claims, or approving narratives requires explicit user instruction.
|
|
111
|
+
- Do not store secrets, credentials, tokens, or sensitive personal information.
|
|
112
|
+
|
|
113
|
+
Start now by reading ${DECKS_STATE_FILE}, reviewing narrative readiness, and then compiling the deck plan only if approval or explicit render override is current.`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function buildDeckReviewPrompt({
|
|
117
|
+
exists,
|
|
118
|
+
workspaceRoot,
|
|
119
|
+
}: {
|
|
120
|
+
exists: boolean
|
|
121
|
+
workspaceRoot?: string
|
|
9
122
|
}): string {
|
|
10
123
|
const state = exists
|
|
11
124
|
? `${DECKS_STATE_FILE} exists. Read it through the revela-decks tool.`
|
|
12
125
|
: `${DECKS_STATE_FILE} does not exist yet. Create it through the revela-decks tool if there is enough deck context.`
|
|
13
126
|
|
|
14
|
-
return `Review Revela deck write readiness.
|
|
127
|
+
return `Review Revela deck/artifact write readiness.
|
|
15
128
|
|
|
16
129
|
Goal:
|
|
17
130
|
- Use ${DECKS_STATE_FILE} as the source of truth for whether the current workspace deck is ready to be written to \`decks/*.html\`.
|
|
131
|
+
- Treat this as an artifact gate for deck rendering, not strategic narrative approval. Narrative readiness is reviewed by \`/revela review\`.
|
|
18
132
|
- Preserve the deck spec for future sessions: every slide's content, layout, components, evidence, visuals, production status, and the 0.9 narrative compiler brief when available.
|
|
19
133
|
- Do not write, patch, or directly edit ${DECKS_STATE_FILE}. Use the \`revela-decks\` tool for all state changes.
|
|
20
134
|
- Let \`revela-decks\` action \`review\` compute writeReadiness; do not manually set readiness to ready.
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs"
|
|
2
|
+
import { relative, resolve, sep } from "path"
|
|
3
|
+
import { hasDecksState, readDecksState, type DeckSpec } from "../decks-state"
|
|
4
|
+
import { resolveActiveHtmlDeckPath, normalizeWorkspacePath } from "../workspace-state/render-targets"
|
|
5
|
+
|
|
6
|
+
export type DeckHtmlContractStatus = "valid" | "invalid" | "skipped"
|
|
7
|
+
|
|
8
|
+
export type DeckHtmlContractIssueType =
|
|
9
|
+
| "file_not_found"
|
|
10
|
+
| "no_matching_deck_spec"
|
|
11
|
+
| "missing_slide_section"
|
|
12
|
+
| "slide_count_mismatch"
|
|
13
|
+
| "missing_data_slide_index"
|
|
14
|
+
| "invalid_data_slide_index"
|
|
15
|
+
| "duplicate_data_slide_index"
|
|
16
|
+
| "slide_index_mismatch"
|
|
17
|
+
| "legacy_data_index_noncanonical"
|
|
18
|
+
|
|
19
|
+
export interface DeckHtmlContractIssue {
|
|
20
|
+
type: DeckHtmlContractIssueType
|
|
21
|
+
severity: "error" | "warning"
|
|
22
|
+
message: string
|
|
23
|
+
slidePosition?: number
|
|
24
|
+
expectedIndex?: number
|
|
25
|
+
actualIndex?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DeckHtmlContractReport {
|
|
29
|
+
status: DeckHtmlContractStatus
|
|
30
|
+
ok: boolean
|
|
31
|
+
workspaceRoot: string
|
|
32
|
+
filePath: string
|
|
33
|
+
deckSlug?: string
|
|
34
|
+
activeHtmlPath?: string
|
|
35
|
+
expectedIndexes: number[]
|
|
36
|
+
actualIndexes: number[]
|
|
37
|
+
issues: DeckHtmlContractIssue[]
|
|
38
|
+
warnings: DeckHtmlContractIssue[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SlideSectionAttrs {
|
|
42
|
+
position: number
|
|
43
|
+
dataSlideIndex?: string
|
|
44
|
+
dataIndex?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function validateDeckHtmlContract(workspaceRoot: string, filePath: string): DeckHtmlContractReport {
|
|
48
|
+
const root = resolve(workspaceRoot)
|
|
49
|
+
const absoluteFile = resolve(root, filePath)
|
|
50
|
+
const relativeFile = workspaceRelative(root, absoluteFile)
|
|
51
|
+
const base: Omit<DeckHtmlContractReport, "status" | "ok"> = {
|
|
52
|
+
workspaceRoot: root,
|
|
53
|
+
filePath: relativeFile,
|
|
54
|
+
expectedIndexes: [],
|
|
55
|
+
actualIndexes: [],
|
|
56
|
+
issues: [],
|
|
57
|
+
warnings: [],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!hasDecksState(root)) return skipped(base, "No DECKS.json exists; deck HTML contract validation is skipped.")
|
|
61
|
+
|
|
62
|
+
const state = readDecksState(root)
|
|
63
|
+
const activeHtmlPath = resolveActiveHtmlDeckPath(state)
|
|
64
|
+
const activeKey = state.activeDeck || singleDeckKey(state.decks)
|
|
65
|
+
const deck = activeKey ? state.decks[activeKey] : undefined
|
|
66
|
+
const normalizedActive = normalizeWorkspacePath(activeHtmlPath ?? "")
|
|
67
|
+
const normalizedTarget = normalizeWorkspacePath(relativeFile)
|
|
68
|
+
|
|
69
|
+
base.activeHtmlPath = normalizedActive || undefined
|
|
70
|
+
base.deckSlug = deck?.slug
|
|
71
|
+
|
|
72
|
+
if (!deck || !normalizedActive || normalizedActive !== normalizedTarget) {
|
|
73
|
+
return skipped(base, `No matching active deck spec exists for ${relativeFile}; deck HTML contract validation is skipped.`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!existsSync(absoluteFile)) {
|
|
77
|
+
base.issues.push({
|
|
78
|
+
type: "file_not_found",
|
|
79
|
+
severity: "error",
|
|
80
|
+
message: `Deck HTML file not found: ${relativeFile}`,
|
|
81
|
+
})
|
|
82
|
+
return finalize(base)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const html = readFileSync(absoluteFile, "utf-8")
|
|
86
|
+
const sections = extractSlideSections(html)
|
|
87
|
+
base.expectedIndexes = expectedSlideIndexes(deck)
|
|
88
|
+
|
|
89
|
+
if (sections.length === 0) {
|
|
90
|
+
base.issues.push({
|
|
91
|
+
type: "missing_slide_section",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: "Deck HTML must contain one <section class=\"slide\"> element per slide spec.",
|
|
94
|
+
})
|
|
95
|
+
return finalize(base)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (sections.length !== base.expectedIndexes.length) {
|
|
99
|
+
base.issues.push({
|
|
100
|
+
type: "slide_count_mismatch",
|
|
101
|
+
severity: "error",
|
|
102
|
+
message: `Deck HTML has ${sections.length} slide sections, but DECKS.json expects ${base.expectedIndexes.length}.`,
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const seen = new Set<number>()
|
|
107
|
+
sections.forEach((section, offset) => {
|
|
108
|
+
const expectedIndex = base.expectedIndexes[offset]
|
|
109
|
+
if (section.dataIndex !== undefined) {
|
|
110
|
+
base.warnings.push({
|
|
111
|
+
type: "legacy_data_index_noncanonical",
|
|
112
|
+
severity: "warning",
|
|
113
|
+
message: `Slide ${section.position} has legacy data-index; use data-slide-index as the canonical slide identity.`,
|
|
114
|
+
slidePosition: section.position,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (section.dataSlideIndex === undefined) {
|
|
119
|
+
base.issues.push({
|
|
120
|
+
type: "missing_data_slide_index",
|
|
121
|
+
severity: "error",
|
|
122
|
+
message: `Slide ${section.position} is missing data-slide-index.`,
|
|
123
|
+
slidePosition: section.position,
|
|
124
|
+
expectedIndex,
|
|
125
|
+
})
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const actualIndex = Number(section.dataSlideIndex)
|
|
130
|
+
if (!Number.isInteger(actualIndex) || actualIndex < 1 || String(actualIndex) !== section.dataSlideIndex.trim()) {
|
|
131
|
+
base.issues.push({
|
|
132
|
+
type: "invalid_data_slide_index",
|
|
133
|
+
severity: "error",
|
|
134
|
+
message: `Slide ${section.position} has invalid data-slide-index=${JSON.stringify(section.dataSlideIndex)}; expected a positive 1-based integer.`,
|
|
135
|
+
slidePosition: section.position,
|
|
136
|
+
expectedIndex,
|
|
137
|
+
})
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
base.actualIndexes.push(actualIndex)
|
|
142
|
+
if (seen.has(actualIndex)) {
|
|
143
|
+
base.issues.push({
|
|
144
|
+
type: "duplicate_data_slide_index",
|
|
145
|
+
severity: "error",
|
|
146
|
+
message: `Slide ${section.position} repeats data-slide-index=${actualIndex}.`,
|
|
147
|
+
slidePosition: section.position,
|
|
148
|
+
actualIndex,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
seen.add(actualIndex)
|
|
152
|
+
|
|
153
|
+
if (expectedIndex !== undefined && actualIndex !== expectedIndex) {
|
|
154
|
+
base.issues.push({
|
|
155
|
+
type: "slide_index_mismatch",
|
|
156
|
+
severity: "error",
|
|
157
|
+
message: `Slide ${section.position} has data-slide-index=${actualIndex}, but DECKS.json expects ${expectedIndex}.`,
|
|
158
|
+
slidePosition: section.position,
|
|
159
|
+
expectedIndex,
|
|
160
|
+
actualIndex,
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return finalize(base)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function assertDeckHtmlContractValid(workspaceRoot: string, filePath: string): void {
|
|
169
|
+
const report = validateDeckHtmlContract(workspaceRoot, filePath)
|
|
170
|
+
if (report.status !== "invalid") return
|
|
171
|
+
throw new Error(
|
|
172
|
+
"Deck HTML contract validation failed. Fix slide identity before inspection or export.\n\n" +
|
|
173
|
+
formatDeckHtmlContractReport(report)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function formatDeckHtmlContractReport(report: DeckHtmlContractReport): string {
|
|
178
|
+
const lines = [
|
|
179
|
+
`Status: ${report.status}`,
|
|
180
|
+
`File: ${report.filePath}`,
|
|
181
|
+
]
|
|
182
|
+
if (report.deckSlug) lines.push(`Deck: ${report.deckSlug}`)
|
|
183
|
+
if (report.activeHtmlPath) lines.push(`Active HTML target: ${report.activeHtmlPath}`)
|
|
184
|
+
if (report.expectedIndexes.length > 0) lines.push(`Expected slide indexes: ${report.expectedIndexes.join(", ")}`)
|
|
185
|
+
if (report.actualIndexes.length > 0) lines.push(`Actual slide indexes: ${report.actualIndexes.join(", ")}`)
|
|
186
|
+
|
|
187
|
+
if (report.issues.length > 0) {
|
|
188
|
+
lines.push("", "Errors:")
|
|
189
|
+
for (const issue of report.issues) lines.push(`- ${issue.message}`)
|
|
190
|
+
}
|
|
191
|
+
if (report.warnings.length > 0) {
|
|
192
|
+
lines.push("", "Warnings:")
|
|
193
|
+
for (const warning of report.warnings) lines.push(`- ${warning.message}`)
|
|
194
|
+
}
|
|
195
|
+
if (report.status === "skipped" && report.warnings.length > 0) {
|
|
196
|
+
lines.push("", "Note: skipped reports do not block standalone HTML files.")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return lines.join("\n")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function extractSlideSections(html: string): SlideSectionAttrs[] {
|
|
203
|
+
const sections: SlideSectionAttrs[] = []
|
|
204
|
+
const sectionTagPattern = /<section\b([^>]*)>/gi
|
|
205
|
+
let match: RegExpExecArray | null
|
|
206
|
+
while ((match = sectionTagPattern.exec(html))) {
|
|
207
|
+
const attrs = match[1] ?? ""
|
|
208
|
+
if (!/\bclass\s*=\s*(["'])[^"']*\bslide\b[^"']*\1/i.test(attrs)) continue
|
|
209
|
+
sections.push({
|
|
210
|
+
position: sections.length + 1,
|
|
211
|
+
dataSlideIndex: readAttr(attrs, "data-slide-index"),
|
|
212
|
+
dataIndex: readAttr(attrs, "data-index"),
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
return sections
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function readAttr(attrs: string, name: string): string | undefined {
|
|
219
|
+
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*=\\s*(["'])(.*?)\\1`, "i")
|
|
220
|
+
return pattern.exec(attrs)?.[2]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function expectedSlideIndexes(deck: DeckSpec): number[] {
|
|
224
|
+
return deck.slides.map((slide) => slide.index)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function finalize(base: Omit<DeckHtmlContractReport, "status" | "ok">): DeckHtmlContractReport {
|
|
228
|
+
const status: DeckHtmlContractStatus = base.issues.length > 0 ? "invalid" : "valid"
|
|
229
|
+
return { ...base, status, ok: status === "valid" }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function skipped(base: Omit<DeckHtmlContractReport, "status" | "ok">, message: string): DeckHtmlContractReport {
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
status: "skipped",
|
|
236
|
+
ok: true,
|
|
237
|
+
warnings: [{ type: "no_matching_deck_spec", severity: "warning", message }],
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function singleDeckKey(decks: Record<string, DeckSpec>): string | undefined {
|
|
242
|
+
const keys = Object.keys(decks)
|
|
243
|
+
return keys.length === 1 ? keys[0] : undefined
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function workspaceRelative(root: string, target: string): string {
|
|
247
|
+
return relative(root, target).split(sep).join("/")
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function escapeRegExp(value: string): string {
|
|
251
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
252
|
+
}
|