@cyber-dash-tech/revela 0.18.2 → 0.18.4
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 +5 -5
- package/README.zh-CN.md +5 -5
- package/lib/commands/review.ts +14 -13
- package/lib/decks-state.ts +1 -0
- package/lib/document-materials/extract.ts +25 -20
- package/lib/material-intake.ts +9 -4
- package/lib/narrative-state/deck-plan-artifact.ts +148 -36
- package/lib/narrative-state/render-plan.ts +41 -11
- package/lib/narrative-vault/constants.ts +1 -1
- package/lib/narrative-vault/paths.ts +7 -2
- package/lib/runtime/index.ts +3 -2
- package/lib/workspace-meta.ts +32 -0
- package/package.json +1 -1
- package/plugin.ts +4 -3
- package/plugins/revela/.mcp.json +1 -1
- package/plugins/revela/hooks/revela_guard.ts +2 -2
- package/plugins/revela/skills/revela-make-deck/SKILL.md +5 -4
- package/skill/SKILL.md +25 -19
- package/tools/workspace-scan.ts +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**English** | [中文](README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ To install globally, add the same entry to `~/.config/opencode/opencode.json`.
|
|
|
34
34
|
Requirements:
|
|
35
35
|
|
|
36
36
|
- The Codex CLI must be installed and the `codex` command must be available in your shell.
|
|
37
|
-
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- Your environment must be able to run `npx`; Revela uses `npx -y @cyber-dash-tech/revela@0.18.4 mcp` to start the MCP server.
|
|
38
38
|
- For interactive Review actions, `codex exec` must also work because the Review UI uses it for Comment/Apply Fix requests.
|
|
39
39
|
|
|
40
40
|
Optional preflight:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
Install Revela through the Codex Git marketplace:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.4
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
The Git marketplace install provides the Codex plugin shell, skills, hooks, and MCP configuration. When Codex starts the Revela MCP server for the first time, it runs `npx -y @cyber-dash-tech/revela@0.18.4 mcp` so npm can fetch the published package and its dependencies.
|
|
63
63
|
|
|
64
64
|
You do not need to run `bun install` inside the Codex marketplace clone.
|
|
65
65
|
|
|
@@ -169,7 +169,7 @@ revela, help me init this workspace from the local materials.
|
|
|
169
169
|
revela, research the public evidence and examples needed for this deck.
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
6. Create or update the deck plan before generating HTML so slide order, chapter structure, source links,
|
|
172
|
+
6. Create or update the deck plan before generating HTML so slide order, chapter structure, source links, unresolved inputs, source limitations, and visual intent are explicit.
|
|
173
173
|
|
|
174
174
|
```text
|
|
175
175
|
revela, create or update the deck plan before generating HTML.
|
package/README.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | **中文**
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](https://www.npmjs.com/package/@cyber-dash-tech/revela) [](LICENSE) [](tests/) [](https://opencode.ai) [](https://bun.sh)
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<img src="assets/img/logo.png" alt="Revela" width="560" />
|
|
@@ -34,7 +34,7 @@ Revela 可在 [OpenCode](https://opencode.ai) 和 Codex 中使用,把来源材
|
|
|
34
34
|
环境要求:
|
|
35
35
|
|
|
36
36
|
- 需要已安装 Codex CLI,并且 shell 中可以执行 `codex`。
|
|
37
|
-
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.
|
|
37
|
+
- 环境中需要可以执行 `npx`;Revela 会用 `npx -y @cyber-dash-tech/revela@0.18.4 mcp` 启动 MCP server。
|
|
38
38
|
- 如果使用 Review UI 的 Comment 或 Apply Fix,需要 `codex exec` 可用。
|
|
39
39
|
|
|
40
40
|
可选的安装前检查:
|
|
@@ -55,11 +55,11 @@ npm_config_cache=/tmp/revela-npm-cache bun run smoke:mcp-pack
|
|
|
55
55
|
通过 Codex Git marketplace 安装 Revela:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.
|
|
58
|
+
codex plugin marketplace add https://github.com/cyber-dash-tech/revela --ref v0.18.4
|
|
59
59
|
codex plugin add revela@revela
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.
|
|
62
|
+
Git marketplace 安装的是 Codex plugin 壳、skills、hooks 和 MCP 配置。Codex 第一次启动 Revela MCP server 时,会运行 `npx -y @cyber-dash-tech/revela@0.18.4 mcp`,由 npm 获取已发布 package 及其 dependencies。
|
|
63
63
|
|
|
64
64
|
不需要在 Codex marketplace clone 里运行 `bun install`。
|
|
65
65
|
|
|
@@ -169,7 +169,7 @@ revela,帮我 init 这个 workspace,先读本地材料。
|
|
|
169
169
|
revela,research 这个 deck 需要的公开证据、案例和 source。
|
|
170
170
|
```
|
|
171
171
|
|
|
172
|
-
6. 先创建或更新 deck plan,明确 slide 顺序、章节结构、source links、
|
|
172
|
+
6. 先创建或更新 deck plan,明确 slide 顺序、章节结构、source links、unresolved inputs、source limitations 和 visual intent,再生成 HTML。
|
|
173
173
|
|
|
174
174
|
```text
|
|
175
175
|
revela,生成 HTML 前先 create or update deck plan。
|
package/lib/commands/review.ts
CHANGED
|
@@ -45,7 +45,7 @@ Report format:
|
|
|
45
45
|
- Start with \`Narrative readiness: <status>\`.
|
|
46
46
|
- Include \`Narrative hash: <hash>\` when returned.
|
|
47
47
|
- List diagnostics with issue type, claim text when available, and suggested next action. Keep Markdown QA repair cards separate from compiler diagnostics.
|
|
48
|
-
- If warnings exist, list them after blockers as residual
|
|
48
|
+
- If warnings exist, list them after blockers as residual diagnostics.
|
|
49
49
|
- Keep deck/artifact readiness separate. If the user wants to write or review deck artifacts, tell them to run \`/revela make --deck\`.
|
|
50
50
|
|
|
51
51
|
Rules:
|
|
@@ -74,8 +74,8 @@ export function buildDeckPlanPrompt({
|
|
|
74
74
|
Goal:
|
|
75
75
|
- Build canonical deck-plan.md directly from the user's goal, reviewed local materials, saved research findings, and active design vocabulary.
|
|
76
76
|
- Do not create, read, repair, or require revela-narrative/.
|
|
77
|
-
- Use sourceLinks in deck-plan.md to reference material paths, material review files, research findings, asset paths,
|
|
78
|
-
- Use "unresolved inputs", "research tasks", "source limitations", or "
|
|
77
|
+
- Use sourceLinks in deck-plan.md to reference material paths, material review files, research findings, asset paths, and URLs. These are source links, not narrative graph links.
|
|
78
|
+
- Use "unresolved inputs", "research tasks", "source limitations", or "user review notes"; do not use the product concept of research gaps.
|
|
79
79
|
- Do not write HTML during planning.
|
|
80
80
|
|
|
81
81
|
Current state:
|
|
@@ -88,7 +88,7 @@ Workflow:
|
|
|
88
88
|
3. Ask only for missing high-impact intent: audience, objective, decision/action, language, rough slide count, or source priority.
|
|
89
89
|
4. Read active design inventory/rules before selecting layouts/components.
|
|
90
90
|
5. Write deck-plan.md with deck objective, audience, output format, chapter outline, source authority, unresolved inputs, and an ordered slide plan.
|
|
91
|
-
6. For every slide block,
|
|
91
|
+
6. For every slide block, use a \`---\` slide separator with slide-local metadata, then include \`#### Content Plan\`, \`#### Source Links\`, and \`#### Design Plan\`.
|
|
92
92
|
7. Include Cover, Table of Contents, and Closing slides unless the user explicitly asks for a non-standard artifact.
|
|
93
93
|
8. Run/read deck-plan diagnostics after writing and report technical issues separately from source limitations.
|
|
94
94
|
|
|
@@ -115,7 +115,7 @@ Goal:
|
|
|
115
115
|
- Read the current deck-plan.md projection and generate or update decks/*.html.
|
|
116
116
|
- Do not create, compile, or require revela-narrative/.
|
|
117
117
|
- Use the deck-render prompt mode for design, layout, components, HTML, QA, and export preflight.
|
|
118
|
-
- Preserve source links and
|
|
118
|
+
- Preserve source links, unresolved inputs, source limitations, and user review notes from deck-plan in slide source notes or speaker notes where appropriate.
|
|
119
119
|
|
|
120
120
|
Current state:
|
|
121
121
|
- ${state}
|
|
@@ -126,7 +126,7 @@ Workflow:
|
|
|
126
126
|
2. If deck-plan.md is missing or structurally invalid, stop and tell the user to run /revela plan --deck.
|
|
127
127
|
3. For a new deck, call revela-deck-foundation before adding slide content.
|
|
128
128
|
4. Read active design rules and needed layouts/components before patching HTML.
|
|
129
|
-
5. Generate
|
|
129
|
+
5. Generate one \`htmlWritingBatches\` entry at a time. Each HTML write/edit/apply_patch may add or rewrite at most 5 slide sections. Keep the HTML valid after every write and preserve existing completed slides.
|
|
130
130
|
6. Each slide must have unique increasing 1-based data-slide-index, a 1920x1080 .slide-canvas, no text overflow, and no unsafe remote asset references.
|
|
131
131
|
7. Run Artifact QA after writes and fix hard errors before reporting ready.
|
|
132
132
|
|
|
@@ -152,7 +152,7 @@ export function buildDeckPrompt({
|
|
|
152
152
|
Goal:
|
|
153
153
|
- Treat this as the explicit transition from canonical narrative state to user-directed deck planning.
|
|
154
154
|
- Use the deck-render prompt mode for design, layout, component, HTML, QA, and deck artifact rules.
|
|
155
|
-
- Default behavior is two-stage: first generate or update \`deck-plan.md\` with ordered slide blocks, low-fidelity layout notes, sourceLinks, and
|
|
155
|
+
- Default behavior is two-stage: first generate or update \`deck-plan.md\` with ordered \`---\` slide blocks, low-fidelity layout notes, sourceLinks, unresolved inputs, source limitations, and user review notes, then proceed only when the user chooses to continue.
|
|
156
156
|
- Every deck plan must include Cover, Table of Contents, and Closing slides. The TOC must show 3-5 chapter headings that match the deck's slide groups.
|
|
157
157
|
- Do not write or overwrite \`decks/*.html\` until the user chooses to proceed from the current deck-plan projection.
|
|
158
158
|
- Do not treat legacy \`writeReadiness.status\`, old review snapshots, approval fields, or existing HTML decks as workflow permission.
|
|
@@ -169,7 +169,7 @@ Workflow:
|
|
|
169
169
|
4. Call \`revela-decks\` action \`compileDeckPlan\`. This returns a claim/evidence planning packet plus deck-plan authoring requirements; it must not write HTML and does not generate the final slide list. Do not infer render structure from \`DECKS.json.slides[]\`.
|
|
170
170
|
5. If \`compileDeckPlan\` returns \`skipped\`, report the reason and ask the user whether to continue manually, repair narrative files, or provide missing intent.
|
|
171
171
|
6. If target slide count, audience, language, output purpose, or visual style is unclear, ask the user for the smallest needed confirmation before writing the plan.
|
|
172
|
-
7. Write \`deck-plan.md\` directly from the planning packet and requirements. Do not use structured upsert tools for normal plan authoring. It must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. Each slide block must
|
|
172
|
+
7. Write \`deck-plan.md\` directly from the planning packet and requirements. Do not use structured upsert tools for normal plan authoring. It must identify the chapter structure first: 3-5 chapter headings, each chapter's slide range, and which non-structural slides belong to each chapter. Each slide block must use a \`---\` separator with positive 1-based \`slideIndex\`, layout, components, and then \`#### Content Plan\`, \`#### Source Links\` for materials/findings/assets/URLs, and \`#### Design Plan\` with low-fidelity render notes. Do not generate visual images or HTML mockups during planning.
|
|
173
173
|
8. Stop after presenting the plan unless the user already asked to proceed. Ask whether to continue, revise the plan, or run more research. Do not require an Approval block or \`confirmDeckPlan\` gate; \`confirmDeckPlan\` is compatibility/provenance only.
|
|
174
174
|
9. Ask for or confirm visual design only after the narrative deck plan exists. For a new deck HTML file, call \`revela-deck-foundation\` first to create the active-design foundation shell; it must not create narrative slide content, choose layouts/components, or read/write \`${DECKS_STATE_FILE}\`.
|
|
175
175
|
10. Fetch active design rules plus required layouts/components with \`revela-designs read\` as needed before patching slides into the foundation shell. Fetch chart rules before ECharts.
|
|
@@ -178,6 +178,7 @@ Workflow:
|
|
|
178
178
|
13. Run artifact diagnostics when useful, but do not treat \`writeReadiness\`, cached slide specs, unconfirmed plans, missing research, or stale coverage as workflow blockers.
|
|
179
179
|
14. Write \`decks/*.html\` when the user chooses to proceed and all deck HTML contract requirements can be satisfied. For new files, patch slide sections between the \`revela-slides\` markers created by \`revela-deck-foundation\`. Generate the artifact chapter by chapter instead of drafting all content slides in one broad pass. Partial decks are allowed during chapter-by-chapter authoring when written slide sections have unique, increasing 1-based \`data-slide-index\` values and valid canvases; do not pad missing planned chapters with filler to match cached \`DECKS.json.slides[]\` length. Keep the HTML file valid after every write, preserve already-written slides, and update one chapter's slide sections at a time.
|
|
180
180
|
15. For each chapter, make every content slide carry a distinct claim, evidence item, comparison, risk, or action. If a chapter lacks enough substance for its allocated slides, merge weak slides or reduce the slide count instead of creating sparse filler.
|
|
181
|
+
15a. Use the current \`htmlWritingBatches\` entry as the HTML write boundary. Each HTML write/edit/apply_patch may add or rewrite at most 5 slide sections.
|
|
181
182
|
16. After each HTML write, the system automatically runs artifact QA before opening Review. If post-write artifact QA reports hard errors, fix them and let QA run again. Review opens only after hard errors pass. Density warnings about thin claim/evidence substance should be reported and improved when useful, but they do not block Review.
|
|
182
183
|
|
|
183
184
|
Deck plan report format:
|
|
@@ -189,7 +190,7 @@ Deck plan report format:
|
|
|
189
190
|
- Include the required Source Authority and remind that \`DECKS.json.slides[]\` is cache/compatibility data, not the render contract.
|
|
190
191
|
- Include \`Required structure: Cover + Table of Contents + Closing\` and do not omit any of those slides.
|
|
191
192
|
- Include a \`Chapters\` section before the slide list. It must list 3-5 TOC headings, their slide ranges, and the non-structural slides assigned to each chapter.
|
|
192
|
-
- For every slide block, include: slide index, title, purpose, narrative role, low-fidelity layout note, layout, components, sourceLinks, visual intent, visual brief,
|
|
193
|
+
- For every slide block, include: slide index, title, purpose, narrative role, low-fidelity layout note, layout, components, sourceLinks, visual intent, visual brief, unresolved inputs, source limitations, user review notes, and render notes.
|
|
193
194
|
- Use this sketch style or similarly simple ASCII boxes:
|
|
194
195
|
|
|
195
196
|
\`\`\`text
|
|
@@ -213,7 +214,7 @@ Supporting claims:
|
|
|
213
214
|
Evidence bindings:
|
|
214
215
|
Visual intent:
|
|
215
216
|
Visual brief:
|
|
216
|
-
|
|
217
|
+
Unresolved inputs / source limitations / user review notes:
|
|
217
218
|
\`\`\`
|
|
218
219
|
- End by asking the user whether to proceed to HTML, revise the plan, or run more research.
|
|
219
220
|
|
|
@@ -222,7 +223,7 @@ Report format before any HTML write:
|
|
|
222
223
|
- Include which deck-plan projection and narrative hash are guiding artifact work.
|
|
223
224
|
- State that \`revela-decks readDeckPlan\` was called and the current \`deck-plan.md\` slide order/chapter groups are being followed.
|
|
224
225
|
- For new HTML files, state that \`revela-deck-foundation\` created the foundation shell and identify the output path/design before slide content is patched.
|
|
225
|
-
- Include the
|
|
226
|
+
- Include the current \`htmlWritingBatches\` entry, its slide indexes, and confirm already-written slides are being preserved.
|
|
226
227
|
- If technical artifact checks cannot be satisfied, list those blockers separately from narrative/deck-plan diagnostics.
|
|
227
228
|
- After writing HTML, read the appended \`Artifact QA\` report from the tool output. If it failed, fix hard errors before considering the deck ready for Review.
|
|
228
229
|
|
|
@@ -233,7 +234,7 @@ Rules:
|
|
|
233
234
|
- Visual intent is part of the deck-plan projection. During HTML generation, satisfy the planned component/visual brief using fetched design components; do not collapse planned visuals into prose-only bullets.
|
|
234
235
|
- Cached deck slide specs in \`DECKS.json\` are legacy projections only. Canonical narrative remains the authority for audience, decision, claims, evidence boundaries, objections, and risks.
|
|
235
236
|
- Cover, Table of Contents, and Closing are mandatory deck structure. TOC chapter headings must match the chapter grouping used for generation.
|
|
236
|
-
- Do not generate the complete deck content in one broad pass. Work
|
|
237
|
+
- Do not generate the complete deck content in one broad pass. Work one \`htmlWritingBatches\` entry at a time while keeping the artifact valid after each write.
|
|
237
238
|
- Applying evidence candidates or rewriting canonical claims requires explicit user instruction.
|
|
238
239
|
- If the user requests slide order, layout, component, or visual-intent changes that do not alter meaning, update only \`deck-plan.md\` or artifact-level plan content.
|
|
239
240
|
- If the user requests claim, evidence, caveat, decision, or recommendation meaning changes, update canonical narrative first, then report alignment diagnostics before compiling a new deck plan.
|
|
@@ -295,7 +296,7 @@ Technical blockers only:
|
|
|
295
296
|
Report format:
|
|
296
297
|
- Start with \`Artifact diagnostics: <status>\`.
|
|
297
298
|
- If technically blocked, list each blocker with file/slide when available, issue type, and smallest repair.
|
|
298
|
-
- If warnings exist but no technical blocker exists, say the user can proceed and note residual
|
|
299
|
+
- If warnings exist but no technical blocker exists, say the user can proceed and note residual diagnostics.
|
|
299
300
|
- Include coverage-driven make diagnostics when returned: whether the active deck artifact coverage is current/stale/partial/missing, which required claims are missing, which claims are affected, and the next command/action recommended by the tool.
|
|
300
301
|
- Report \`narrative_gap\` warnings as story-structure risks such as weak so-what, missing risk/assumption handling, conclusion before support, missing audience framing, or abrupt transition.
|
|
301
302
|
- Do not invent evidence or silently downgrade blockers. Use the tool result as authoritative.
|
package/lib/decks-state.ts
CHANGED
|
@@ -1651,6 +1651,7 @@ function isIgnorableSourceMaterial(path: string): boolean {
|
|
|
1651
1651
|
normalized.startsWith("decks/") ||
|
|
1652
1652
|
normalized.startsWith("researches/") ||
|
|
1653
1653
|
normalized.startsWith("assets/") ||
|
|
1654
|
+
normalized.startsWith(".revela/") ||
|
|
1654
1655
|
normalized.startsWith(".opencode/"),
|
|
1655
1656
|
)
|
|
1656
1657
|
}
|
|
@@ -11,6 +11,7 @@ import { extractXlsx } from "../read-hooks/extractors/xlsx"
|
|
|
11
11
|
import { hasDecksState, readDecksState, writeDecksState } from "../decks-state"
|
|
12
12
|
import { computeSourceFingerprint, sourceMaterialMetadata, upsertSourceMaterial } from "../source-materials"
|
|
13
13
|
import { recordWorkspaceAction } from "../workspace-state/actions"
|
|
14
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
|
|
14
15
|
|
|
15
16
|
export type DocumentMaterial = {
|
|
16
17
|
path: string
|
|
@@ -852,7 +853,7 @@ async function extractPdfImages(buf: Buffer, cacheDir: string, workspaceDir: str
|
|
|
852
853
|
async function processPdfFile(filePath: string, workspaceDir: string): Promise<DocumentMaterialsResult> {
|
|
853
854
|
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
854
855
|
const fingerprint = buildFingerprint(filePath)
|
|
855
|
-
const cacheDir =
|
|
856
|
+
const cacheDir = existingWorkspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
856
857
|
const manifestPath = join(cacheDir, "manifest.json")
|
|
857
858
|
|
|
858
859
|
if (existsSync(manifestPath)) {
|
|
@@ -874,19 +875,21 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
874
875
|
}
|
|
875
876
|
}
|
|
876
877
|
|
|
877
|
-
|
|
878
|
-
|
|
878
|
+
const writeCacheDir = workspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
879
|
+
const writeManifestPath = join(writeCacheDir, "manifest.json")
|
|
880
|
+
mkdirSync(join(writeCacheDir, "images"), { recursive: true })
|
|
881
|
+
mkdirSync(join(writeCacheDir, "tables"), { recursive: true })
|
|
879
882
|
|
|
880
883
|
const buf = readFileSync(filePath)
|
|
881
884
|
const text = await extractPdfText(buf)
|
|
882
|
-
const textPath = join(
|
|
885
|
+
const textPath = join(writeCacheDir, "text.txt")
|
|
883
886
|
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
884
887
|
|
|
885
|
-
const images = await extractPdfImages(buf,
|
|
886
|
-
const relativeManifestPath = workspaceRelative(
|
|
888
|
+
const images = await extractPdfImages(buf, writeCacheDir, workspaceDir)
|
|
889
|
+
const relativeManifestPath = workspaceRelative(writeManifestPath, workspaceDir)
|
|
887
890
|
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
888
891
|
const readViewPath = writeReadView({
|
|
889
|
-
cacheDir,
|
|
892
|
+
cacheDir: writeCacheDir,
|
|
890
893
|
workspaceDir,
|
|
891
894
|
source: relativeSource,
|
|
892
895
|
type: "pdf",
|
|
@@ -905,7 +908,7 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
905
908
|
cache_status: "miss",
|
|
906
909
|
source: relativeSource,
|
|
907
910
|
type: "pdf",
|
|
908
|
-
cache_dir: workspaceRelative(
|
|
911
|
+
cache_dir: workspaceRelative(writeCacheDir, workspaceDir),
|
|
909
912
|
manifest_path: relativeManifestPath,
|
|
910
913
|
text_path: relativeTextPath,
|
|
911
914
|
read_view_path: readViewPath,
|
|
@@ -929,14 +932,14 @@ async function processPdfFile(filePath: string, workspaceDir: string): Promise<D
|
|
|
929
932
|
tables: [],
|
|
930
933
|
}
|
|
931
934
|
|
|
932
|
-
writeFileSync(
|
|
935
|
+
writeFileSync(writeManifestPath, JSON.stringify(manifest, null, 2), "utf-8")
|
|
933
936
|
return result
|
|
934
937
|
}
|
|
935
938
|
|
|
936
939
|
async function processOfficeFile(filePath: string, workspaceDir: string, type: SupportedType): Promise<DocumentMaterialsResult> {
|
|
937
940
|
const relativeSource = workspaceRelative(filePath, workspaceDir)
|
|
938
941
|
const fingerprint = buildFingerprint(filePath)
|
|
939
|
-
const cacheDir =
|
|
942
|
+
const cacheDir = existingWorkspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
940
943
|
const manifestPath = join(cacheDir, "manifest.json")
|
|
941
944
|
|
|
942
945
|
if (existsSync(manifestPath)) {
|
|
@@ -958,8 +961,10 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
958
961
|
}
|
|
959
962
|
}
|
|
960
963
|
|
|
961
|
-
|
|
962
|
-
|
|
964
|
+
const writeCacheDir = workspaceMetaPath(workspaceDir, "doc-materials", fingerprint)
|
|
965
|
+
const writeManifestPath = join(writeCacheDir, "manifest.json")
|
|
966
|
+
mkdirSync(join(writeCacheDir, "images"), { recursive: true })
|
|
967
|
+
mkdirSync(join(writeCacheDir, "tables"), { recursive: true })
|
|
963
968
|
|
|
964
969
|
const buf = readFileSync(filePath)
|
|
965
970
|
const files = unzipSync(new Uint8Array(buf))
|
|
@@ -970,26 +975,26 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
970
975
|
? await extractDocx(buf)
|
|
971
976
|
: await extractXlsx(buf)
|
|
972
977
|
|
|
973
|
-
const textPath = join(
|
|
978
|
+
const textPath = join(writeCacheDir, "text.txt")
|
|
974
979
|
writeFileSync(textPath, `[Extracted from: ${basename(filePath)}]\n\n${text}`, "utf-8")
|
|
975
980
|
|
|
976
981
|
const pptxAssets = type === "pptx"
|
|
977
|
-
? extractPptxImages(files,
|
|
982
|
+
? extractPptxImages(files, writeCacheDir, workspaceDir)
|
|
978
983
|
: null
|
|
979
984
|
const images = type === "pptx"
|
|
980
985
|
? pptxAssets!.images
|
|
981
986
|
: type === "docx"
|
|
982
|
-
? extractDocxImages(files,
|
|
983
|
-
: extractXlsxImages(files,
|
|
987
|
+
? extractDocxImages(files, writeCacheDir, workspaceDir)
|
|
988
|
+
: extractXlsxImages(files, writeCacheDir, workspaceDir)
|
|
984
989
|
const slides = type === "pptx"
|
|
985
990
|
? extractPptxSlides(files, images, pptxAssets!.skipped_assets)
|
|
986
991
|
: undefined
|
|
987
|
-
const relativeManifestPath = workspaceRelative(
|
|
992
|
+
const relativeManifestPath = workspaceRelative(writeManifestPath, workspaceDir)
|
|
988
993
|
const relativeTextPath = workspaceRelative(textPath, workspaceDir)
|
|
989
994
|
const tables = extractTables(type, relativeTextPath)
|
|
990
995
|
const skippedAssets = pptxAssets?.skipped_assets ?? []
|
|
991
996
|
const readViewPath = writeReadView({
|
|
992
|
-
cacheDir,
|
|
997
|
+
cacheDir: writeCacheDir,
|
|
993
998
|
workspaceDir,
|
|
994
999
|
source: relativeSource,
|
|
995
1000
|
type,
|
|
@@ -1008,7 +1013,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
1008
1013
|
cache_status: "miss",
|
|
1009
1014
|
source: relativeSource,
|
|
1010
1015
|
type,
|
|
1011
|
-
cache_dir: workspaceRelative(
|
|
1016
|
+
cache_dir: workspaceRelative(writeCacheDir, workspaceDir),
|
|
1012
1017
|
manifest_path: relativeManifestPath,
|
|
1013
1018
|
text_path: relativeTextPath,
|
|
1014
1019
|
read_view_path: readViewPath,
|
|
@@ -1032,7 +1037,7 @@ async function processOfficeFile(filePath: string, workspaceDir: string, type: S
|
|
|
1032
1037
|
tables: result.tables ?? [],
|
|
1033
1038
|
}
|
|
1034
1039
|
|
|
1035
|
-
writeFileSync(
|
|
1040
|
+
writeFileSync(writeManifestPath, JSON.stringify(manifest, null, 2), "utf-8")
|
|
1036
1041
|
return result
|
|
1037
1042
|
}
|
|
1038
1043
|
|
package/lib/material-intake.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { basename, extname, isAbsolute, join, relative, resolve, sep } from "pat
|
|
|
3
3
|
import { extractDocumentMaterials, type DocumentMaterialsResult } from "./document-materials/extract"
|
|
4
4
|
import { sourceMaterialMetadata, sourceMaterialType } from "./source-materials"
|
|
5
5
|
import type { SourceMaterial } from "./decks-state"
|
|
6
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "./workspace-meta"
|
|
6
7
|
|
|
7
8
|
export type MaterialIntakeStatus =
|
|
8
9
|
| "scanned"
|
|
@@ -107,24 +108,28 @@ export interface CheckMaterialIntakeResult {
|
|
|
107
108
|
}
|
|
108
109
|
|
|
109
110
|
const DOC_EXTENSIONS = new Set([".pdf", ".docx", ".doc", ".xlsx", ".xls", ".pptx", ".ppt", ".csv", ".md", ".txt"])
|
|
110
|
-
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".opencode", "researches", "revela-narrative", "designs", "domains"])
|
|
111
|
+
const EXCLUDE_DIRS = new Set(["node_modules", ".git", "dist", ".opencode", ".revela", "researches", "revela-narrative", "designs", "domains"])
|
|
111
112
|
const EXCLUDE_FILENAMES = new Set(["AGENTS.md", "DECKS.md", "README.md", "README.zh-CN.md"])
|
|
112
113
|
const EXTRACTION_EXTENSIONS = new Set(["pdf", "ppt", "pptx", "doc", "docx", "xls", "xlsx"])
|
|
113
114
|
const SUPPORTED_EXTRACTION_EXTENSIONS = new Set(["pdf", "pptx", "docx", "xlsx"])
|
|
114
115
|
|
|
115
116
|
export function materialRegistryPath(workspaceRoot: string): string {
|
|
116
|
-
return
|
|
117
|
+
return workspaceMetaPath(workspaceRoot, "material-intake", "registry.json")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function existingMaterialRegistryPath(workspaceRoot: string): string {
|
|
121
|
+
return existingWorkspaceMetaPath(workspaceRoot, "material-intake", "registry.json")
|
|
117
122
|
}
|
|
118
123
|
|
|
119
124
|
export function readMaterialRegistry(workspaceRoot: string): MaterialRegistry {
|
|
120
|
-
const path =
|
|
125
|
+
const path = existingMaterialRegistryPath(workspaceRoot)
|
|
121
126
|
if (!existsSync(path)) return { version: 1, updatedAt: new Date(0).toISOString(), sources: [] }
|
|
122
127
|
return JSON.parse(readFileSync(path, "utf-8")) as MaterialRegistry
|
|
123
128
|
}
|
|
124
129
|
|
|
125
130
|
export function writeMaterialRegistry(workspaceRoot: string, registry: MaterialRegistry): string {
|
|
126
131
|
const path = materialRegistryPath(workspaceRoot)
|
|
127
|
-
mkdirSync(join(workspaceRoot, ".
|
|
132
|
+
mkdirSync(join(workspaceRoot, ".revela", "material-intake"), { recursive: true })
|
|
128
133
|
writeFileSync(path, JSON.stringify({ ...registry, updatedAt: new Date().toISOString() }, null, 2), "utf-8")
|
|
129
134
|
return workspaceRelative(path, workspaceRoot)
|
|
130
135
|
}
|
|
@@ -14,6 +14,7 @@ export const DECK_PLAN_INDEX_PATH = "deck-plan/index.md"
|
|
|
14
14
|
export const DECK_PLAN_SLIDES_DIR = "deck-plan/slides"
|
|
15
15
|
export const LEGACY_DECK_PLAN_ARTIFACT_PATH = "decks/deck-plan.md"
|
|
16
16
|
export const DECK_PLAN_ARTIFACT_PATH = DECK_PLAN_MARKDOWN_PATH
|
|
17
|
+
export const MAX_HTML_SLIDES_PER_BATCH = 5
|
|
17
18
|
|
|
18
19
|
export interface DeckPlanArtifactInput {
|
|
19
20
|
deck: DeckSpec
|
|
@@ -69,11 +70,21 @@ export interface DeckPlanProjection {
|
|
|
69
70
|
designName?: string
|
|
70
71
|
outputPath?: string
|
|
71
72
|
slides: DeckPlanSlideProjection[]
|
|
73
|
+
htmlWritingBatches: DeckPlanHtmlWritingBatch[]
|
|
74
|
+
htmlWritingInstruction: string
|
|
72
75
|
graphNodes: Array<{ id: string; type: WorkspaceGraphNodeType; file: string }>
|
|
73
76
|
graphRelations: VaultRelation[]
|
|
74
77
|
diagnostics: DeckPlanProjectionDiagnostic[]
|
|
75
78
|
}
|
|
76
79
|
|
|
80
|
+
export interface DeckPlanHtmlWritingBatch {
|
|
81
|
+
label: string
|
|
82
|
+
chapterTitle: string
|
|
83
|
+
slideIndexes: number[]
|
|
84
|
+
maxSlides: number
|
|
85
|
+
instructions: string
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
export interface DeckPlanSlideProjection {
|
|
78
89
|
path: string
|
|
79
90
|
absolutePath: string
|
|
@@ -283,6 +294,7 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
283
294
|
file: slide.path,
|
|
284
295
|
source: "inline" as const,
|
|
285
296
|
})))
|
|
297
|
+
const htmlWritingBatches = buildHtmlWritingBatches(slides)
|
|
286
298
|
return {
|
|
287
299
|
path,
|
|
288
300
|
absolutePath,
|
|
@@ -294,6 +306,8 @@ export function readDeckPlanProjection(workspaceRoot: string, expected?: { narra
|
|
|
294
306
|
designName: stringField(parsed.frontmatter, "designName") || stringField(parsed.frontmatter, "design"),
|
|
295
307
|
outputPath: stringField(parsed.frontmatter, "outputPath") || stringField(parsed.frontmatter, "output"),
|
|
296
308
|
slides,
|
|
309
|
+
htmlWritingBatches,
|
|
310
|
+
htmlWritingInstruction: htmlWritingInstruction(),
|
|
297
311
|
graphNodes,
|
|
298
312
|
graphRelations,
|
|
299
313
|
diagnostics,
|
|
@@ -470,31 +484,35 @@ function writeDeckPlanSingleFile(workspaceRoot: string, input: {
|
|
|
470
484
|
|
|
471
485
|
function renderDeckPlanSlideBlock(slide: DeckPlanSlideProjection, visualIntent?: DeckPlanSlideUpsertInput["visualIntent"]): string {
|
|
472
486
|
const lines: string[] = []
|
|
473
|
-
lines.push(
|
|
474
|
-
lines.push("")
|
|
475
|
-
lines.push(
|
|
476
|
-
lines.push(
|
|
477
|
-
lines.push(
|
|
478
|
-
lines.push(
|
|
479
|
-
lines.push(
|
|
480
|
-
lines.push(
|
|
481
|
-
lines.push("")
|
|
482
|
-
lines.push("
|
|
487
|
+
lines.push("---")
|
|
488
|
+
lines.push(`slideIndex: ${slide.slideIndex ?? ""}`)
|
|
489
|
+
lines.push(`id: ${slide.id}`)
|
|
490
|
+
lines.push(`title: ${yamlScalar(slide.title)}`)
|
|
491
|
+
lines.push(`chapter: ${yamlScalar(slide.chapter || "Unassigned")}`)
|
|
492
|
+
lines.push(`role: ${yamlScalar(slide.narrativeRole || "Not specified")}`)
|
|
493
|
+
lines.push(`structural: ${slide.structural ? "true" : "false"}`)
|
|
494
|
+
lines.push(`layout: ${slide.layout || "unspecified"}`)
|
|
495
|
+
lines.push(`components: ${slide.components.join(", ") || "none"}`)
|
|
496
|
+
lines.push("---")
|
|
483
497
|
lines.push("")
|
|
484
|
-
|
|
485
|
-
else lines.push("- Brief: Not specified.")
|
|
498
|
+
lines.push("#### Content Plan")
|
|
486
499
|
lines.push("")
|
|
487
|
-
lines.push(
|
|
500
|
+
lines.push(`- Message: ${slide.narrativeRole || "Not specified."}`)
|
|
501
|
+
lines.push(`- Role: ${slide.narrativeRole || "Not specified"}`)
|
|
502
|
+
lines.push("- Speaker notes: Not specified.")
|
|
488
503
|
lines.push("")
|
|
489
|
-
for (const component of slide.componentPlan) lines.push(renderComponentPlanMarkdown(component, 5))
|
|
490
504
|
lines.push("#### Source Links")
|
|
491
505
|
lines.push("")
|
|
492
506
|
lines.push(renderSourceLinksMarkdown(slide.sourceLinks))
|
|
493
507
|
lines.push("")
|
|
494
|
-
lines.push("####
|
|
508
|
+
lines.push("#### Design Plan")
|
|
495
509
|
lines.push("")
|
|
496
|
-
|
|
497
|
-
|
|
510
|
+
lines.push(`- Layout: ${slide.layout || "unspecified"}`)
|
|
511
|
+
lines.push(`- Components: ${slide.components.join(", ") || "none"}`)
|
|
512
|
+
lines.push("- Visual intent:")
|
|
513
|
+
lines.push(...indentMultiline(visualIntent ? renderVisualIntent(visualIntent) : "- Brief: Not specified."))
|
|
514
|
+
lines.push("")
|
|
515
|
+
for (const component of slide.componentPlan) lines.push(renderComponentPlanMarkdown(component, 5))
|
|
498
516
|
lines.push("")
|
|
499
517
|
return lines.join("\n")
|
|
500
518
|
}
|
|
@@ -525,7 +543,6 @@ function renderSourceLinksMarkdown(sourceLinks: DeckPlanSourceLinks): string {
|
|
|
525
543
|
["Findings", sourceLinks.findings],
|
|
526
544
|
["Assets", sourceLinks.assets],
|
|
527
545
|
["URLs", sourceLinks.urls],
|
|
528
|
-
["Caveats", sourceLinks.caveats],
|
|
529
546
|
] as const) {
|
|
530
547
|
lines.push(`${label}:`)
|
|
531
548
|
if (values.length === 0) lines.push("- None.")
|
|
@@ -575,6 +592,8 @@ function readDeckPlanSlideFiles(workspaceRoot: string, knownNodeIds?: Set<string
|
|
|
575
592
|
}
|
|
576
593
|
|
|
577
594
|
function readDeckPlanSlidesFromSingleFile(workspaceRoot: string, absolutePath: string, markdown: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
595
|
+
const slideBlocks = readDeckPlanSeparatorSlidesFromSingleFile(workspaceRoot, absolutePath, markdown, knownNodeIds)
|
|
596
|
+
if (slideBlocks.length > 0) return slideBlocks
|
|
578
597
|
const path = relativePath(workspaceRoot, absolutePath)
|
|
579
598
|
const body = parseVaultFrontmatter(markdown).body
|
|
580
599
|
const matches = [...body.matchAll(/^[ \t]*###\s+Slide\s+(\d+)\s+(?:—|-)\s+(.+?)\s*$/gm)]
|
|
@@ -620,6 +639,58 @@ function readDeckPlanSlidesFromSingleFile(workspaceRoot: string, absolutePath: s
|
|
|
620
639
|
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
621
640
|
}
|
|
622
641
|
|
|
642
|
+
function readDeckPlanSeparatorSlidesFromSingleFile(workspaceRoot: string, absolutePath: string, markdown: string, knownNodeIds?: Set<string>): DeckPlanSlideProjection[] {
|
|
643
|
+
const path = relativePath(workspaceRoot, absolutePath)
|
|
644
|
+
const body = parseVaultFrontmatter(markdown).body
|
|
645
|
+
const slidesHeading = /^[ \t]*##\s+Slides\s*$/mi.exec(body)
|
|
646
|
+
if (!slidesHeading || slidesHeading.index === undefined) return []
|
|
647
|
+
const headingEnd = body.indexOf("\n", slidesHeading.index)
|
|
648
|
+
const slidesStart = headingEnd === -1 ? slidesHeading.index + slidesHeading[0].length : headingEnd + 1
|
|
649
|
+
const rest = body.slice(slidesStart)
|
|
650
|
+
const nextSection = rest.search(/^[ \t]*##\s+(?!#)/m)
|
|
651
|
+
const slidesRegion = nextSection === -1 ? rest : rest.slice(0, nextSection)
|
|
652
|
+
const metadataMatches = [...slidesRegion.matchAll(/^[ \t]*---[ \t]*\n[\s\S]*?\n[ \t]*---[ \t]*(?:\n|$)/gm)]
|
|
653
|
+
const slides: DeckPlanSlideProjection[] = []
|
|
654
|
+
for (let i = 0; i < metadataMatches.length; i++) {
|
|
655
|
+
const match = metadataMatches[i]
|
|
656
|
+
const start = match.index ?? 0
|
|
657
|
+
const next = i + 1 < metadataMatches.length ? metadataMatches[i + 1].index ?? slidesRegion.length : slidesRegion.length
|
|
658
|
+
const block = slidesRegion.slice(start, next).trim()
|
|
659
|
+
const parsed = parseVaultFrontmatter(block)
|
|
660
|
+
const slideIndex = numberField(parsed.frontmatter, "slideIndex")
|
|
661
|
+
const title = stringField(parsed.frontmatter, "title") || firstHeading(parsed.body) || `Slide ${slideIndex ?? i + 1}`
|
|
662
|
+
const fields = parseSlideBlockFields(parsed.body)
|
|
663
|
+
const sourceLinks = normalizeSourceLinks(parseDeckPlanSourceLinks(singleFileSubsection(block, "Source Links")))
|
|
664
|
+
const narrativeLinks = parseDeckPlanNarrativeLinks(singleFileSubsection(block, "Narrative Links") || block, knownNodeIds)
|
|
665
|
+
const links = sourceLinksToNarrativeLinks(sourceLinks, narrativeLinks)
|
|
666
|
+
const componentPlan = parseDeckPlanComponentPlan(singleFileSubsection(block, "Component Plan") || singleFileSubsection(block, "Design Plan"))
|
|
667
|
+
slides.push({
|
|
668
|
+
path,
|
|
669
|
+
absolutePath,
|
|
670
|
+
id: stringField(parsed.frontmatter, "id") || fields.id || `slide-${slugify(title)}`,
|
|
671
|
+
slideIndex,
|
|
672
|
+
title,
|
|
673
|
+
chapter: stringField(parsed.frontmatter, "chapter") || fields.chapter || "",
|
|
674
|
+
layout: stringField(parsed.frontmatter, "layout") || fields.layout || "",
|
|
675
|
+
components: arrayField(parsed.frontmatter, "components").length > 0 ? arrayField(parsed.frontmatter, "components") : parseCsv(fields.components || componentPlan.map((component) => component.name).join(", ")),
|
|
676
|
+
componentPlan,
|
|
677
|
+
structural: booleanField(parsed.frontmatter, "structural", fields.structural === "true" || fields.structural === "yes"),
|
|
678
|
+
narrativeRole: stringField(parsed.frontmatter, "role") || stringField(parsed.frontmatter, "narrativeRole") || fields.role || fields.narrativeRole || "",
|
|
679
|
+
markdown: block,
|
|
680
|
+
frontmatter: parsed.frontmatter,
|
|
681
|
+
sections: parseMarkdownSections(block),
|
|
682
|
+
links,
|
|
683
|
+
sourceLinks,
|
|
684
|
+
caveats: uniqueStrings([...parseBulletText(singleFileSubsection(block, "Caveats")), ...sourceLinks.caveats]),
|
|
685
|
+
})
|
|
686
|
+
}
|
|
687
|
+
return slides.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function firstHeading(markdown: string): string {
|
|
691
|
+
return /^#{1,6}\s+(.+?)\s*$/m.exec(markdown)?.[1]?.trim() ?? ""
|
|
692
|
+
}
|
|
693
|
+
|
|
623
694
|
function parseSlideBlockFields(block: string): Record<string, string> {
|
|
624
695
|
const fields: Record<string, string> = {}
|
|
625
696
|
for (const rawLine of block.split(/\r?\n/)) {
|
|
@@ -787,9 +858,53 @@ function deckPlanIndexDiagnostics(slides: DeckPlanSlideProjection[]): DeckPlanPr
|
|
|
787
858
|
return diagnostics
|
|
788
859
|
}
|
|
789
860
|
|
|
861
|
+
function buildHtmlWritingBatches(slides: DeckPlanSlideProjection[]): DeckPlanHtmlWritingBatch[] {
|
|
862
|
+
const ordered = slides
|
|
863
|
+
.filter((slide) => Number.isInteger(slide.slideIndex) && (slide.slideIndex ?? 0) > 0)
|
|
864
|
+
.sort((a, b) => (a.slideIndex ?? Number.MAX_SAFE_INTEGER) - (b.slideIndex ?? Number.MAX_SAFE_INTEGER))
|
|
865
|
+
const chapterGroups: Array<{ chapterTitle: string; slideIndexes: number[] }> = []
|
|
866
|
+
for (const slide of ordered) {
|
|
867
|
+
const chapterTitle = slide.chapter || "Unassigned"
|
|
868
|
+
const current = chapterGroups[chapterGroups.length - 1]
|
|
869
|
+
if (current && current.chapterTitle === chapterTitle) current.slideIndexes.push(slide.slideIndex!)
|
|
870
|
+
else chapterGroups.push({ chapterTitle, slideIndexes: [slide.slideIndex!] })
|
|
871
|
+
}
|
|
872
|
+
const batches: DeckPlanHtmlWritingBatch[] = []
|
|
873
|
+
for (const group of chapterGroups) {
|
|
874
|
+
const chunks = chunkNumbers(group.slideIndexes, MAX_HTML_SLIDES_PER_BATCH)
|
|
875
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
876
|
+
const chunk = chunks[index]
|
|
877
|
+
const chapterSuffix = chunks.length > 1 ? ` part ${index + 1}` : ""
|
|
878
|
+
const label = batches.length === 0
|
|
879
|
+
? `Initial shell and ${group.chapterTitle}${chapterSuffix}`
|
|
880
|
+
: `${group.chapterTitle}${chapterSuffix}`
|
|
881
|
+
batches.push({
|
|
882
|
+
label,
|
|
883
|
+
chapterTitle: group.chapterTitle,
|
|
884
|
+
slideIndexes: chunk,
|
|
885
|
+
maxSlides: MAX_HTML_SLIDES_PER_BATCH,
|
|
886
|
+
instructions: batches.length === 0
|
|
887
|
+
? `Create or update the foundation if needed, then write only slide sections ${formatSlideRange(chunk)}. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`
|
|
888
|
+
: `Patch only slide sections ${formatSlideRange(chunk)}, preserve previously written slides, and keep the file valid after the patch. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`,
|
|
889
|
+
})
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return batches
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function chunkNumbers(values: number[], size: number): number[][] {
|
|
896
|
+
const chunks: number[][] = []
|
|
897
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size))
|
|
898
|
+
return chunks
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function htmlWritingInstruction(): string {
|
|
902
|
+
return `Before every HTML write/edit/apply_patch, follow htmlWritingBatches and add or rewrite at most ${MAX_HTML_SLIDES_PER_BATCH} <section class="slide"> blocks. Run Artifact QA after each batch before continuing.`
|
|
903
|
+
}
|
|
904
|
+
|
|
790
905
|
function slideDiagnostics(slide: DeckPlanSlideProjection, knownNodeIds?: Set<string>): DeckPlanProjectionDiagnostic[] {
|
|
791
906
|
const diagnostics: DeckPlanProjectionDiagnostic[] = []
|
|
792
|
-
if (!slide.structural && slide.
|
|
907
|
+
if (!slide.structural && linksCount(slide.sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: `Non-structural deck-plan slide ${slide.id} has no material, finding, asset, or URL source link.`, file: slide.path, nodeId: slide.id })
|
|
793
908
|
if (!slide.layout) diagnostics.push({ severity: "warning", code: "slide_layout_missing", message: `Deck-plan slide ${slide.id} is missing a layout.`, file: slide.path, nodeId: slide.id })
|
|
794
909
|
if (slide.components.length === 0) diagnostics.push({ severity: "warning", code: "slide_components_missing", message: `Deck-plan slide ${slide.id} has no component names in frontmatter.`, file: slide.path, nodeId: slide.id })
|
|
795
910
|
if (slide.componentPlan.length === 0) diagnostics.push({ severity: "warning", code: "slide_component_plan_missing", message: `Deck-plan slide ${slide.id} is missing structured ## Component Plan entries.`, file: slide.path, nodeId: slide.id })
|
|
@@ -847,7 +962,7 @@ function validateDeckPlanSlideUpsert(input: DeckPlanSlideUpsertInput, options: {
|
|
|
847
962
|
const visual = normalizeVisualIntent(input.visualIntent)
|
|
848
963
|
if (visual.component && !componentNames.has(visual.component)) diagnostics.push(errorDiagnostic("slide_visual_component_missing", `visualIntent.component '${visual.component}' is not present in component plan.`, nodeId))
|
|
849
964
|
const sourceLinks = sourceLinksForInput(input)
|
|
850
|
-
if (!input.structural && linksCount(sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: "Non-structural slides should include at least one material, finding, asset,
|
|
965
|
+
if (!input.structural && linksCount(sourceLinks) === 0) diagnostics.push({ severity: "warning", code: "slide_source_link_missing", message: "Non-structural slides should include at least one material, finding, asset, or URL source link.", nodeId })
|
|
851
966
|
return diagnostics
|
|
852
967
|
}
|
|
853
968
|
|
|
@@ -887,7 +1002,7 @@ function validateComponentInput(component: DeckPlanSlideUpsertComponentInput, co
|
|
|
887
1002
|
}
|
|
888
1003
|
|
|
889
1004
|
function linksCount(sourceLinks: DeckPlanSourceLinks): number {
|
|
890
|
-
return sourceLinks.materials.length + sourceLinks.findings.length + sourceLinks.assets.length + sourceLinks.urls.length
|
|
1005
|
+
return sourceLinks.materials.length + sourceLinks.findings.length + sourceLinks.assets.length + sourceLinks.urls.length
|
|
891
1006
|
}
|
|
892
1007
|
|
|
893
1008
|
function errorDiagnostic(code: string, message: string, nodeId?: string): DeckPlanProjectionDiagnostic {
|
|
@@ -1017,12 +1132,6 @@ function renderDeckPlanSlideMarkdown(input: DeckPlanSlideUpsertInput & { id: str
|
|
|
1017
1132
|
lines.push("")
|
|
1018
1133
|
}
|
|
1019
1134
|
lines.push(renderSourceLinksMarkdown(sourceLinksForInput(input)).replace(/^####/gm, "##"))
|
|
1020
|
-
lines.push("## Caveats")
|
|
1021
|
-
lines.push("")
|
|
1022
|
-
const caveats = input.caveats?.filter((item) => item.trim()) ?? []
|
|
1023
|
-
if (caveats.length === 0) lines.push("- None.")
|
|
1024
|
-
else for (const caveat of caveats) lines.push(`- ${caveat.trim()}`)
|
|
1025
|
-
lines.push("")
|
|
1026
1135
|
return lines.join("\n")
|
|
1027
1136
|
}
|
|
1028
1137
|
|
|
@@ -1083,10 +1192,10 @@ function renderMinimalDeckPlanIndex(narrativeHash: string | undefined, slides: D
|
|
|
1083
1192
|
if (evidenceIds.length === 0) lines.push("- No evidence links planned yet.")
|
|
1084
1193
|
else for (const id of evidenceIds) lines.push(`- [[${id}]]`)
|
|
1085
1194
|
lines.push("")
|
|
1086
|
-
lines.push("##
|
|
1195
|
+
lines.push("## Source Limitations")
|
|
1087
1196
|
lines.push("")
|
|
1088
1197
|
const boundaryIds = uniqueStrings(slides.flatMap((slide) => slide.links.filter((link) => link.relation === "addresses_risk" || link.relation === "answers_objection" || link.relation === "mentions_gap").map((link) => link.id)))
|
|
1089
|
-
if (boundaryIds.length === 0) lines.push("- No risk, objection, or gap links planned
|
|
1198
|
+
if (boundaryIds.length === 0) lines.push("- No legacy risk, objection, or gap links planned.")
|
|
1090
1199
|
else for (const id of boundaryIds) lines.push(`- [[${id}]]`)
|
|
1091
1200
|
lines.push("")
|
|
1092
1201
|
lines.push("## Chapter Writing Batches")
|
|
@@ -1130,8 +1239,8 @@ function booleanField(frontmatter: Record<string, string | string[] | boolean>,
|
|
|
1130
1239
|
|
|
1131
1240
|
function arrayField(frontmatter: Record<string, string | string[] | boolean>, key: string): string[] {
|
|
1132
1241
|
const value = frontmatter[key]
|
|
1133
|
-
if (Array.isArray(value)) return value.map((item) => item.trim()).filter(
|
|
1134
|
-
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter(
|
|
1242
|
+
if (Array.isArray(value)) return value.map((item) => item.trim()).filter((item) => item && item.toLowerCase() !== "none")
|
|
1243
|
+
if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter((item) => item && item.toLowerCase() !== "none")
|
|
1135
1244
|
return []
|
|
1136
1245
|
}
|
|
1137
1246
|
|
|
@@ -1265,8 +1374,8 @@ function renderDeckPlanMarkdown(input: DeckPlanArtifactInput): string {
|
|
|
1265
1374
|
lines.push("")
|
|
1266
1375
|
lines.push("- Write one `<section class=\"slide\" data-slide-index=\"N\">` per planned slide in the completed deck, using positive 1-based slide indexes that are unique and strictly increase in DOM order. Partial chapter-by-chapter drafts may contain only the written prefix/range.")
|
|
1267
1376
|
lines.push("- Keep every rendered slide exactly 1920x1080px with no page-level scrollbars or hidden overflow.")
|
|
1268
|
-
lines.push("- Preserve claim-led chapters, visual intent, evidence ids, source trace,
|
|
1269
|
-
lines.push(
|
|
1377
|
+
lines.push("- Preserve claim-led chapters, visual intent, evidence ids, source trace, source limitations, unresolved inputs, and user review notes.")
|
|
1378
|
+
lines.push(`- Generate HTML in the listed writing batches; do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in one write or patch.`)
|
|
1270
1379
|
lines.push("")
|
|
1271
1380
|
lines.push("## Chapter Map")
|
|
1272
1381
|
lines.push("")
|
|
@@ -1284,14 +1393,17 @@ function renderDeckPlanMarkdown(input: DeckPlanArtifactInput): string {
|
|
|
1284
1393
|
}
|
|
1285
1394
|
lines.push("## Chapter Writing Batches")
|
|
1286
1395
|
lines.push("")
|
|
1287
|
-
lines.push(
|
|
1396
|
+
lines.push(`Use these batches for HTML generation. Each batch is capped at ${MAX_HTML_SLIDES_PER_BATCH} slide sections. Keep the HTML valid after every batch and preserve previously written slides.`)
|
|
1288
1397
|
lines.push("")
|
|
1289
1398
|
if (input.renderPlan) {
|
|
1290
|
-
for (const batch of input.renderPlan.chapterWritingBatches) lines.push(`- ${batch.label}: ${batch.chapterTitle}, slides ${formatSlideRange(batch.slideIndexes)}. ${batch.instructions}`)
|
|
1399
|
+
for (const batch of input.renderPlan.chapterWritingBatches) lines.push(`- ${batch.label}: ${batch.chapterTitle}, slides ${formatSlideRange(batch.slideIndexes)}; max ${batch.maxSlides} slides. ${batch.instructions}`)
|
|
1291
1400
|
} else {
|
|
1292
1401
|
input.chapters.forEach((chapter, index) => {
|
|
1293
1402
|
const prefix = index === 0 ? "Initial shell and first chapter" : `Chapter batch ${index + 1}`
|
|
1294
|
-
|
|
1403
|
+
for (const [chunkIndex, chunk] of chunkNumbers(chapter.slideIndexes, MAX_HTML_SLIDES_PER_BATCH).entries()) {
|
|
1404
|
+
const suffix = chunkIndex === 0 ? "" : ` part ${chunkIndex + 1}`
|
|
1405
|
+
lines.push(`- ${prefix}${suffix}: ${chapter.title}, slides ${formatSlideRange(chunk)}; max ${MAX_HTML_SLIDES_PER_BATCH} slides.`)
|
|
1406
|
+
}
|
|
1295
1407
|
})
|
|
1296
1408
|
}
|
|
1297
1409
|
if (input.renderPlan) {
|
|
@@ -93,6 +93,7 @@ export interface RenderPlanWritingBatch {
|
|
|
93
93
|
label: string
|
|
94
94
|
chapterTitle: string
|
|
95
95
|
slideIndexes: number[]
|
|
96
|
+
maxSlides: number
|
|
96
97
|
instructions: string
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -112,6 +113,7 @@ export interface RenderPlanContract {
|
|
|
112
113
|
|
|
113
114
|
type VisualIntentKind = "hero" | "toc" | "metric-stat" | "evidence-table" | "comparison-grid" | "risk-matrix" | "steps" | "text-only"
|
|
114
115
|
type ClaimChapterSlideKind = "framing" | "evidence" | "implication"
|
|
116
|
+
const MAX_HTML_SLIDES_PER_BATCH = 5
|
|
115
117
|
|
|
116
118
|
interface VisualIntent {
|
|
117
119
|
kind: VisualIntentKind
|
|
@@ -251,10 +253,11 @@ function buildDeckPlanRequirements(narrativeHash: string): DeckPlanRequirements
|
|
|
251
253
|
"Each substantive chapter should have framing, proof, and implication/boundary coverage.",
|
|
252
254
|
"Chapter divider or chapter TOC slides may use the toc component as structural wayfinding.",
|
|
253
255
|
"Do not create filler slides, repeated thesis pages, or generic bridge slides.",
|
|
254
|
-
"Preserve evidence ids, source trace,
|
|
256
|
+
"Preserve evidence ids, source trace, source limitations, unresolved inputs, and user review notes where available.",
|
|
255
257
|
"Do not render internal labels such as Evidence gap:, Unsupported scope:, Caveat:, Missing Data, or Evidence Boundary in executive body copy.",
|
|
256
258
|
"Do not infer plan structure from DECKS.json slides[]; it is compatibility cache only.",
|
|
257
|
-
"Use sourceLinks in slide blocks for materials, findings, assets,
|
|
259
|
+
"Use sourceLinks in slide blocks for materials, findings, assets, and URLs; do not use canonical ## Relations in deck-plan files.",
|
|
260
|
+
"Use `---` slide separators under ## Slides with slide-local metadata, followed by #### Content Plan, #### Source Links, and #### Design Plan.",
|
|
258
261
|
],
|
|
259
262
|
requiredSections: [
|
|
260
263
|
"Goal",
|
|
@@ -283,7 +286,7 @@ export function buildRenderPlanContract(deck: DeckSpec, chapters: DeckPlanChapte
|
|
|
283
286
|
"Render chapter divider slides with the toc component when slideKind is chapter-divider.",
|
|
284
287
|
"Chapter divider and global TOC slides are structural wayfinding and do not count toward central-claim substance.",
|
|
285
288
|
"Each central claim chapter needs non-structural framing, proof, and implication/boundary slides unless the current deck-plan projection explicitly says otherwise.",
|
|
286
|
-
|
|
289
|
+
`Generate HTML in chapter-bounded batches of at most ${MAX_HTML_SLIDES_PER_BATCH} slides, preserving valid HTML and already-written slides after every batch.`,
|
|
287
290
|
],
|
|
288
291
|
htmlIdentityContract: [
|
|
289
292
|
"Every written slide section uses class slide and a positive 1-based data-slide-index.",
|
|
@@ -306,18 +309,38 @@ export function buildRenderPlanContract(deck: DeckSpec, chapters: DeckPlanChapte
|
|
|
306
309
|
allowedStructuralSlides: chapter.sourceClaimId ? ["chapter-divider", "toc"] : ["cover", "toc", "ask"],
|
|
307
310
|
}
|
|
308
311
|
}),
|
|
309
|
-
chapterWritingBatches: chapters
|
|
310
|
-
label: index === 0 ? "Initial shell and first chapter" : `Chapter batch ${index + 1}`,
|
|
311
|
-
chapterTitle: chapter.title,
|
|
312
|
-
slideIndexes: chapter.slideIndexes,
|
|
313
|
-
instructions: index === 0
|
|
314
|
-
? "Create the stable HTML shell, required structural slides, and this first chapter range only."
|
|
315
|
-
: "Patch exactly this chapter range, preserve previously written slides, and keep the file valid after the patch.",
|
|
316
|
-
})),
|
|
312
|
+
chapterWritingBatches: chapterWritingBatches(chapters),
|
|
317
313
|
slideRenderMetadata: deck.slides.map((slide) => slideRenderMetadata(slide, chapters)),
|
|
318
314
|
}
|
|
319
315
|
}
|
|
320
316
|
|
|
317
|
+
function chapterWritingBatches(chapters: DeckPlanChapter[]): RenderPlanWritingBatch[] {
|
|
318
|
+
const batches: RenderPlanWritingBatch[] = []
|
|
319
|
+
for (const chapter of chapters) {
|
|
320
|
+
const chunks = chunkNumbers(chapter.slideIndexes, MAX_HTML_SLIDES_PER_BATCH)
|
|
321
|
+
for (let index = 0; index < chunks.length; index += 1) {
|
|
322
|
+
const chunk = chunks[index]
|
|
323
|
+
const chapterSuffix = chunks.length > 1 ? ` part ${index + 1}` : ""
|
|
324
|
+
batches.push({
|
|
325
|
+
label: batches.length === 0 ? `Initial shell and ${chapter.title}${chapterSuffix}` : `${chapter.title}${chapterSuffix}`,
|
|
326
|
+
chapterTitle: chapter.title,
|
|
327
|
+
slideIndexes: chunk,
|
|
328
|
+
maxSlides: MAX_HTML_SLIDES_PER_BATCH,
|
|
329
|
+
instructions: batches.length === 0
|
|
330
|
+
? `Create the stable HTML shell if needed, then write only slide sections ${formatSlideRange(chunk)}. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`
|
|
331
|
+
: `Patch only slide sections ${formatSlideRange(chunk)}, preserve previously written slides, and keep the file valid after the patch. Do not add or rewrite more than ${MAX_HTML_SLIDES_PER_BATCH} slide sections in this write.`,
|
|
332
|
+
})
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return batches
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function chunkNumbers(values: number[], size: number): number[][] {
|
|
339
|
+
const chunks: number[][] = []
|
|
340
|
+
for (let index = 0; index < values.length; index += size) chunks.push(values.slice(index, index + size))
|
|
341
|
+
return chunks
|
|
342
|
+
}
|
|
343
|
+
|
|
321
344
|
function slideRenderMetadata(slide: SlideSpec, chapters: DeckPlanChapter[]): RenderPlanSlideMetadata {
|
|
322
345
|
const chapter = chapters.find((item) => item.slideIndexes.includes(slide.index))
|
|
323
346
|
const slideKind = renderPlanSlideKind(slide, chapter)
|
|
@@ -1115,6 +1138,13 @@ function claimChapterTitle(claim: NarrativeClaim): string {
|
|
|
1115
1138
|
return title.endsWith(".") ? title.slice(0, -1) : title
|
|
1116
1139
|
}
|
|
1117
1140
|
|
|
1141
|
+
function formatSlideRange(indexes: number[]): string {
|
|
1142
|
+
if (indexes.length === 0) return "none"
|
|
1143
|
+
const sorted = [...indexes].sort((a, b) => a - b)
|
|
1144
|
+
if (sorted.length === 1) return String(sorted[0])
|
|
1145
|
+
return `${sorted[0]}-${sorted[sorted.length - 1]}`
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1118
1148
|
function hasCurrentApprovalOrOverride(narrative: NarrativeStateV1, narrativeHash: string): boolean {
|
|
1119
1149
|
return narrative.approvals.some((approval) => approval.narrativeHash === narrativeHash && (approval.scope === "narrative" && approval.approvedBy === "user" || approval.scope === "render_override" || approval.approvedBy === "override"))
|
|
1120
1150
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const NARRATIVE_VAULT_DIR = "revela-narrative"
|
|
2
|
-
export const NARRATIVE_VAULT_CACHE_DIR = ".
|
|
2
|
+
export const NARRATIVE_VAULT_CACHE_DIR = ".revela/narrative-cache"
|
|
3
3
|
|
|
4
4
|
export const NARRATIVE_VAULT_NODE_DIRS = ["claims", "evidence", "objections", "risks", "research-gaps"] as const
|
|
5
5
|
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { existsSync } from "fs"
|
|
2
2
|
import { join } from "path"
|
|
3
|
-
import {
|
|
3
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
|
|
4
|
+
import { NARRATIVE_VAULT_DIR } from "./constants"
|
|
4
5
|
|
|
5
6
|
export function narrativeVaultPath(workspaceRoot: string): string {
|
|
6
7
|
return join(workspaceRoot, NARRATIVE_VAULT_DIR)
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export function narrativeVaultCachePath(workspaceRoot: string): string {
|
|
10
|
-
return
|
|
11
|
+
return workspaceMetaPath(workspaceRoot, "narrative-cache")
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function existingNarrativeVaultCachePath(workspaceRoot: string): string {
|
|
15
|
+
return existingWorkspaceMetaPath(workspaceRoot, "narrative-cache")
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export function hasNarrativeVault(workspaceRoot: string): boolean {
|
package/lib/runtime/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice } from "../hook-
|
|
|
27
27
|
import { deckPlanDesignDiagnostics, readDeckPlanArtifact, upsertDeckPlanSlideArtifact, type DeckPlanSlideUpsertInput } from "../narrative-state/deck-plan-artifact"
|
|
28
28
|
import { extractDesignClasses } from "../design/designs"
|
|
29
29
|
import { recordRenderedArtifact, workspaceRelative } from "../workspace-state/rendered-artifacts"
|
|
30
|
+
import { existingWorkspaceMetaPath, workspaceMetaPath } from "../workspace-meta"
|
|
30
31
|
import { checkMaterialIntake, extractMaterial, materialIntakeNoticeForCommand, prepareLocalMaterials, recordMaterialReview } from "../material-intake"
|
|
31
32
|
import type { ReviewDeckOpenInput, ReviewDeckReadInput } from "./review"
|
|
32
33
|
import pkg from "../../package.json"
|
|
@@ -414,7 +415,7 @@ export function checkDesignRulesReadiness(input: RuntimeWorkspaceInput = {}): De
|
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
function recordDesignRulesRead(workspaceRoot: string, designName: string, rules: string): void {
|
|
417
|
-
const markerPath =
|
|
418
|
+
const markerPath = workspaceMetaPath(workspaceRoot, "codex-hooks", "design-rules-read.json")
|
|
418
419
|
mkdirSync(dirname(markerPath), { recursive: true })
|
|
419
420
|
writeFileSync(markerPath, JSON.stringify({
|
|
420
421
|
designName,
|
|
@@ -424,7 +425,7 @@ function recordDesignRulesRead(workspaceRoot: string, designName: string, rules:
|
|
|
424
425
|
}
|
|
425
426
|
|
|
426
427
|
function designRulesMarkerPath(workspaceRoot: string): string {
|
|
427
|
-
return
|
|
428
|
+
return existingWorkspaceMetaPath(workspaceRoot, "codex-hooks", "design-rules-read.json")
|
|
428
429
|
}
|
|
429
430
|
|
|
430
431
|
function hashDesignRules(rules: string): string {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { existsSync } from "fs"
|
|
2
|
+
import { join } from "path"
|
|
3
|
+
|
|
4
|
+
export const WORKSPACE_META_DIR = ".revela"
|
|
5
|
+
export const LEGACY_WORKSPACE_META_DIR = ".opencode/revela"
|
|
6
|
+
|
|
7
|
+
export function workspaceMetaPath(workspaceRoot: string, ...segments: string[]): string {
|
|
8
|
+
return join(workspaceRoot, WORKSPACE_META_DIR, ...segments)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function legacyWorkspaceMetaPath(workspaceRoot: string, ...segments: string[]): string {
|
|
12
|
+
return join(workspaceRoot, LEGACY_WORKSPACE_META_DIR, ...segments)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function existingWorkspaceMetaPath(workspaceRoot: string, ...segments: string[]): string {
|
|
16
|
+
const current = workspaceMetaPath(workspaceRoot, ...segments)
|
|
17
|
+
if (existsSync(current)) return current
|
|
18
|
+
const legacy = legacyWorkspaceMetaPath(workspaceRoot, ...segments)
|
|
19
|
+
return existsSync(legacy) ? legacy : current
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function workspaceMetaRelativePath(...segments: string[]): string {
|
|
23
|
+
return join(WORKSPACE_META_DIR, ...segments).replace(/\\/g, "/")
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isWorkspaceMetaRelativePath(path: string): boolean {
|
|
27
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\.\//, "")
|
|
28
|
+
return normalized === WORKSPACE_META_DIR ||
|
|
29
|
+
normalized.startsWith(`${WORKSPACE_META_DIR}/`) ||
|
|
30
|
+
normalized === ".opencode" ||
|
|
31
|
+
normalized.startsWith(".opencode/")
|
|
32
|
+
}
|
package/package.json
CHANGED
package/plugin.ts
CHANGED
|
@@ -98,6 +98,7 @@ import { extractDesignClasses } from "./lib/design/designs"
|
|
|
98
98
|
import { log, childLog } from "./lib/log"
|
|
99
99
|
import { appendToolResult } from "./lib/tool-result"
|
|
100
100
|
import { formatArtifactQaUserNotice, formatMarkdownQaUserNotice, formatStateGateUserNotice } from "./lib/hook-notifications"
|
|
101
|
+
import { workspaceMetaPath, workspaceMetaRelativePath } from "./lib/workspace-meta"
|
|
101
102
|
|
|
102
103
|
// OpenCode internal agent signatures — used to skip system prompt injection
|
|
103
104
|
// for built-in system agents (title, summary, compaction).
|
|
@@ -712,7 +713,7 @@ const server: Plugin = (async (pluginCtx) => {
|
|
|
712
713
|
if (input.tool === "write") {
|
|
713
714
|
const filePath: string = (output.args as any)?.filePath ?? ""
|
|
714
715
|
if (isDecksStatePath(filePath)) {
|
|
715
|
-
const blockedDir =
|
|
716
|
+
const blockedDir = workspaceMetaPath(workspaceRoot, "blocked-writes")
|
|
716
717
|
mkdirSync(blockedDir, { recursive: true })
|
|
717
718
|
const blockedPath = join(blockedDir, "DECKS-json-direct-write.blocked.md")
|
|
718
719
|
const blocker = `${DECKS_STATE_FILE} is a controlled Revela state file. Use the revela-decks tool instead of write/apply_patch.`
|
|
@@ -738,9 +739,9 @@ Next step: use \`revela-decks\` with action \`init\`, \`upsertDeck\`, \`upsertSl
|
|
|
738
739
|
|
|
739
740
|
const stateTargets = extractDecksStateTargetsFromPatch(patchText)
|
|
740
741
|
if (stateTargets.length > 0) {
|
|
741
|
-
const blockedDir =
|
|
742
|
+
const blockedDir = workspaceMetaPath(workspaceRoot, "blocked-writes")
|
|
742
743
|
mkdirSync(blockedDir, { recursive: true })
|
|
743
|
-
const blockedRelativePath =
|
|
744
|
+
const blockedRelativePath = workspaceMetaRelativePath("blocked-writes", `DECKS-json-direct-patch-${Date.now()}.blocked.md`)
|
|
744
745
|
const blocker = `${DECKS_STATE_FILE} is a controlled Revela state file. Use the revela-decks tool instead of write/apply_patch.`
|
|
745
746
|
const blockedPatch = `*** Begin Patch
|
|
746
747
|
*** Add File: ${blockedRelativePath}
|
package/plugins/revela/.mcp.json
CHANGED
|
@@ -22,7 +22,7 @@ export async function runPreWriteChecks(input: string): Promise<HookResult> {
|
|
|
22
22
|
messages.push([
|
|
23
23
|
"Revela narrative cache patches are blocked.",
|
|
24
24
|
`Controlled cache target(s): ${cacheTargets.map((target) => `\`${target}\``).join(", ")}`,
|
|
25
|
-
"Edit `revela-narrative/**/*.md` instead; compile/cache files under `.
|
|
25
|
+
"Edit `revela-narrative/**/*.md` instead; compile/cache files under `.revela/narrative-cache/` are regenerated.",
|
|
26
26
|
].join("\n"))
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -67,7 +67,7 @@ export function extractDeckHtmlPatchTargets(input: string): string[] {
|
|
|
67
67
|
export function extractNarrativeCachePatchTargets(input: string): string[] {
|
|
68
68
|
const targets = new Set<string>()
|
|
69
69
|
for (const patch of patchPayloads(input)) {
|
|
70
|
-
const pattern = /(?:^\*\*\* Update File: |^\*\*\* Add File: |^\*\*\* Delete File: |^\*\*\* Move to: )([^\r\n]
|
|
70
|
+
const pattern = /(?:^\*\*\* Update File: |^\*\*\* Add File: |^\*\*\* Delete File: |^\*\*\* Move to: )([^\r\n]*(?:\.revela|\.opencode\/revela)\/narrative-cache\/[^\r\n]+)\s*$/gm
|
|
71
71
|
let match: RegExpExecArray | null
|
|
72
72
|
while ((match = pattern.exec(patch))) targets.add(match[1].trim())
|
|
73
73
|
}
|
|
@@ -19,13 +19,14 @@ Use this skill when the user asks to plan, make, generate, or update a Revela de
|
|
|
19
19
|
1. For planning requests, inspect local materials/reviews/research, then write or repair `deck-plan.md` directly. Do not use structured upsert tools for normal plan authoring.
|
|
20
20
|
2. Call `revela_design_list`, `revela_design_read` with `section: "rules"`, and `revela_design_inventory` before selecting layouts/components; use the returned layout slots and component nesting hints in `deck-plan.md`.
|
|
21
21
|
3. Call `revela_read_deck_plan` after writing or repairing `deck-plan.md`. If diagnostics report layout, slot, component, `children`, or `sourceLinks` issues, patch the Markdown directly and call `revela_read_deck_plan` again.
|
|
22
|
-
4. Call `revela_read_deck_plan` before HTML generation and follow the current projection. `revela_read_deck_plan` is QA/diagnostics, not a writer.
|
|
22
|
+
4. Call `revela_read_deck_plan` before HTML generation and follow the current projection. Read `htmlWritingBatches` from the result before any HTML write. `revela_read_deck_plan` is QA/diagnostics, not a writer.
|
|
23
23
|
5. For new HTML files, call `revela_create_deck_foundation`.
|
|
24
24
|
6. Before patching slide HTML, read the specific layouts/components with `revela_design_read_layout` and `revela_design_read_component`; fetch chart rules before ECharts.
|
|
25
25
|
7. Patch slides into the foundation between Revela slide markers. Preserve positive 1-based `data-slide-index` values. Every slide must have exactly one direct `.slide-canvas` child.
|
|
26
|
-
8. Generate
|
|
27
|
-
9.
|
|
26
|
+
8. Generate one `htmlWritingBatches` entry at a time. A single HTML write/edit/apply_patch may add or rewrite at most 5 slide sections. If a chapter is longer than 5 slides, use the consecutive batch parts returned by `revela_read_deck_plan`.
|
|
27
|
+
9. Keep the HTML valid after each write.
|
|
28
|
+
10. After every HTML write, call `revela_run_deck_qa` and repair hard errors before review or export.
|
|
28
29
|
|
|
29
30
|
## Deck Plan Requirements
|
|
30
31
|
|
|
31
|
-
Every normal deck plan should include Cover, Table of Contents, and Closing. Use 3-5 chapter headings, explicit slide ranges, `
|
|
32
|
+
Every normal deck plan should include Cover, Table of Contents, and Closing. Use 3-5 chapter headings, explicit slide ranges, and `---` slide separators under `## Slides`. Each slide block should have slide-local metadata followed by `#### Content Plan`, `#### Source Links` for materials/findings/assets/URLs, and `#### Design Plan` for layout, component plan, visual intent, placement notes, and render notes. Use unresolved inputs, source limitations, and user review notes instead of AI-authored caveat/risk judgement. Use `box.children` when multiple child components support one semantic idea; do not duplicate the same child as both nested and top-level.
|
package/skill/SKILL.md
CHANGED
|
@@ -78,12 +78,14 @@ Batches, slide plan, visual intent, evidence trace, boundaries, and narrative
|
|
|
78
78
|
links. Do not call `compileDeckPlan` merely to understand an existing plan, and
|
|
79
79
|
do not reinterpret cached `DECKS.json.slides[]` as the render contract.
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
Deck HTML must be generated in bounded batches, not in one broad `write` or
|
|
82
|
+
`apply_patch` call. Follow `htmlWritingBatches` from `readDeckPlan`; each HTML
|
|
83
|
+
write/edit/apply_patch may add or rewrite at most 5 `<section class="slide">`
|
|
84
|
+
blocks. The first HTML write may create the stable HTML shell, but its slide
|
|
85
|
+
sections are still capped at 5. Subsequent writes must patch only the next
|
|
86
|
+
listed batch, preserving already-written slides and keeping the file valid after
|
|
87
|
+
every write. Do not continue to the next batch while the current file has
|
|
88
|
+
Artifact QA hard errors.
|
|
87
89
|
|
|
88
90
|
---
|
|
89
91
|
|
|
@@ -118,15 +120,17 @@ Before writing HTML, the deck-plan projection should include:
|
|
|
118
120
|
|
|
119
121
|
- `deck-plan.md` with `designName`, `outputPath` when known, chapter map, and
|
|
120
122
|
ordered slide blocks.
|
|
121
|
-
- Each slide block uses `sourceLinks` for materials, findings, assets, URLs
|
|
122
|
-
|
|
123
|
+
- Each slide block uses `sourceLinks` for materials, findings, assets, and URLs.
|
|
124
|
+
Legacy narrative/caveat links may be read for compatibility, but new
|
|
123
125
|
plans should not use them.
|
|
126
|
+
- Use `---` slide separators under `## Slides` with slide-local metadata, then
|
|
127
|
+
`#### Content Plan`, `#### Source Links`, and `#### Design Plan`.
|
|
124
128
|
- `Required structure: Cover + Table of Contents + Closing`.
|
|
125
129
|
- A `Chapters` section with 3-5 TOC headings, slide ranges, and the
|
|
126
130
|
non-structural slides assigned to each chapter.
|
|
127
131
|
- One row/block per slide with title, purpose, narrative role, content summary,
|
|
128
132
|
layout, components, `sourceLinks`, visual intent, visual brief, render notes,
|
|
129
|
-
and
|
|
133
|
+
unresolved inputs, source limitations, and user review notes.
|
|
130
134
|
- Source Authority, Chapter Map, Slides, Unresolved Inputs, and HTML Contract
|
|
131
135
|
sections.
|
|
132
136
|
- A low-fidelity layout sketch for every slide when requested by the handoff
|
|
@@ -167,14 +171,16 @@ a required workflow gate.
|
|
|
167
171
|
|
|
168
172
|
## Chapter-By-Chapter Generation
|
|
169
173
|
|
|
170
|
-
Generate the artifact
|
|
171
|
-
one
|
|
174
|
+
Generate the artifact by following `htmlWritingBatches`. Never add or rewrite
|
|
175
|
+
more than 5 slide sections in one `write`, `edit`, or `apply_patch` call.
|
|
172
176
|
|
|
173
177
|
For decks with 5 or more slides:
|
|
174
178
|
|
|
175
179
|
- First call `revela-deck-foundation` for new files, then patch structural
|
|
176
|
-
slides and the first
|
|
177
|
-
- Then fill or revise exactly one
|
|
180
|
+
slides and the first listed batch between the `revela-slides` markers.
|
|
181
|
+
- Then fill or revise exactly one listed batch at a time.
|
|
182
|
+
- If a chapter has more than 5 slides, split it into consecutive batches from
|
|
183
|
+
`htmlWritingBatches`.
|
|
178
184
|
- Do not mix multiple central-claim chapters in the same write.
|
|
179
185
|
- Chapter divider or chapter TOC slides are allowed as structural wayfinding and
|
|
180
186
|
should usually use the `toc` component.
|
|
@@ -292,15 +298,15 @@ patch and rerun QA before considering the deck ready.
|
|
|
292
298
|
|
|
293
299
|
## Evidence And Source Rules
|
|
294
300
|
|
|
295
|
-
- Do not invent quotes, URLs, page references, source paths,
|
|
296
|
-
ids.
|
|
297
|
-
- Preserve
|
|
298
|
-
visible in
|
|
301
|
+
- Do not invent quotes, URLs, page references, source paths, source limitations,
|
|
302
|
+
user review notes, or evidence ids.
|
|
303
|
+
- Preserve source trace, explicit source limitations, and unresolved inputs when
|
|
304
|
+
visible in deck-plan source context or slide specs.
|
|
299
305
|
- Evidence-sensitive claims need visible evidence/source context when available.
|
|
300
306
|
- Never stretch partial evidence into support for future-state, recommendation,
|
|
301
307
|
roadmap, or product-vision claims.
|
|
302
|
-
- Keep missing evidence visible as
|
|
303
|
-
it with assumptions.
|
|
308
|
+
- Keep missing evidence visible as an unresolved input, source limitation, user
|
|
309
|
+
review note, or blocker instead of filling it with assumptions.
|
|
304
310
|
- Do not render internal evidence diagnostics as executive-facing body copy.
|
|
305
311
|
Avoid labels such as `Evidence gap:`, `Unsupported scope:`, `Caveat:`,
|
|
306
312
|
`Missing Data`, or `Evidence Boundary` in normal slide text unless the user
|
package/tools/workspace-scan.ts
CHANGED
|
@@ -13,7 +13,7 @@ const DOC_EXTENSIONS = new Set([
|
|
|
13
13
|
|
|
14
14
|
// Directories to exclude from scanning
|
|
15
15
|
const EXCLUDE_DIRS = new Set([
|
|
16
|
-
"node_modules", ".git", "dist", ".opencode",
|
|
16
|
+
"node_modules", ".git", "dist", ".opencode", ".revela",
|
|
17
17
|
"researches", // Exclude revela's own research output
|
|
18
18
|
"revela-narrative", // Exclude canonical narrative vault source files
|
|
19
19
|
"designs", "domains", // Exclude revela plugin assets
|