Compare commits

...

6 Commits

Author SHA1 Message Date
songtianyi.theo
84c19b83f0 docs: require hero imagery for svg slide roles 2026-07-08 15:47:52 +08:00
songtianyi.theo
85c07a990e docs: align svglide image resource boundary 2026-07-08 15:42:33 +08:00
songtianyi.theo
a617d168c4 docs: move svglide plans out of skill references 2026-07-08 15:41:45 +08:00
songtianyi.theo
d7cd8e1b37 docs: harden create-svglide validation plan 2026-07-08 15:33:09 +08:00
songtianyi.theo
0e98163562 docs: align svg slides canvas to 960x540 2026-07-08 14:08:26 +08:00
songtianyi.theo
e3e5a0b723 docs: add svg slides local generation workflow 2026-07-08 13:58:27 +08:00
27 changed files with 7264 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
# Lark Slides Development Plans
本目录只保存开发阶段计划、评审记录和执行拆解,不属于 `lark-slides` skill 的运行时提示词或工具使用参考。
约束:
- 不要从 `skills/lark-slides/SKILL.md` 路由到本目录。
- 不要把本目录内容作为 agent 调用 `slides` 工具时的稳定协议或操作规范。
- 当计划中的结论沉淀为长期有效规则时,先拆成原子化、可验证的运行时说明,再放入 `skills/lark-slides/references/` 或其下的专题目录。
- 当计划仅用于阶段性开发判断时,继续留在本目录,避免污染工具提示词边界。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ metadata:
| 用户需求 | 优先动作 | 关键文档 / 命令 |
|----------|----------|-----------------|
| 新建 PPT | 先规划 `slide_plan.json`,再按复杂度选择一步或两步创建 | `planning-layer.md``visual-planning.md``asset-planning.md``slides +create` |
| 本地生成或校验 SVG Slides / SVGlide 产物 | 先读 `references/svg-slides/README.md`,生成 local publish-ready bundle发布层另走 `+create-svglide` 后续计划 | `references/svg-slides/README.md``scripts/validate_svg_deck.mjs``scripts/svg_slides_bundle.mjs` |
| 已有 PPT 大幅改写 | 多页整页重建用 `+replace-pages`,单页局部编辑用 `+replace-slide` | `xml_presentations.get``lark-slides-replace-pages.md``lark-slides-edit-workflows.md` |
| 编辑单个标题、文本块、图片或局部元素 | 优先块级替换/插入,不改页序 | `slides +replace-slide``lark-slides-replace-slide.md` |
| 读取或分析已有 PPT | 解析 slides/wiki token回读全文或单页 XML保存 `xml_presentation_id``slide_id``revision_id` | `xml_presentations.get``xml_presentation.slide.get` |
@@ -29,6 +30,8 @@ metadata:
**CRITICAL — 生成任何 XML 之前MUST 先用 Read 工具读取 [xml-schema-quick-ref.md](references/xml-schema-quick-ref.md),禁止凭记忆猜测 XML 结构。**
**CRITICAL — SVG Slides / SVGlide 与当前 XML/SXSD 工作流是不同协议。两者都使用 960x540 画布,但 SVG Slides 使用 `viewBox="0 0 960 540"` 和 `slide:*` SVG 语义XML/SXSD 使用 SML XML。处理 SVG Slides 生成或校验时,先读 [`references/svg-slides/README.md`](references/svg-slides/README.md),不要把 SVG 规则写进 `xml-schema-quick-ref.md`。**
**CRITICAL — PPT 生成与模板编辑硬约束PPT 的尺寸是 960x540确保主体内容在页面边界内。多用生图辅助搜图必须要图文并茂。不要为了画出一个具象物体而堆叠 3 个以上仅用于拟形的 shape。生成背景图时必须在 prompt 中明确要求不要出现任何文字。用户指定 PPT 模板时,用 lark-drive 技能导入成 lark slides回读理解每页版式后直接在该 slides 上编辑,可以填改文字和图片、按需增删模板页,必须严格沿用原版式和字体,只改内容不做设计,完成后回读并微调,凝练文字或缩减字号消除文字溢出,调整 shape 顺序或位置避免文字遮挡。**
**CRITICAL — 新建演示文稿或大幅改写页面时MUST 先生成 `.lark-slides/plan/<deck-or-task-id>/slide_plan.json`,再生成 XML。先创建对应目录规划层规则和中间产物生命周期见 [planning-layer.md](references/planning-layer.md)。仅替换一个标题、插入一个块等小型已有页编辑可豁免。**

View File

@@ -0,0 +1,160 @@
# create-svglide boundary study
## Goal
Use `slides +create` as the design constraint sample for `slides +create-svglide`.
The central rule is:
```text
slides +create is a thin publisher for already-authored slide XML.
slides +create-svglide should be a thin publisher for already-authored SVGlide artifacts.
```
This document is evidence-first. It separates what the existing shortcut actually does from the broader generation and validation work described by the `lark-slides` skill.
## Source Surface
| Area | Files | Why it matters |
| --- | --- | --- |
| Go shortcut implementation | `shortcuts/slides/slides_create.go`, `shortcuts/slides/helpers.go`, `shortcuts/slides/slides_media_upload.go`, `shortcuts/slides/shortcuts.go` | Shows the real runtime boundary of `slides +create`. |
| Unit tests | `shortcuts/slides/slides_create_test.go` | Shows behavior that must not drift silently. |
| E2E proof | `tests/cli_e2e/slides/slides_create_workflow_test.go`, `tests/cli_e2e/slides/coverage.md` | Shows what is proven outside the shortcut body. |
| Skill and references | `skills/lark-slides/SKILL.md`, `skills/lark-slides/references/lark-slides-create.md`, `xml-schema-quick-ref.md`, `validation-checklist.md`, `troubleshooting.md` | Shows which work belongs to agent guidance or scripts instead of Go shortcut code. |
## `slides +create` Responsibility Matrix
| Responsibility | Evidence | Boundary meaning |
| --- | --- | --- |
| Register a write shortcut named `slides +create` for user and bot auth | `shortcuts/slides/slides_create.go:24-43`, `shortcuts/slides/shortcuts.go:8-17` | The command is a shortcut wrapper, not a general slide-generation subsystem. |
| Build a minimal presentation XML shell | `shortcuts/slides/slides_create.go:224-241` | The shortcut creates only the deck container: title plus 960x540 presentation metadata. |
| Create the online XML presentation | `shortcuts/slides/slides_create.go:125-148` | The first real API call is presentation creation. |
| Accept optional `--slides` as a JSON array of `<slide>` XML strings | `shortcuts/slides/slides_create.go:40-52` | Page content is supplied by the caller as final XML strings. |
| Enforce a maximum of 10 inline slide XML strings | `shortcuts/slides/slides_create.go:50-52` | Larger decks must use the lower-level page-create API after container creation. |
| Detect local image placeholders in submitted XML | `shortcuts/slides/helpers.go:113-153` | The shortcut only interprets one small XML convention: `<img src=\"@path\">`. |
| Validate placeholder files before creating the presentation | `shortcuts/slides/slides_create.go:53-67` | Avoids creating an orphan deck for missing/oversized local images. |
| Upload placeholder images and replace them with file tokens | `shortcuts/slides/slides_create.go:163-177`, `shortcuts/slides/slides_media_upload.go:119-138`, `shortcuts/slides/helpers.go:283-309` | Image upload is helper orchestration, not content generation. |
| Submit each supplied slide XML string to the page-create API | `shortcuts/slides/slides_create.go:179-200` | The shortcut forwards caller-authored XML to the backend. |
| Report partial progress when page creation fails | `shortcuts/slides/slides_create.go:194-196`, `shortcuts/slides/slides_create_test.go:354-420` | It does not roll back; it tells the caller where to resume. |
| Output machine-readable creation results | `shortcuts/slides/slides_create.go:150-219` | The output is an API orchestration receipt. |
| Optionally attempt bot-created deck permission grant | `shortcuts/slides/slides_create.go:215-217`, `shortcuts/slides/slides_create_test.go:66-198` | Bot grant is post-create convenience, not part of content semantics. |
## Behavior Locks From Tests
| Behavior | Evidence | Boundary meaning |
| --- | --- | --- |
| User-mode create returns `xml_presentation_id`, `title`, and `url`, without `permission_grant` | `shortcuts/slides/slides_create_test.go:23-63` | User-mode output is a creation receipt, not a validation report. |
| Missing `--title` becomes `Untitled` in dry-run and execution | `shortcuts/slides/slides_create_test.go:200-253` | Title normalization is a small deterministic convenience that belongs in the shortcut. |
| `--slides` creates the deck first, then adds pages, then returns `slide_ids` and `slides_added` | `shortcuts/slides/slides_create_test.go:285-352` | Page creation is orchestration after container creation. |
| `--slides []` behaves like no slides | `shortcuts/slides/slides_create_test.go:532-570` | Empty artifact lists should be explicit no-op additions, not special generators. |
| Invalid JSON and more than 10 inline slides fail validation with `Param == "--slides"` | `shortcuts/slides/slides_create_test.go:422-505` | Input-contract errors should be structured and routeable. |
| Missing `xml_presentation_id` from the backend fails | `shortcuts/slides/slides_create_test.go:255-283` | Creation success requires a usable resource id. |
| URL fallback is local and does not call Drive metas or batch query | `shortcuts/slides/slides_create_test.go:649-688` | Avoid adding extra API dependencies when a local receipt can be built. |
| Image placeholders are uploaded once per unique path and rewritten before page creation | `shortcuts/slides/slides_create_test.go:751-854` | Asset handling is publish-boundary plumbing, not design work. |
| Missing local placeholder files fail before any API call | `shortcuts/slides/slides_create_test.go:856-877` | Local artifact existence is a publish-blocking precondition. |
| Dry-run exposes the API plan shape and placeholder ids | `shortcuts/slides/slides_create_test.go:572-602`, `shortcuts/slides/slides_create_test.go:879-900` | Dry-run should describe orchestration, not execute validation-heavy side effects. |
| Readback is proven by E2E as a separate follow-up call | `tests/cli_e2e/slides/slides_create_workflow_test.go:32-85`, `tests/cli_e2e/slides/coverage.md:9-16` | Readback is evidence for tests and delivery, not default `Execute` behavior. |
| Bot permission grant is non-fatal and tri-state: granted, skipped, or failed | `shortcuts/slides/slides_create_test.go:66-198` | Convenience post-actions must not turn creation success into failure. |
## `slides +create` Does Not Do
| Non-responsibility | Evidence | Design implication for `+create-svglide` |
| --- | --- | --- |
| Does not generate slide XML from a prompt | `shortcuts/slides/slides_create.go:40-43`, `shortcuts/slides/slides_create.go:158-205` | `+create-svglide` must not become `--topic -> deck`. |
| Does not deeply validate slide XML semantics | `shortcuts/slides/slides_create.go:44-69` | Only minimal publish-blocking validation belongs in the shortcut. |
| Does not preview or repair layout | `shortcuts/slides/slides_create.go:125-221` | Preview and repair belong in skill/scripts or a runner before publish. |
| Does not run readback inside `Execute` | `shortcuts/slides/slides_create.go:125-221`, `tests/cli_e2e/slides/slides_create_workflow_test.go:68-85` | Readback is a test/proof step, not default shortcut behavior. |
| Does not guarantee atomic creation | `shortcuts/slides/slides_create.go:194-196`, `shortcuts/slides/slides_create_test.go:354-420` | New publish shortcuts should provide recovery context, not hide partial success. |
| Does not handle more than 10 inline pages | `shortcuts/slides/slides_create.go:18-21`, `shortcuts/slides/slides_create_test.go:441-465` | Bound the first version instead of building a complex batch manager. |
| Does not own visual quality | `skills/lark-slides/SKILL.md:91-127`, `skills/lark-slides/SKILL.md:153-160` | Visual quality gates belong before the shortcut consumes artifacts. |
## Counterexamples
| Tempting requirement | Why it looks tempting | What `slides +create` teaches |
| --- | --- | --- |
| Add readback by default | E2E uses readback to prove persistence. | E2E calls the get API after creation; `Execute` itself stops after outputting the create result. Keep readback optional or outside MVP. |
| Validate every page semantically before calling the backend | Better local errors sound useful. | `+create` only validates JSON shape, count, and local placeholder files; backend owns XML parsing. For SVGlide, only validate fields required to route and publish. |
| Run preview lint and auto-repair | SVGlide has preview tooling. | `+create` does not make layout judgments. Preview lint and repair must remain pre-publish tooling. |
| Accept a prompt and generate the deck | Higher-level UX is attractive. | `+create` consumes final submission artifacts. A prompt-to-deck runner would be a different command or script layer. |
| Hide partial failures by retrying/rebuilding automatically | It feels friendlier. | `+create` surfaces partial progress instead. Recovery should be explicit and resumable. |
## `slides +create-svglide` Allowed Extra Responsibilities
`+create-svglide` can be slightly heavier than `+create` only where SVGlide's input contract requires it. The extra work must still be publish-boundary work, not generation work.
| Extra responsibility | Allowed because | Limit |
| --- | --- | --- |
| Read a SVGlide manifest or run directory | Unlike `--slides`, SVGlide artifacts are file-based. | Normalize to one manifest model immediately; do not infer design intent. |
| Validate manifest schema and page order | Needed to know what to publish. | Validate shape and required fields only. |
| Validate page file existence and path safety | Equivalent to `+create` validating `@path` placeholders. | Do not inspect aesthetics or text quality. |
| Validate publish-required SVGlide fields | The target publish API or parser may require namespace, contract/version, dimensions, or roles before it can accept a page. | Check only required markers; do not rewrite ordinary SVG into protocol SVG in the shortcut. |
| Upload declared local assets | Equivalent to `+create` uploading `@path` images. | Upload and token replacement only; no asset search or generation. |
| Submit SVGlide pages to the target publish API | Equivalent to `+create` submitting each slide XML string. | Keep output and partial-progress behavior explicit; do not assume the CLI must convert to XML if the backend can consume SVGlide directly. |
## `slides +create-svglide` Must Not Own
| Responsibility | Owner |
| --- | --- |
| Research, outline, design brief, slide content planning | `skills/lark-slides` guidance and external runner/scripts |
| SVG authoring | Agent or runner before publish |
| Preview rendering, preview lint, and repair loop | Skill scripts or runner before publish |
| Visual quality scoring | Skill/scripts/quality gate, not shortcut `Execute` |
| Readback as default success criterion | E2E or optional verification flag |
| PPE/Whistle routing as core naming | Environment/profile layer only |
## MVP Scope
Recommended first implementation:
```bash
lark-cli slides +create-svglide --manifest ./svglide-run/manifest.json --as user
```
MVP behavior:
1. Parse manifest.
2. Validate required fields, page order, file existence, path safety, dimensions, and minimal SVGlide contract markers.
3. Create presentation shell.
4. Upload local assets declared in the manifest.
5. Submit pages to the backend.
6. Output `xml_presentation_id`, `url`, `page_ids` or `slide_ids`, uploaded asset count, and partial-progress context on failure.
MVP exclusions:
1. No prompt input.
2. No generation stages.
3. No preview repair.
4. No default readback.
5. No PPE-specific command name, directory name, or type name.
## Test Boundary For `+create-svglide`
The first test suite should mirror the shape of `slides +create` tests instead of proving the whole SVGlide generation pipeline.
| Test area | Required proof |
| --- | --- |
| Input contract | Invalid manifest, missing page file, unsafe path, and unsupported page count fail with structured params. |
| Dry-run | Shows create, asset upload, and page publish steps with placeholder presentation id and deterministic step labels. |
| Asset handling | Duplicate local assets upload once; page payloads reference uploaded tokens before publish. |
| Partial failure | If the deck exists and page N fails, error includes presentation id, failed page index, and successfully published page count. |
| Bot grant | Inherit user/bot output behavior from `slides +create`; grant failure is reported but not promoted to create failure. |
| E2E | Create/publish result is asserted first; optional readback is a separate proof step unless the command explicitly adds a `--readback` contract. |
## Team Finding
The effective research team for this boundary is:
| Role | Scope |
| --- | --- |
| Code Reader | Extract runtime responsibilities from Go implementation. |
| Test Reader | Extract behavior locks and prove what is outside `Execute`. |
| Skill Boundary Reader | Separate agent/script responsibilities from shortcut responsibilities. |
| Architect/Skeptic | Reject over-broad scope and map only proven `+create` patterns into `+create-svglide`. |
The team's proof standard is not "we read the files"; it is:
```text
Every proposed +create-svglide responsibility must map to either:
1. an existing +create responsibility, or
2. a minimal extra responsibility forced by SVGlide's artifact input shape.
```

View File

@@ -0,0 +1,160 @@
# create-svglide 边界研究
## 目标
`slides +create` 作为 `slides +create-svglide` 的设计约束样本。
核心规则是:
```text
slides +create 是已经写好的 slide XML 的薄发布器。
slides +create-svglide 也应该是已经生成好的 SVGlide 产物的薄发布器。
```
本文档以证据为先,区分现有 shortcut 真实承担的职责,以及 `lark-slides` skill 中描述的更宽泛的生成与验证工作。
## 研究范围
| 范围 | 文件 | 作用 |
| --- | --- | --- |
| Go shortcut 实现 | `shortcuts/slides/slides_create.go``shortcuts/slides/helpers.go``shortcuts/slides/slides_media_upload.go``shortcuts/slides/shortcuts.go` | 确认 `slides +create` 的真实运行时边界。 |
| 单元测试 | `shortcuts/slides/slides_create_test.go` | 确认可被测试锁定、不能随意漂移的行为。 |
| E2E 证明 | `tests/cli_e2e/slides/slides_create_workflow_test.go``tests/cli_e2e/slides/coverage.md` | 确认哪些证明发生在 shortcut 外部。 |
| Skill 与 references | `skills/lark-slides/SKILL.md``skills/lark-slides/references/lark-slides-create.md``xml-schema-quick-ref.md``validation-checklist.md``troubleshooting.md` | 确认哪些工作属于 agent 指导或脚本,而不是 Go shortcut。 |
## `slides +create` 职责矩阵
| 职责 | 证据 | 边界含义 |
| --- | --- | --- |
| 注册一个名为 `slides +create` 的写操作 shortcut支持 user 和 bot 身份 | `shortcuts/slides/slides_create.go:24-43``shortcuts/slides/shortcuts.go:8-17` | 这是 shortcut 封装,不是通用幻灯片生成系统。 |
| 构造最小 presentation XML 外壳 | `shortcuts/slides/slides_create.go:224-241` | shortcut 只创建 deck 容器:标题和 960x540 presentation 元数据。 |
| 创建在线 XML presentation | `shortcuts/slides/slides_create.go:125-148` | 第一个真实 API 调用是创建 presentation。 |
| 接收可选 `--slides`,格式为 `<slide>` XML 字符串 JSON 数组 | `shortcuts/slides/slides_create.go:40-52` | 页面内容由调用方以最终 XML 字符串形式提供。 |
| 限制一次内联提交最多 10 页 slide XML | `shortcuts/slides/slides_create.go:50-52` | 更大的 deck 应先创建容器,再用底层 page-create API 追加页面。 |
| 检测已提交 XML 里的本地图片占位符 | `shortcuts/slides/helpers.go:113-153` | shortcut 只理解一个很窄的 XML 约定:`<img src="@path">`。 |
| 创建 presentation 前校验占位符文件 | `shortcuts/slides/slides_create.go:53-67` | 避免因为本地图片缺失或超限而创建孤儿 deck。 |
| 上传占位符图片并替换为 file token | `shortcuts/slides/slides_create.go:163-177``shortcuts/slides/slides_media_upload.go:119-138``shortcuts/slides/helpers.go:283-309` | 图片上传是发布边界上的 helper 编排,不是内容生成。 |
| 把每个调用方提供的 slide XML 字符串提交给 page-create API | `shortcuts/slides/slides_create.go:179-200` | shortcut 把调用方写好的 XML 转交给后端。 |
| 页面创建失败时报告部分进度 | `shortcuts/slides/slides_create.go:194-196``shortcuts/slides/slides_create_test.go:354-420` | 不回滚;告诉调用方从哪里恢复。 |
| 输出机器可读的创建结果 | `shortcuts/slides/slides_create.go:150-219` | 输出是 API 编排回执。 |
| bot 创建 deck 后可选尝试给当前用户授权 | `shortcuts/slides/slides_create.go:215-217``shortcuts/slides/slides_create_test.go:66-198` | bot grant 是创建后的便利动作,不属于内容语义。 |
## 测试锁定的行为
| 行为 | 证据 | 边界含义 |
| --- | --- | --- |
| user 模式创建返回 `xml_presentation_id``title``url`,不返回 `permission_grant` | `shortcuts/slides/slides_create_test.go:23-63` | user 模式输出是创建回执,不是验证报告。 |
| 省略 `--title`dry-run 和真实执行都归一为 `Untitled` | `shortcuts/slides/slides_create_test.go:200-253` | 标题归一是适合放在 shortcut 内的小型确定性便利。 |
| `--slides` 会先创建 deck再添加页面最后返回 `slide_ids``slides_added` | `shortcuts/slides/slides_create_test.go:285-352` | 页面创建是容器创建后的编排。 |
| `--slides []` 等价于不传 slides | `shortcuts/slides/slides_create_test.go:532-570` | 空产物列表应是明确的无追加操作,不应触发特殊生成逻辑。 |
| 非法 JSON 和超过 10 个内联 slides 会以 `Param == "--slides"` 的校验错误失败 | `shortcuts/slides/slides_create_test.go:422-505` | 输入契约错误必须结构化,便于调用方路由处理。 |
| 后端缺少 `xml_presentation_id` 时失败 | `shortcuts/slides/slides_create_test.go:255-283` | 创建成功必须拿到可用资源 id。 |
| URL fallback 在本地构造,不调用 Drive metas 或 batch query | `shortcuts/slides/slides_create_test.go:649-688` | 能用本地回执构造的内容,不应增加额外 API 依赖。 |
| 图片占位符按唯一路径上传一次,并在页面创建前完成替换 | `shortcuts/slides/slides_create_test.go:751-854` | 素材处理是发布边界的管道能力,不是设计工作。 |
| 本地占位符文件缺失时,在任何 API 调用前失败 | `shortcuts/slides/slides_create_test.go:856-877` | 本地产物存在性是发布前置条件。 |
| Dry-run 暴露 API 计划形状和占位 id | `shortcuts/slides/slides_create_test.go:572-602``shortcuts/slides/slides_create_test.go:879-900` | Dry-run 应描述编排计划,而不是执行重型校验副作用。 |
| Readback 在 E2E 中作为单独 follow-up 调用证明 | `tests/cli_e2e/slides/slides_create_workflow_test.go:32-85``tests/cli_e2e/slides/coverage.md:9-16` | Readback 是测试和交付证据,不是默认 `Execute` 行为。 |
| Bot 授权是非致命三态granted、skipped、failed | `shortcuts/slides/slides_create_test.go:66-198` | 便利性的后置动作不应把创建成功升级成失败。 |
## `slides +create` 不负责的事情
| 非职责 | 证据 | 对 `+create-svglide` 的设计含义 |
| --- | --- | --- |
| 不从 prompt 生成 slide XML | `shortcuts/slides/slides_create.go:40-43``shortcuts/slides/slides_create.go:158-205` | `+create-svglide` 不能变成 `--topic -> deck`。 |
| 不深度校验 slide XML 语义 | `shortcuts/slides/slides_create.go:44-69` | shortcut 内只应放发布阻塞级的最小校验。 |
| 不预览或修复布局 | `shortcuts/slides/slides_create.go:125-221` | preview 和 repair 属于发布前的 skill/scripts 或 runner。 |
| 不在 `Execute` 内做 readback | `shortcuts/slides/slides_create.go:125-221``tests/cli_e2e/slides/slides_create_workflow_test.go:68-85` | Readback 是测试/证明步骤,不是默认 shortcut 行为。 |
| 不保证原子创建 | `shortcuts/slides/slides_create.go:194-196``shortcuts/slides/slides_create_test.go:354-420` | 新发布类 shortcut 应提供恢复上下文,而不是隐藏部分成功。 |
| 不处理超过 10 个内联页面 | `shortcuts/slides/slides_create.go:18-21``shortcuts/slides/slides_create_test.go:441-465` | 第一版应设边界,而不是一开始实现复杂批处理器。 |
| 不负责视觉质量 | `skills/lark-slides/SKILL.md:91-127``skills/lark-slides/SKILL.md:153-160` | 视觉质量门禁应发生在 shortcut 消费产物之前。 |
## 反例
| 诱人的需求 | 为什么看起来合理 | `slides +create` 给出的约束 |
| --- | --- | --- |
| 默认加入 readback | E2E 用 readback 证明持久化。 | E2E 是创建后另调 get API`Execute` 输出创建结果后即结束。Readback 应保持可选或放在 MVP 外。 |
| 调后端前语义校验每一页 | 本地错误更友好。 | `+create` 只校验 JSON 形状、页数、本地占位符文件XML 解析由后端负责。SVGlide 也只校验发布路由必需字段。 |
| 运行 preview lint 并自动 repair | SVGlide 有 preview 工具链。 | `+create` 不做布局判断。Preview lint 和 repair 应留在发布前工具链。 |
| 接受 prompt 并生成 deck | 高层 UX 很吸引人。 | `+create` 消费最终提交物。Prompt-to-deck runner 应是另一层命令或脚本。 |
| 通过自动重试/重建隐藏部分失败 | 看起来更友好。 | `+create` 暴露部分进度。恢复应该显式、可续跑。 |
## `slides +create-svglide` 允许新增的职责
`+create-svglide` 只能在 SVGlide 输入契约强制要求的地方比 `+create` 稍重。新增工作仍必须属于发布边界,而不是生成边界。
| 额外职责 | 允许原因 | 限制 |
| --- | --- | --- |
| 读取 SVGlide manifest 或 run directory | 与 `--slides` 不同SVGlide 产物是文件型产物。 | 立即归一化为一个 manifest 模型;不要推断设计意图。 |
| 校验 manifest schema 和页序 | 需要知道要发布什么。 | 只校验形状和必填字段。 |
| 校验页面文件存在性和路径安全 | 等价于 `+create` 校验 `@path` 占位符。 | 不检查美观度或文本质量。 |
| 校验发布必需的 SVGlide 字段 | 目标发布 API 或 parser 可能需要 namespace、contract/version、尺寸或 role 才能接收页面。 | 只检查必需标记;不要在 shortcut 中把普通 SVG 重写成协议 SVG。 |
| 上传声明的本地素材 | 等价于 `+create` 上传 `@path` 图片。 | 只做上传和 token 替换;不做素材搜索或生成。 |
| 把 SVGlide 页面提交给目标发布 API | 等价于 `+create` 提交每个 slide XML 字符串。 | 保持输出和部分进度语义明确;如果后端能直接消费 SVGlide不要假设 CLI 必须转 XML。 |
## `slides +create-svglide` 必须不拥有的职责
| 职责 | 所属边界 |
| --- | --- |
| research、outline、design brief、slide content planning | `skills/lark-slides` 指导和外部 runner/scripts |
| SVG authoring | agent 或 runner在发布前完成 |
| preview rendering、preview lint、repair loop | skill scripts 或 runner在发布前完成 |
| 视觉质量评分 | skill/scripts/quality gate不属于 shortcut `Execute` |
| readback 作为默认成功标准 | E2E 或可选验证 flag |
| PPE/Whistle 路由进入核心命名 | 只能属于环境/profile 层 |
## MVP 范围
推荐第一版实现:
```bash
lark-cli slides +create-svglide --manifest ./svglide-run/manifest.json --as user
```
MVP 行为:
1. 解析 manifest。
2. 校验必填字段、页序、文件存在性、路径安全、尺寸、最小 SVGlide contract 标记。
3. 创建 presentation 外壳。
4. 上传 manifest 声明的本地素材。
5. 把页面提交给后端。
6. 输出 `xml_presentation_id``url``page_ids``slide_ids`、上传素材数量,以及失败时的部分进度上下文。
MVP 排除项:
1. 不接受 prompt 输入。
2. 不包含生成阶段。
3. 不做 preview repair。
4. 不默认 readback。
5. 不在命令名、目录名或类型名中包含 PPE。
## `+create-svglide` 测试边界
第一版测试应镜像 `slides +create` 的测试形状,而不是证明完整 SVGlide 生成流水线。
| 测试范围 | 必须证明 |
| --- | --- |
| 输入契约 | 非法 manifest、缺失页面文件、不安全路径、不支持的页数以结构化 param 失败。 |
| Dry-run | 展示 create、asset upload、page publish 步骤,包含占位 presentation id 和确定性的 step label。 |
| 素材处理 | 重复本地素材只上传一次;页面 payload 在发布前引用已上传 token。 |
| 部分失败 | deck 已存在但第 N 页失败时,错误包含 presentation id、失败页序号、已成功发布页数。 |
| Bot grant | 继承 `slides +create` 的 user/bot 输出行为grant 失败不升级成 create 失败。 |
| E2E | 先断言 create/publish 结果;可选 readback 作为单独证明步骤,除非命令显式加入 `--readback` 契约。 |
## Team 结论
适合研究这个边界的 team 是:
| 角色 | 范围 |
| --- | --- |
| Code Reader | 从 Go 实现中抽取运行时职责。 |
| Test Reader | 抽取行为锁定点,并证明哪些行为不在 `Execute` 内。 |
| Skill Boundary Reader | 区分 agent/script 职责和 shortcut 职责。 |
| Architect/Skeptic | 拒绝过宽 scope只把已被 `+create` 证明的模式映射到 `+create-svglide`。 |
这个 team 的证明标准不是“读过文件”,而是:
```text
每一个 proposed +create-svglide 职责都必须映射到:
1. 一个已有 +create 职责;或
2. 一个由 SVGlide 产物输入形态强制产生的最小额外职责。
```

View File

@@ -0,0 +1,41 @@
# SVG Slides Local Generation
This reference family is for local SVG Slides generation and validation.
It is not the Lark Slides XML/SXSD workflow and it is not the publish shortcut. Use it to produce a local publish-ready bundle that a future `slides +create-svglide` publisher can consume.
## Read Routes
| Task | Read first | Then read |
|---|---|---|
| Generate a new SVG deck | `workflow.md` | `design-brief.md`, `protocol.md`, `authoring-rules.md`, `visual-design.md`, `validation.md` |
| Repair protocol failures | `validation.md` | `protocol.md`, `authoring-rules.md` |
| Improve visual quality | `visual-design.md` | `design-brief.md`, `workflow.md` |
| Use charts | `chart-workflow.md` | `protocol.md`, `validation.md` |
| Continue an existing deck | `editing-existing-decks.md` | `workflow.md`, `protocol.md` |
| Audit provenance | `source/split-manifest.json` | `source/full.debranded.md` |
## Boundary
Generation and validation produce a local publish-ready bundle.
A future SVG Slides publisher consumes this bundle. That publishing path is intentionally outside this reference family.
A local bundle may set `publish_ready=true`; it must not claim it is published.
## Canvas Decision
This CLI adaptation uses a 960x540 SVG canvas: `viewBox="0 0 960 540"`.
The source snapshot is preserved for provenance and coverage audit. Where the source describes a different default canvas, the CLI adaptation layer intentionally normalizes generated SVG Slides to 960x540.
## Required Local Gates
1. `node skills/lark-slides/scripts/validate_svg_deck.mjs <deck-dir> --json`
2. `node skills/lark-slides/scripts/svg_slides_bundle.mjs <deck-dir> --title "<title>"`
3. Browser text-boundary check when Playwright is available.
## Source Coverage
- Covers manifest sections: title
- Coverage mode: routing entry; source text is preserved in `source/full.debranded.md`, while this file points workers to the coverage-preserving split docs.

View File

@@ -0,0 +1,79 @@
# SVG Slides Authoring Rules
## Required Authoring Pattern
Write complete slide files. A slide edit is not a fragment, patch, or HTML page.
Use this order:
1. Optional `<defs>`.
2. One background as the first rendered child.
3. Top-level shapes, images, charts, groups, and optional notes.
Every rendered element that the slide engine must understand needs the appropriate `slide:role`. Do not depend on generic browser rendering when the protocol has an explicit semantic role.
## Forbidden Constructs
Do not use:
- `<style>` blocks;
- `class=`;
- `<div>` or `<section>` wrappers in text `foreignObject`;
- bare text under `foreignObject`;
- SVG `<text>`;
- SVG `<marker>`;
- hex colors;
- named colors;
- `none` for `fill` or `stroke`;
- role-less primitives in the rendered slide body.
## Text Boxes
Use plain text boxes for text-only content:
```xml
<foreignObject slide:role="shape" slide:shape-type="text" x="96" y="96" width="640" height="120" style="font-size:32px;color:rgba(15,23,42,1);line-height:1.2">
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Main argument</p>
</foreignObject>
```
Use shape-with-text only when the object is truly one styled box with one text block. If the card has multiple parts, use `<g slide:role="group">`.
## Image Elements
Use images when there is a real image asset or generated visual. The SVG file references the local asset path.
Informational images such as charts, diagrams, screenshots, and infographics must preserve their original ratio. Decorative images may be composed more freely, but should still fit the resolved design brief.
Unless the user explicitly requests no images, cover, section divider, and closing pages should use a large hero image or generated visual. Full-bleed image backgrounds use `<image slide:role="background">`; large non-background images use `<image slide:role="image" slide:shape-type="image">`.
When text sits on an image, place a semi-transparent `<rect slide:role="shape" slide:shape-type="shape">` scrim or a solid text zone after the image and before the text. Do not use SVG `<mask>` for this readability layer.
Generated cover, section divider, or closing images must not contain baked-in text. Render text as slide text on top of the image.
## Chart Embeds
A chart is an external SVG sidecar referenced by:
```xml
<rect slide:role="chart" href="resources/charts/example.svg" x="120" y="180" width="800" height="500"/>
```
Do not hand-draw a chart from primitives when the slide's point depends on a real quantitative data series. Use the chart workflow to generate the sidecar first.
## Custom Paths
Custom paths require accurate bounds. `slide:width` and `slide:height` describe the real extent of the path data, not the full canvas.
If a path has not been normalized, measure its bounding box before writing the final slide. Oversized path boxes make selection, hit testing, and layout misleading.
## Grouped Cards
Use `<g slide:role="group">` for a multi-element cluster: card background, badge, icon, title, body, chart, image, or connector. Each child still carries its own role.
Do not use `<g slide:role="shape">` as a generic container. It is only for the shape-with-text form.
## Source Coverage
- Covers manifest sections: slides_edit_tool, image_usage, compute_custom_shape_bbox_tool
- Coverage mode: preserve authoring constraints and tool semantics that affect generated SVG structure.

View File

@@ -0,0 +1,64 @@
# SVG Slides Chart Workflow
## When To Use A Chart
Use a chart when the slide's point depends on a real quantitative series:
- trend;
- multi-category comparison;
- part-to-whole split;
- distribution;
- ranking;
- two-dimensional positioning.
For single numbers or trivial two-bucket comparisons, prefer a large text callout unless the comparison needs a chart to be understood.
## When Not To Use A Chart
Do not generate a chart for vague, unsourced, decorative, or invented data. Do not choose a chart type because the raw data happens to look compatible; choose it because the takeaway requires that representation.
When in doubt, a sorted bar chart is safer than a pie or doughnut.
## Chart Sidecar Contract
A chart is generated as an SVG sidecar before slide authoring and embedded by reference.
The generation request must decide the takeaway first. The takeaway must be faithful to the data and short enough to guide chart design.
The request must include:
- chart type;
- JSON data matching that type;
- style matching the destination slide;
- actual on-slide width and height;
- output path under `resources/charts/`.
The declared chart width should match the embed width. Chart internals derive text size from width. Do not declare a wide chart and embed it in a narrow slot.
## Embed Contract
Embed a generated chart with:
```xml
<rect slide:role="chart" href="resources/charts/name.svg" x="120" y="180" width="800" height="500"/>
```
The embed width and height must match the chart sidecar's intended display size. Keep a 16:10-ish chart area when possible to avoid letterboxing.
One chart should carry one distinct insight. Pair charts with short callouts or labels, and vary chart composition across the deck.
## Validation Notes
Static deck validation confirms the chart placeholder shape, not the correctness of the chart sidecar data. Review chart sidecars for:
- source-backed data;
- truthful takeaway;
- readable labels;
- width at or above the practical floor;
- matching palette;
- intact `href`.
## Source Coverage
- Covers manifest sections: generate_svg_chart_tool
- Coverage mode: preserve chart generation, data contract, rendering constraints, and validation expectations.

View File

@@ -0,0 +1,80 @@
# SVG Slides Design Brief
## Inputs
Resolve the design brief after these inputs are known:
- topic and goal;
- audience;
- delivery mode;
- language;
- page count when known;
- source material;
- user-fixed brand, color, or content constraints;
- one to three short visual-direction phrases.
Do not ask the user to choose tone, density, palette, or typography unless they volunteered hard constraints.
## Output Contract
The brief must produce:
- `narrative_spine`;
- `depth`;
- `tone`;
- `visual_system`.
These outputs govern outline, content density, wording, asset choices, typography, color, layout, and decoration.
## narrative_spine
`narrative_spine` defines the slide sequence discipline. It is the default source of order, sectioning, and narrative movement.
The user can override it by giving or editing an outline. After that point, the user outline wins.
## depth
`depth` decides altitude and density:
- how much context each slide carries;
- what to include and exclude;
- how many main points per slide;
- how source evidence should appear;
- whether a page should split instead of cram.
## tone
`tone` controls writing style and evidence posture. It should reflect the audience and delivery mode.
Presented decks can use shorter on-slide wording because the speaker carries context. Self-read decks need more complete explanatory text but still must avoid walls of text.
## visual_system
`visual_system` is the authority for look and feel. It should include:
- color logic;
- typography category and treatment;
- layout grammar;
- imagery or material direction;
- page-role imagery defaults for cover, section divider, and closing pages;
- decoration and motif rules;
- constraints to avoid.
Unless the user explicitly requests no images, `visual_system` must specify how cover, section divider, and closing pages use a high-impact hero image or generated visual. The brief should describe the imagery subject, treatment, crop attitude, and how foreground text stays readable.
Font mapping must preserve the same category and treatment. Do not swap serif and sans, ignore uppercase treatment, or pick generic fonts when the brief calls for a distinctive style.
## How It Drives Generation
Use the brief in this order:
1. Shape the outline from `narrative_spine`.
2. Size the content from `depth`.
3. Write titles and evidence from `tone`.
4. Build the deck-level style from `visual_system`.
5. Author each slide's layout from the content logic plus the visual system.
## Source Coverage
- Covers manifest sections: resolve_design_brief
- Coverage mode: preserve design brief inputs, output contract, and downstream influence on outline and page authoring.

View File

@@ -0,0 +1,39 @@
# SVG Slides Editing Existing Decks
## Continue Existing Deck
When the user asks to continue, edit, extend, or repair an existing uploaded deck, operate on the existing converted project instead of recreating from scratch.
Preserve every existing page unless the user asks to change it. A page with minimal content should remain minimal if that is what the source deck contained.
## Preserve Existing Pages
For text or layout changes, edit only the target slide files. Preserve styling by default. Restyle only when the user explicitly asks.
Preserve media, chart, video, and audio blocks verbatim when the request does not touch them.
## Add Or Delete Pages
Add pages through the organize workflow, then author the new standalone SVG pages.
Delete only pages the user asked to remove. Do not rerun the new-deck outline workflow over an existing deck; it can overwrite existing slide files and lose original pages.
## Template Reference Boundary
An uploaded reference can mean two different things:
- Continue or edit this deck: preserve and modify that deck.
- Create a new deck inspired by this reference: author fresh SVG using the normal create workflow.
Clarify when the user's wording does not identify which behavior they want.
## PPTX Conversion Boundary
Converted decks may contain imported chart placeholders or media. Preserve legacy chart references unless the user asks to update chart data, type, theme, or emphasis.
If a chart is resized materially, regenerate the chart sidecar with the new dimensions rather than only squeezing the existing placeholder.
## Source Coverage
- Covers manifest sections: slide_organize_tool, slides_convert_tool, slides_parse_template_tool
- Coverage mode: preserve existing-deck continuation, conversion, and template parsing boundaries without turning them into publish behavior.

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="chart_embed" viewBox="0 0 960 540">
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="56" width="680" height="72" style="font-size:36px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(17,24,39,1);font-weight:800;line-height:1.15;text-align:left;vertical-align:top;letter-spacing:0px;padding:0px">
<h2 xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:36px;line-height:1.15;color:rgba(17,24,39,1);letter-spacing:0px">Chart is a referenced sidecar</h2>
</foreignObject>
<rect slide:role="chart" href="resources/charts/example_bar.svg" x="80" y="160" width="560" height="350"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="690" y="190" width="190" height="118" style="font-size:19px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(55,65,81,1);font-weight:500;line-height:1.38;text-align:left;vertical-align:top;letter-spacing:0px;padding:0px">
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:19px;line-height:1.38;color:rgba(55,65,81,1);letter-spacing:0px">The chart payload lives outside the slide and is referenced by href.</p>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="group_card" viewBox="0 0 960 540">
<defs>
<linearGradient id="card_grad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="rgba(255,255,255,1)"/>
<stop offset="100%" stop-color="rgba(226,232,240,1)"/>
</linearGradient>
</defs>
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(241,245,249,1)"/>
<g slide:role="group" id="card_primary">
<rect slide:role="shape" slide:shape-type="round-rect" x="120" y="140" width="520" height="300" rx="24" ry="24" fill="url(#card_grad)" stroke="rgba(148,163,184,1)" stroke-width="1"/>
<circle slide:role="shape" slide:shape-type="circle" cx="180" cy="206" r="26" fill="rgba(37,99,235,1)"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="224" y="178" width="340" height="50" style="font-size:28px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(15,23,42,1);font-weight:800;line-height:1.2;text-align:left;vertical-align:top;letter-spacing:0px;padding:0px">
<h2 xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:28px;line-height:1.2;color:rgba(15,23,42,1);letter-spacing:0px">Grouped card</h2>
</foreignObject>
<foreignObject slide:role="shape" slide:shape-type="text" x="154" y="264" width="420" height="86" style="font-size:20px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(51,65,85,1);font-weight:500;line-height:1.38;text-align:left;vertical-align:top;letter-spacing:0px;padding:0px">
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:20px;line-height:1.38;color:rgba(51,65,85,1);letter-spacing:0px">A card is a group; every visual child still carries its own slide role.</p>
</foreignObject>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="minimal_slide" viewBox="0 0 960 540">
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(248,250,252,1)"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="760" height="92" style="font-size:42px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(15,23,42,1);font-weight:800;line-height:1.12;text-align:left;vertical-align:top;letter-spacing:0px;padding:0px">
<h1 xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:42px;line-height:1.12;color:rgba(15,23,42,1);letter-spacing:0px">One protocol-compliant SVG slide</h1>
</foreignObject>
</svg>

After

Width:  |  Height:  |  Size: 753 B

View File

@@ -0,0 +1,94 @@
# SVG Slides Protocol
## Canvas
- Each page is one standalone SVG file.
- Root must contain `xmlns="http://www.w3.org/2000/svg"`.
- Root must contain `xmlns:slide="https://slides.bytedance.com/ns"`.
- Root must contain `slide:role="slide"`.
- Root must contain an `id`.
- Root must contain `viewBox="0 0 960 540"`.
- Child coordinates are in viewBox units.
- Do not rely on HTML document behavior. SVG nodes use SVG semantics; XHTML appears only inside approved `foreignObject` children.
- This 960x540 canvas is the CLI adaptation target. The preserved source snapshot may mention other defaults, but generated local bundles must use 960x540.
## Background
- Exactly one rendered background is required.
- Optional `<defs>` may appear first.
- The first rendered child after optional `<defs>` must be a `<rect>` or `<image>` with `slide:role="background"`.
- Background must cover the full canvas.
- Gradient backgrounds must reference gradients declared in the same slide's `<defs>`.
- A full-bleed image background should be an `<image slide:role="background">`.
- Text scrims over image backgrounds are normal shape overlays after the background, not additional backgrounds.
## Text
- Plain text uses `foreignObject slide:role="shape" slide:shape-type="text"`.
- Text `foreignObject` needs numeric `x`, `y`, `width`, and `height`.
- The first direct XHTML child must be `p`, `ul`, `ol`, `h1`, `h2`, `h3`, or `small`.
- Do not wrap text in `div` or `section`.
- Do not put bare text directly under `foreignObject`.
- Text style belongs in `style`.
- `font-size` must include `px`.
- Text color must be `rgb(...)` or `rgba(...)`.
- Text boxes must be sized to fit; static validation does not prove rendered wrapping.
## Shapes And Groups
- Geometry needs `slide:role="shape"` and a meaningful `slide:shape-type`.
- Common geometry includes `rect`, `ellipse`, `circle`, `path`, and `line`.
- Multi-element cards use `<g slide:role="group">`.
- Children inside a group still keep their own `slide:role`.
- A shape-with-text group is only for one geometry plus one text block. Cards with badges, icons, charts, or multiple text blocks must be regular groups.
- Custom paths must declare a meaningful `slide:width` and `slide:height` that match the path's real bounding box.
## Lines
- Lines use `<line slide:role="shape" slide:shape-type="line">`.
- Arrows use `slide:start-arrow` or `slide:end-arrow`.
- SVG marker arrows are forbidden.
## Images
- Images use `<image slide:role="image" slide:shape-type="image" href="...">`.
- Informational images preserve source aspect ratio.
- Do not wrap a single image in a group unless it is truly part of a larger multi-element composition.
- Borders and shadows belong on the image element itself when used.
## Charts
- Charts use `<rect slide:role="chart" href="..." x="..." y="..." width="..." height="...">`.
- The rect is a chart placeholder; it is not a drawn rectangle.
- Place charts at top level or inside `<g slide:role="group">`.
- Preserve chart `href` verbatim unless the user asks to change chart data, type, emphasis, theme, or source.
## Notes
- Speaker notes are optional and do not render on canvas.
- At most one `<slide:note>` may appear.
- Notes contain direct paragraph children.
## Colors
- Use `rgb(...)`, `rgba(...)`, or `url(#id)`.
- Do not use hex colors.
- Do not use named colors.
- Do not use `none` for `fill` or `stroke`; use `rgba(0,0,0,0)` for transparent fills.
## Animation
- Animation is part of delivery, not decoration.
- Most slides should be static.
- Presented decks may use progressive reveal for complex steps, charts, processes, timelines, or comparisons.
- Self-read, formal, board, or consulting decks should read fully without clicks.
- Use at most three builds on a slide.
- Use one effect type per slide.
- Animated elements need explicit `id`.
- Animate top-level elements or top-level groups.
- Use one deck-level page transition when needed; do not vary transition style slide by slide.
## Source Coverage
- Covers manifest sections: svg_reference, svg_document_rules
- Coverage mode: preserve hard SVG protocol requirements from the source while applying the CLI canvas adaptation to 960x540; visual guidance belongs in `visual-design.md`, not here.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{
"version": "svg-slides.split-manifest.v1",
"source": "skills/lark-slides/references/svg-slides/source/full.debranded.md",
"source_export": "/Users/bytedance/Documents/Codex/2026-07-01/https-bytedance-larkoffice-com-docx-kncld7xr5ohwonxhksncz3lxnvd/outputs/lark_doc_KnCLd7xr5ohWONxhKsncZ3Lxnvd/full.debranded.md",
"source_role": "provenance_and_coverage_authority_not_default_runtime_context",
"sections": [
{"id": "title", "lines": [1, 1], "target": "README.md"},
{"id": "system_prompt_workflow", "lines": [3, 196], "target": "workflow.md"},
{"id": "svg_reference", "lines": [198, 865], "target": "protocol.md"},
{"id": "resolve_design_brief", "lines": [867, 1080], "target": "design-brief.md"},
{"id": "deck_design_reference_catalog", "lines": [1082, 1234], "target": "visual-design.md"},
{"id": "slide_outline_tool", "lines": [1236, 1254], "target": "workflow.md"},
{"id": "activate_slides_edit_tool", "lines": [1256, 1262], "target": "workflow.md"},
{"id": "slides_edit_tool", "lines": [1264, 1281], "target": "authoring-rules.md"},
{"id": "svg_document_rules", "lines": [1283, 1287], "target": "protocol.md"},
{"id": "image_usage", "lines": [1289, 1291], "target": "authoring-rules.md"},
{"id": "incremental_processing", "lines": [1293, 1331], "target": "workflow.md"},
{"id": "finish_slides_edit_tool", "lines": [1333, 1339], "target": "validation.md"},
{"id": "slide_organize_tool", "lines": [1341, 1347], "target": "editing-existing-decks.md"},
{"id": "compute_custom_shape_bbox_tool", "lines": [1349, 1355], "target": "authoring-rules.md"},
{"id": "generate_svg_chart_tool", "lines": [1357, 2356], "target": "chart-workflow.md"},
{"id": "slides_convert_tool", "lines": [2358, 2395], "target": "editing-existing-decks.md"},
{"id": "slides_parse_template_tool", "lines": [2397, 2420], "target": "editing-existing-decks.md"}
]
}

View File

@@ -0,0 +1,120 @@
# SVG Slides Validation
## Static Protocol Validator
Run:
```bash
node skills/lark-slides/scripts/validate_svg_deck.mjs <deck-dir-or-slides-dir> --json
```
The validator checks hard protocol rules:
- standalone SVG root;
- `slide:role="slide"`;
- required namespaces;
- `viewBox="0 0 960 540"`;
- background order;
- forbidden style blocks and CSS classes;
- forbidden text wrappers;
- color syntax;
- text `font-size` units;
- line role and arrow semantics;
- XML parseability.
The validator is a hard gate for publish-ready local bundles.
## Browser Text Boundary Check
Static XML cannot prove final rendered wrapping, CJK font fallback, or actual text height. Run browser text-boundary QA when Playwright is available:
```bash
node skills/lark-slides/scripts/svg_slides_browser_text_bounds.mjs <deck-dir-or-slides-dir> --out /tmp/svg-slides-text-bounds.json
```
If Playwright is unavailable, the script exits 2 and explains the missing optional dependency.
## Bundle Manifest
Run:
```bash
node skills/lark-slides/scripts/svg_slides_bundle.mjs <deck-dir> --title "<title>"
```
The bundle manifest records:
- protocol version;
- title;
- slide list;
- validation receipt paths;
- `publish_ready=true`;
- `published=false`.
## Receipt Requirements
A local publish-ready bundle needs:
- `manifest.json`;
- `receipts/validate_svg_deck.json`;
- optional browser text-boundary receipt when browser QA ran;
- slide files listed in deterministic order.
## What Passing Validation Does Not Prove
Passing validation does not prove visual excellence, source quality, chart truth, or backend acceptance. It proves that the generated local SVG files obey the hard protocol rules represented by the validator.
Always separate:
- protocol pass;
- browser text-boundary pass;
- visual design review;
- live publish proof.
## Local Publish-Ready Bundle
Run:
```bash
node skills/lark-slides/scripts/svg_slides_bundle.mjs <deck-dir> --title "<deck title>"
```
The command writes:
- `manifest.json`
- `receipts/validate_svg_deck.json`
The manifest uses:
```json
{
"version": "svglide.manifest.v1",
"protocol": "svg-slides.v1",
"size": {"width": 960, "height": 540},
"publish_ready": true,
"published": false
}
```
`publish_ready=true` means local static validation passed. It does not mean the deck was published to Lark Slides.
## Browser Text Boundary QA
When Playwright is available in the development environment, run:
```bash
node skills/lark-slides/scripts/svg_slides_browser_text_bounds.mjs <deck-dir> --out receipts/preview_text_bounds.json
```
Exit codes:
- `0`: no text-boundary problems.
- `1`: rendered text overflow was detected.
- `2`: the script could not run, for example Playwright is unavailable.
This browser check is a generation-quality gate. It is not a publish API proof.
## Source Coverage
- Covers manifest sections: finish_slides_edit_tool
- Coverage mode: preserve finish/validation gates and explicitly separate protocol pass from visual quality pass.

View File

@@ -0,0 +1,74 @@
# SVG Slides Visual Design
## Typography
Use real font families that are likely to render. Keep a stable display/body pairing across the deck.
Titles, hero numbers, and key labels may use display fonts. Body text should use readable fonts. English and CJK decks need compatible font choices rather than generic fallback everywhere.
Do not switch typography per slide without a structural reason.
## Layout Freedom
SVG Slides gives full coordinate-level layout control. Use that control to encode the page's logic.
Start each slide by identifying the relationship in the content:
- comparison;
- sequence;
- timeline;
- cycle;
- hierarchy;
- matrix or quadrant;
- funnel;
- part-to-whole;
- cause to effect;
- evidence to implication.
Then compose a bespoke structure using position, scale, alignment, grouping, flow direction, connectors, depth, and contrast. A layout invented for the slide's actual logic is better than a canned diagram.
## Visual Differentiation
Every substantive slide should have a visual idea: image, chart, diagram, process, comparison, spatial map, large number, table-like structure, or custom shape system.
Avoid repeating title-plus-bullets. Reuse deck-level motif and style, not the exact same page layout.
Cover, section divider, and closing pages are not exceptions. Unless the user explicitly requests no images, make these pages image-led with a high-impact hero image or generated visual. Text over imagery must use an intentional readability treatment, such as a translucent scrim or solid text zone, instead of relying on contrast by accident.
## Density
Density comes from audience and delivery mode. Split rather than cram when a slide needs more than one central idea.
Each slide should defend one central idea. Content slide titles should be declarative arguments, not topic labels. Cover, section, and closing slides can use shorter labels.
## Anti-Patterns
Avoid:
- generic white slides with bullets only;
- the same card grid on every page;
- low-contrast text;
- decorative lines crossing text;
- filler agenda or Q&A pages in short decks;
- placeholder images;
- unverified data visualization;
- text walls;
- overusing animation.
## Remaining Human Judgment
Static validation proves protocol shape, not taste. A deck can pass validation and still be visually weak.
Review visual quality separately:
- Does the layout express the slide's logic?
- Does each slide have a clear central claim?
- Are data and claims source-backed?
- Is typography intentional and consistent?
- Is the page readable in a browser at expected size?
- Does the deck vary composition while staying in one visual system?
## Source Coverage
- Covers manifest sections: deck_design_reference_catalog
- Coverage mode: preserve visual quality rules and examples as generation guidance; do not collapse them into generic style advice.

View File

@@ -0,0 +1,98 @@
# SVG Slides Workflow
## Layer Boundary
This workflow owns local SVG deck generation and local validation. It does not call live APIs, choose a PPE lane, or prove backend acceptance of the generated payload.
The output is a local publish-ready bundle: standalone SVG slide files, source notes, optional assets, validation receipts, and a manifest. It is not published.
## Phase 1: Understand Request
Decide whether the user wants a new deck, a continuation of an existing deck, a repair pass, or a visual-quality pass. Clarify only when the target file, target slides, audience, delivery mode, or requested outcome is genuinely ambiguous.
Audience means the final viewer, not the creator. A specific audience can drive density and evidence style directly. Generic labels such as "users", "clients", or "team" are not specific enough for broad generation unless the user asks not to be interrupted.
## Phase 2: Settle Goal Audience Delivery
Settle three values before designing slides:
- `goal`: what the presentation should make the viewer understand or decide.
- `audience`: who will read or watch it.
- `delivery_mode`: `presented` when a speaker talks over it, `self_read` when it must stand alone.
Do not ask the user to pick tone, palette, density, or style. Those are inferred later by the design brief.
## Phase 3: Build Source Material
Broad topic-only requests require real source material. Search snippets, memory, and internal knowledge are not enough.
Collect full source text before drafting claims. Save a local research file with data points, claims, caveats, and source references. Every important claim or number used later must be traceable from `slide_content.md` back to this source material.
## Phase 4: Resolve Design Brief
Create a design brief after goal, audience, delivery mode, language, page count, and source material are known.
The brief must include:
- `narrative_spine`: the sequence logic and discipline of the deck.
- `depth`: altitude, density, include/exclude rules, and main points per slide.
- `tone`: writing and evidence style.
- `visual_system`: color, typography, layout, imagery, material, and decoration direction.
The design brief is authoritative for the generated deck. Do not override it with generic taste while authoring pages.
## Phase 5: Confirm Outline
For broad topics, create an actual slide sequence, not a chapter list. Use the user's explicit page count when given. Otherwise use 8-12 substantive slides for normal decks, unless the user explicitly asked for a short deck.
When the user gave a detailed outline, use it. When the user reorders, removes, adds, or rewrites slides, the user's outline wins over the brief's `narrative_spine`.
## Phase 6: Write slide_content
Write `slide_content.md` before SVG authoring.
`slide_content.md` records the structure, slide roles, key material, data points, claims, quotes, and source references. It does not lock exact final sentences, image paths, chart layout, or final page composition.
## Phase 7: Lock Visual Direction And Plan Visuals
Translate `visual_system` into concrete deck-level style:
- `aesthetic_direction`: the design language and mood from the brief.
- `color_palette`: consistent deck palette, expressed later as `rgb(...)` / `rgba(...)`.
- `typography`: a stable display/body pairing that matches the brief's category and treatment.
- `visual_assets`: per-slide image and chart needs, including aspect ratio and placement intent.
Unless the user explicitly requests no images, cover, section divider, and closing pages default to a high-impact hero image or generated visual. Record the intended asset, crop/aspect ratio, placement, and text-readability overlay treatment in `visual_assets`; do not leave these page roles as text-only by default.
Plan charts before writing slides. Any real quantitative series that supports a slide's point should use the chart workflow rather than a hand-drawn fake chart.
## Phase 8: Author SVG Pages
Each page is a complete standalone SVG document. Compose freely for the page's content logic. Do not stamp out a fixed template pattern.
For each page, record authoring intent before writing:
- the central idea;
- the layout relationship being encoded;
- visual assets used;
- animation decision, or `static`;
- expected validation risks.
Do not regenerate the whole deck structure after slide files have been authored. Add or remove pages through the existing-deck workflow.
## Output Bundle
The local bundle should contain:
- `slides/*.svg`: one standalone SVG slide per page;
- `slide_content.md`: source-backed content plan;
- `research_notes.md` when source material was gathered;
- `resources/` for chart/image sidecars;
- `manifest.json` from `svg_slides_bundle.mjs`;
- `receipts/validate_svg_deck.json` from the static validator;
- optional browser text-boundary receipt.
## Source Coverage
- Covers manifest sections: system_prompt_workflow, slide_outline_tool, activate_slides_edit_tool, incremental_processing
- Coverage mode: preserve workflow semantics from the source while replacing product-specific tool names with local generation stages.

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
function fail(message, code = 2) {
console.error(message);
process.exit(code);
}
const args = process.argv.slice(2);
const targetArg = args.find((arg) => !arg.startsWith("--"));
const outIndex = args.indexOf("--out");
const outPath = outIndex >= 0 ? args[outIndex + 1] : "";
if (!targetArg) {
fail("Usage: node skills/lark-slides/scripts/svg_slides_browser_text_bounds.mjs <deck-dir-or-slides-dir> [--out <json-path>]");
}
let chromium;
try {
({ chromium } = await import("playwright"));
} catch {
fail("playwright is not installed; install it in a dev environment before browser text-boundary QA", 2);
}
const target = path.resolve(targetArg);
const slidesDir = fs.existsSync(path.join(target, "slides")) ? path.join(target, "slides") : target;
if (!fs.existsSync(slidesDir)) {
fail(`Slides directory not found: ${slidesDir}`);
}
const slideFiles = fs.readdirSync(slidesDir).filter((file) => file.endsWith(".svg")).sort();
if (!slideFiles.length) {
fail(`No .svg files found in ${slidesDir}`);
}
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 960, height: 540 }, deviceScaleFactor: 1 });
const results = [];
for (const file of slideFiles) {
const abs = path.join(slidesDir, file);
const svg = fs.readFileSync(abs, "utf8");
await page.setContent(`<!doctype html><html><body style="margin:0">${svg}</body></html>`, { waitUntil: "load" });
const problems = await page.evaluate(() => {
return [...document.querySelectorAll("foreignObject")].flatMap((node, index) => {
if (node.getAttribute("slide:role") !== "shape" || node.getAttribute("slide:shape-type") !== "text") {
return [];
}
const box = node.getBoundingClientRect();
const children = [...node.children];
if (!children.length) {
return [{ index: index + 1, reason: "empty_text_object" }];
}
return children.map((child) => {
const childBox = child.getBoundingClientRect();
const overflowX = childBox.left < box.left - 0.5 || childBox.right > box.right + 0.5;
const overflowY = childBox.top < box.top - 0.5 || childBox.bottom > box.bottom + 0.5;
if (!overflowX && !overflowY) return null;
return {
index: index + 1,
reason: "text_bounds_overflow",
box: { x: box.x, y: box.y, width: box.width, height: box.height },
childBox: { x: childBox.x, y: childBox.y, width: childBox.width, height: childBox.height }
};
}).filter(Boolean);
});
});
results.push({ file: path.relative(process.cwd(), abs), problemCount: problems.length, problems });
}
await browser.close();
const problemCount = results.reduce((sum, item) => sum + item.problemCount, 0);
const report = { status: problemCount === 0 ? "passed" : "failed", problemCount, results };
const json = `${JSON.stringify(report, null, 2)}\n`;
if (outPath) {
fs.mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true });
fs.writeFileSync(outPath, json);
}
process.stdout.write(json);
process.exit(problemCount === 0 ? 0 : 1);

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env node
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
function fail(message, code = 2) {
console.error(message);
process.exit(code);
}
const args = process.argv.slice(2);
const deckArg = args.find((arg) => !arg.startsWith("--"));
const titleIndex = args.indexOf("--title");
const title = titleIndex >= 0 ? args[titleIndex + 1] : "";
if (!deckArg || !title) {
fail("Usage: node skills/lark-slides/scripts/svg_slides_bundle.mjs <deck-dir> --title <title>");
}
const root = path.resolve(deckArg);
const slidesDir = fs.existsSync(path.join(root, "slides")) ? path.join(root, "slides") : root;
if (!fs.existsSync(slidesDir)) {
fail(`Slides directory not found: ${slidesDir}`);
}
const validator = path.resolve("skills/lark-slides/scripts/validate_svg_deck.mjs");
const validate = spawnSync("node", [validator, root, "--json"], { encoding: "utf8" });
if (!validate.stdout.trim()) {
process.stderr.write(validate.stderr);
process.exit(validate.status || 1);
}
const receipt = JSON.parse(validate.stdout);
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
fs.writeFileSync(path.join(root, "receipts", "validate_svg_deck.json"), `${JSON.stringify(receipt, null, 2)}\n`);
if (receipt.totalErrors !== 0) {
fail(`SVG deck is not publish-ready: ${receipt.totalErrors} validation error(s)`, 1);
}
const slideFiles = fs.readdirSync(slidesDir)
.filter((file) => file.endsWith(".svg"))
.sort();
const pages = slideFiles.map((file, index) => {
const abs = path.join(slidesDir, file);
const raw = fs.readFileSync(abs);
return {
id: path.basename(file, ".svg"),
index: index + 1,
file: path.relative(root, abs).split(path.sep).join("/"),
sha256: crypto.createHash("sha256").update(raw).digest("hex")
};
});
const manifest = {
version: "svglide.manifest.v1",
protocol: "svg-slides.v1",
title,
size: { width: 960, height: 540 },
publish_ready: true,
published: false,
pages,
receipts: {
validate_svg_deck: "receipts/validate_svg_deck.json"
}
};
fs.writeFileSync(path.join(root, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
console.log(JSON.stringify({ ok: true, manifest: path.join(root, "manifest.json"), pages: pages.length }, null, 2));

View File

@@ -0,0 +1,49 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
const script = path.resolve("skills/lark-slides/scripts/svg_slides_bundle.mjs");
function tempDeck() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-bundle-"));
fs.mkdirSync(path.join(root, "slides"));
return root;
}
const validSlide = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="bundle_slide" viewBox="0 0 960 540">
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Bundle</p>
</foreignObject>
</svg>`;
test("bundle builder writes manifest and validation receipt", () => {
const root = tempDeck();
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide);
const result = spawnSync("node", [script, root, "--title", "Bundle Test"], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr || result.stdout);
const manifest = JSON.parse(fs.readFileSync(path.join(root, "manifest.json"), "utf8"));
assert.equal(manifest.version, "svglide.manifest.v1");
assert.equal(manifest.protocol, "svg-slides.v1");
assert.equal(manifest.title, "Bundle Test");
assert.deepEqual(manifest.size, { width: 960, height: 540 });
assert.equal(manifest.publish_ready, true);
assert.equal(manifest.published, false);
assert.equal(manifest.pages.length, 1);
assert.match(manifest.pages[0].sha256, /^[a-f0-9]{64}$/);
const receipt = JSON.parse(fs.readFileSync(path.join(root, "receipts", "validate_svg_deck.json"), "utf8"));
assert.equal(receipt.totalErrors, 0);
});
test("bundle builder rejects invalid SVG deck", () => {
const root = tempDeck();
fs.writeFileSync(path.join(root, "slides", "slide_01.svg"), validSlide.replace("rgba(255,255,255,1)", "#fff"));
const result = spawnSync("node", [script, root, "--title", "Invalid"], { encoding: "utf8" });
assert.equal(result.status, 1);
assert.match(result.stderr, /not publish-ready/);
assert.equal(fs.existsSync(path.join(root, "receipts", "validate_svg_deck.json")), true);
assert.equal(fs.existsSync(path.join(root, "manifest.json")), false);
});

View File

@@ -0,0 +1,98 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const rootArg = process.argv.find((arg) => !arg.startsWith("--") && arg !== process.argv[1] && arg !== process.argv[0]);
const root = path.resolve(rootArg || "skills/lark-slides/references/svg-slides");
const json = process.argv.includes("--json");
const manifestPath = path.join(root, "source", "split-manifest.json");
function readText(file) {
return fs.readFileSync(file, "utf8");
}
function coverageBlock(markdown) {
const lines = markdown.split(/\r?\n/);
const start = lines.findIndex((line) => line.trim() === "## Source Coverage");
if (start === -1) return "";
const block = [];
for (let i = start + 1; i < lines.length; i += 1) {
if (/^#{1,2}\s+/.test(lines[i])) break;
block.push(lines[i]);
}
return block.join("\n");
}
function coverageIds(block) {
const match = block.match(/^- Covers manifest sections:\s*(.+)$/m);
if (!match) return [];
return match[1].split(",").map((value) => value.trim()).filter(Boolean);
}
const errors = [];
if (!fs.existsSync(manifestPath)) {
errors.push(`missing manifest: ${manifestPath}`);
}
const manifest = errors.length === 0 ? JSON.parse(readText(manifestPath)) : { sections: [] };
const sectionsById = new Map(manifest.sections.map((section) => [section.id, section]));
const seen = new Map();
if (fs.existsSync(root)) {
for (const entry of fs.readdirSync(root)) {
if (!entry.endsWith(".md")) continue;
const filePath = path.join(root, entry);
const block = coverageBlock(readText(filePath));
if (!block) {
errors.push(`${entry}: missing ## Source Coverage`);
continue;
}
const ids = coverageIds(block);
if (ids.length === 0) {
errors.push(`${entry}: missing "- Covers manifest sections:" line`);
continue;
}
for (const id of ids) {
const section = sectionsById.get(id);
if (!section) {
errors.push(`${entry}: unknown manifest section "${id}"`);
continue;
}
if (section.target !== entry) {
errors.push(`${entry}: section "${id}" belongs to ${section.target}`);
}
const files = seen.get(id) || [];
files.push(entry);
seen.set(id, files);
}
}
}
for (const section of manifest.sections) {
const files = seen.get(section.id) || [];
if (files.length === 0) {
errors.push(`${section.id}: not covered by ${section.target}`);
}
if (files.length > 1) {
errors.push(`${section.id}: covered multiple times by ${files.join(", ")}`);
}
}
const report = {
root,
manifest: manifestPath,
sectionCount: manifest.sections.length,
coveredCount: seen.size,
errors
};
if (json) {
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
} else if (errors.length === 0) {
console.log(`Source coverage OK: ${report.coveredCount}/${report.sectionCount} sections`);
} else {
console.error(`Source coverage failed: ${errors.length} errors`);
for (const error of errors) console.error(`- ${error}`);
}
process.exit(errors.length === 0 ? 0 : 1);

View File

@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
const script = path.resolve("skills/lark-slides/scripts/svg_slides_source_coverage_check.mjs");
function tempRoot() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-coverage-"));
fs.mkdirSync(path.join(root, "source"), { recursive: true });
return root;
}
function writeManifest(root, sections) {
fs.writeFileSync(path.join(root, "source", "split-manifest.json"), JSON.stringify({
version: "svg-slides.split-manifest.v1",
source: "source/full.debranded.md",
sections
}, null, 2));
}
function run(root) {
return spawnSync("node", [script, root, "--json"], { encoding: "utf8" });
}
test("passes when each manifest section is covered by its target file", () => {
const root = tempRoot();
writeManifest(root, [{ id: "workflow", lines: [1, 10], target: "workflow.md" }]);
fs.writeFileSync(path.join(root, "workflow.md"), "# Workflow\n\n## Source Coverage\n\n- Covers manifest sections: workflow\n");
const result = run(root);
assert.equal(result.status, 0, result.stderr);
assert.equal(JSON.parse(result.stdout).errors.length, 0);
});
test("fails when a section is missing from Source Coverage", () => {
const root = tempRoot();
writeManifest(root, [{ id: "protocol", lines: [1, 10], target: "protocol.md" }]);
fs.writeFileSync(path.join(root, "protocol.md"), "# Protocol\n\n## Source Coverage\n\n- Covers manifest sections: other\n");
const result = run(root);
assert.equal(result.status, 1);
const report = JSON.parse(result.stdout);
assert.ok(report.errors.some((error) => error.includes("unknown manifest section")));
assert.ok(report.errors.some((error) => error.includes("not covered")));
});
test("fails when a section is declared by the wrong target file", () => {
const root = tempRoot();
writeManifest(root, [{ id: "visual", lines: [1, 10], target: "visual-design.md" }]);
fs.writeFileSync(path.join(root, "workflow.md"), "# Workflow\n\n## Source Coverage\n\n- Covers manifest sections: visual\n");
const result = run(root);
assert.equal(result.status, 1);
assert.ok(JSON.parse(result.stdout).errors.some((error) => error.includes("belongs to visual-design.md")));
});

View File

@@ -0,0 +1,244 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
function usage() {
console.error("Usage: node skills/lark-slides/scripts/validate_svg_deck.mjs <deck-dir-or-slides-dir> [--json]");
}
const args = process.argv.slice(2);
const json = args.includes("--json");
const targetArg = args.find((arg) => !arg.startsWith("--"));
if (!targetArg) {
usage();
process.exit(2);
}
const target = path.resolve(targetArg);
const slidesDir = fs.existsSync(path.join(target, "slides")) ? path.join(target, "slides") : target;
if (!fs.existsSync(slidesDir)) {
console.error(`Slides directory not found: ${slidesDir}`);
process.exit(2);
}
const slideFiles = fs.readdirSync(slidesDir)
.filter((file) => file.endsWith(".svg"))
.sort()
.map((file) => path.join(slidesDir, file));
if (!slideFiles.length) {
console.error(`No .svg files found in: ${slidesDir}`);
process.exit(2);
}
function commandExists(name) {
const result = spawnSync("sh", ["-lc", `command -v ${name}`], { encoding: "utf8" });
return result.status === 0;
}
function checkXml(file, errors) {
if (!commandExists("xmllint")) {
errors.push({
rule: "xml.valid",
severity: "warn",
message: "xmllint is unavailable; XML parser check skipped",
});
return;
}
const result = spawnSync("xmllint", ["--noout", file], { encoding: "utf8" });
if (result.status !== 0) {
errors.push({
rule: "xml.valid",
severity: "error",
message: (result.stderr || result.stdout || "xmllint failed").trim(),
});
}
}
function firstElementAfterDefs(svg) {
const rootOpen = svg.match(/<svg\b[^>]*>/);
if (!rootOpen) return null;
let inner = svg.slice(rootOpen.index + rootOpen[0].length, svg.lastIndexOf("</svg>")).trim();
if (inner.startsWith("<defs")) {
const end = inner.indexOf("</defs>");
if (end === -1) return null;
inner = inner.slice(end + "</defs>".length).trim();
}
return inner.match(/^<([a-zA-Z][\w:-]*)\b([^>]*)>/)?.[0] || null;
}
function stripDefs(svg) {
return svg.replace(/<defs\b[\s\S]*?<\/defs>/g, "");
}
function attrValue(tag, name) {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return tag.match(new RegExp(`${escaped}="([^"]*)"`))?.[1] || "";
}
function isColorValueAllowed(value) {
return /^(rgba?\([^)]*\)|url\(#[-\w]+\))$/.test(value.trim());
}
function checkSlide(file) {
const rel = path.relative(process.cwd(), file);
const svg = fs.readFileSync(file, "utf8");
const errors = [];
checkXml(file, errors);
const root = svg.match(/<svg\b[^>]*>/)?.[0] || "";
if (!root) {
errors.push({ rule: "svg.root", severity: "error", message: "missing <svg> root" });
} else {
if (!/xmlns="http:\/\/www\.w3\.org\/2000\/svg"/.test(root)) {
errors.push({ rule: "svg.root.xmlns", severity: "error", message: "missing SVG namespace" });
}
if (!/xmlns:slide="https:\/\/slides\.bytedance\.com\/ns"/.test(root)) {
errors.push({ rule: "svg.root.slide-xmlns", severity: "error", message: "missing slide namespace" });
}
if (!/slide:role="slide"/.test(root)) {
errors.push({ rule: "svg.root.slide-role", severity: "error", message: "root must have slide:role=\"slide\"" });
}
if (!/id="[^"]+"/.test(root)) {
errors.push({ rule: "svg.root.id", severity: "error", message: "root must have id" });
}
if (!/viewBox="0 0 960 540"/.test(root)) {
errors.push({ rule: "svg.root.viewBox", severity: "error", message: "expected viewBox=\"0 0 960 540\"" });
}
}
if (/<presentation\b/.test(svg)) {
errors.push({ rule: "svg.no-presentation-wrapper", severity: "error", message: "single slide file must not wrap with <presentation>" });
}
const first = firstElementAfterDefs(svg);
const backgroundCount = (svg.match(/slide:role="background"/g) || []).length;
if (backgroundCount !== 1) {
errors.push({ rule: "background.count", severity: "error", message: `expected exactly one background, found ${backgroundCount}` });
}
if (!first || !/^(<rect\b|<image\b)/.test(first) || !/slide:role="background"/.test(first)) {
errors.push({ rule: "background.first-child", severity: "error", message: "first rendered child after optional <defs> must be the background" });
}
const bodyNoDefs = stripDefs(svg);
const forbidden = [
{ rule: "forbid.style-block", re: /<style\b/, message: "slide SVG must not rely on <style> blocks" },
{ rule: "forbid.css-class", re: /\bclass="/, message: "slide SVG must not rely on CSS classes" },
{ rule: "forbid.div-wrapper", re: /<div\b/, message: "text foreignObject must not contain <div>" },
{ rule: "forbid.section-wrapper", re: /<section\b/, message: "text foreignObject must not contain <section>" },
{ rule: "forbid.svg-text", re: /<text\b/, message: "use foreignObject rich text, not SVG <text>" },
{ rule: "forbid.svg-marker", re: /\bmarker-(start|end|mid)=|<marker\b/, message: "line arrowheads must use slide:* arrow attributes, not SVG marker" },
{ rule: "forbid.legacy-fontSize", re: /\bfontSize="/, message: "text visual properties must be in style, not legacy fontSize attribute" },
{ rule: "forbid.legacy-bold", re: /\bbold="/, message: "text visual properties must be in style, not legacy bold attribute" },
];
for (const item of forbidden) {
if (item.re.test(bodyNoDefs)) {
errors.push({ rule: item.rule, severity: "error", message: item.message });
}
}
for (const match of bodyNoDefs.matchAll(/\b(fill|stroke|stop-color)="([^"]+)"/g)) {
const [, attr, value] = match;
if (!isColorValueAllowed(value)) {
errors.push({
rule: "color.attr",
severity: "error",
message: `${attr} must be rgb(...), rgba(...), or url(#...); got ${JSON.stringify(value)}`,
});
}
}
for (const match of bodyNoDefs.matchAll(/(?:^|;)\s*color\s*:\s*([^;"]+)/g)) {
const value = match[1].trim();
if (!/^rgba?\([^)]*\)$/.test(value)) {
errors.push({
rule: "color.css",
severity: "error",
message: `CSS color must be rgb(...) or rgba(...); got ${JSON.stringify(value)}`,
});
}
}
const foreignObjects = [...svg.matchAll(/<foreignObject\b([^>]*)>([\s\S]*?)<\/foreignObject>/g)];
for (const [index, match] of foreignObjects.entries()) {
const attrText = match[1];
const inner = match[2].trim();
const label = `foreignObject #${index + 1}`;
const isTextObject = /slide:role="shape"/.test(attrText) && /slide:shape-type="text"/.test(attrText);
if (!isTextObject) continue;
for (const attr of ["x", "y", "width", "height"]) {
if (!new RegExp(`\\b${attr}="[-0-9.]+`).test(attrText)) {
errors.push({ rule: "text.geometry", severity: "error", message: `${label} missing numeric ${attr}` });
}
}
const styleText = attrValue(match[0], "style");
if (!/font-size:\s*\d+(?:\.\d+)?px/.test(styleText)) {
errors.push({ rule: "text.style.font-size", severity: "error", message: `${label} missing font-size with px suffix in style` });
}
if (!/color:\s*rgba?\(/.test(styleText)) {
errors.push({ rule: "text.style.color", severity: "error", message: `${label} missing rgb/rgba color in style` });
}
if (!/^<(p|ul|ol|h1|h2|h3|small)\b/.test(inner)) {
errors.push({
rule: "text.direct-child",
severity: "error",
message: `${label} first direct child must be p/ul/ol/h1/h2/h3/small, got ${inner.slice(0, 40) || "empty"}`,
});
}
if (/<(div|section)\b/.test(inner)) {
errors.push({ rule: "text.no-wrapper", severity: "error", message: `${label} contains an invalid wrapper element` });
}
if (/^([^<]|\s)+$/.test(inner)) {
errors.push({ rule: "text.no-bare-text", severity: "error", message: `${label} contains bare text instead of xhtml children` });
}
}
for (const match of bodyNoDefs.matchAll(/<line\b([^>]*)>/g)) {
const attrs = match[1];
if (!/slide:role="shape"/.test(attrs) || !/slide:shape-type="line"/.test(attrs)) {
errors.push({ rule: "line.role", severity: "error", message: "line must carry slide:role=\"shape\" and slide:shape-type=\"line\"" });
}
if (!/\bstroke="rgba?\(/.test(attrs)) {
errors.push({ rule: "line.stroke", severity: "error", message: "line must have rgb/rgba stroke" });
}
}
return { file: rel, errorCount: errors.filter((item) => item.severity === "error").length, errors };
}
const results = slideFiles.map(checkSlide);
const totalErrors = results.reduce((sum, result) => sum + result.errorCount, 0);
const report = {
target: path.relative(process.cwd(), target),
slidesDir: path.relative(process.cwd(), slidesDir),
slideCount: slideFiles.length,
totalErrors,
results,
};
if (json) {
console.log(JSON.stringify(report, null, 2));
} else {
console.log(`SVG deck validation: ${report.target}`);
console.log(`Slides: ${report.slideCount}`);
console.log(`Errors: ${report.totalErrors}`);
for (const result of results) {
const status = result.errorCount ? "FAIL" : "PASS";
console.log(`\n[${status}] ${result.file}`);
for (const error of result.errors) {
if (error.severity === "warn") {
console.log(` WARN ${error.rule}: ${error.message}`);
} else {
console.log(` ${error.rule}: ${error.message}`);
}
}
}
}
process.exit(totalErrors ? 1 : 0);

View File

@@ -0,0 +1,75 @@
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
const script = path.resolve("skills/lark-slides/scripts/validate_svg_deck.mjs");
function tempDeck() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "svg-slides-validator-"));
fs.mkdirSync(path.join(root, "slides"));
return root;
}
function writeSlide(root, name, body) {
fs.writeFileSync(path.join(root, "slides", name), body);
}
function runValidator(root) {
return spawnSync("node", [script, root, "--json"], { encoding: "utf8" });
}
const validSlide = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="valid" viewBox="0 0 960 540">
<rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="600" height="80" style="font-size:32px;font-family:DM Sans,PingFang SC,Noto Sans SC,Arial,sans-serif;color:rgba(15,23,42,1);line-height:1.2;letter-spacing:0px;padding:0px">
<p xmlns="http://www.w3.org/1999/xhtml" style="margin:0px;font-size:32px;color:rgba(15,23,42,1)">Valid</p>
</foreignObject>
</svg>`;
test("valid SVG deck passes", () => {
const root = tempDeck();
writeSlide(root, "slide_01.svg", validSlide);
const result = runValidator(root);
assert.equal(result.status, 0, result.stderr);
const report = JSON.parse(result.stdout);
assert.equal(report.slideCount, 1);
assert.equal(report.totalErrors, 0);
});
test("invalid SVG deck reports protocol errors", () => {
const root = tempDeck();
writeSlide(root, "slide_01.svg", `<svg xmlns="http://www.w3.org/2000/svg" id="bad" viewBox="0 0 960 540">
<style>.t{fill:red}</style>
<rect width="960" height="540" fill="#fff"/>
<foreignObject slide:role="shape" slide:shape-type="text" x="80" y="80" width="300" height="80" style="font-size:32;color:#111">
<div xmlns="http://www.w3.org/1999/xhtml"><p>Bad</p></div>
</foreignObject>
</svg>`);
const result = runValidator(root);
assert.equal(result.status, 1);
const report = JSON.parse(result.stdout);
const rules = report.results.flatMap((item) => item.errors.map((error) => error.rule));
assert.ok(rules.includes("svg.root.slide-xmlns"));
assert.ok(rules.includes("svg.root.slide-role"));
assert.ok(rules.includes("background.first-child"));
assert.ok(rules.includes("forbid.style-block"));
assert.ok(rules.includes("forbid.div-wrapper"));
assert.ok(rules.includes("color.attr"));
assert.ok(rules.includes("text.style.font-size"));
assert.ok(rules.includes("text.style.color"));
});
test("examples directory remains protocol-valid", () => {
const examples = path.resolve("skills/lark-slides/references/svg-slides/examples");
const result = spawnSync("node", [script, examples, "--json"], { encoding: "utf8" });
assert.equal(result.status, 0, result.stderr);
const report = JSON.parse(result.stdout);
assert.equal(report.slideCount, 3);
assert.equal(report.totalErrors, 0);
});