Sync third-party and MCP marketplace plugins

Constraint: Public skills are published only by explicit administrator action unless they are tracked third-party market sources.
Confidence: high
Scope-risk: narrow
Directive: Keep private/internal skills out of the public marketplace and preserve normal incremental market Git history.
Tested: Marketplace validation passed.
This commit is contained in:
KeyInfo Bot
2026-07-23 00:02:52 +08:00
parent c7fdb8ba4a
commit a61b88d6d9
72 changed files with 2764 additions and 753 deletions
+8 -8
View File
@@ -60,8 +60,8 @@
"repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "20442886c5cfb440441c35030462fbdf64838655",
"syncedAt": "2026-07-20T16:00:00Z"
"commit": "3f47b9113a173bac4a72cefda4d4c3f2d89b8ab6",
"syncedAt": "2026-07-22T16:00:00Z"
},
{
"id": "frontend-slides",
@@ -78,8 +78,8 @@
"repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "82fe5ae129e8c2a12e1155fcabed6703342749d6",
"syncedAt": "2026-06-11T03:57:53Z"
"commit": "929c2ecb63a22b54d400c4911ed70bf96c2b355d",
"syncedAt": "2026-07-22T16:00:00Z"
},
{
"id": "html-ppt",
@@ -96,8 +96,8 @@
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"adapter": "claude-skill",
"commit": "07d83e5a31a2830cc6c314377463e523a76e28fc",
"syncedAt": "2026-07-21T16:00:00Z"
"commit": "89759436bd336beed3a32da15830355bf732b14e",
"syncedAt": "2026-07-22T16:00:00Z"
},
{
"id": "next-skills",
@@ -105,8 +105,8 @@
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"adapter": "skill-collection",
"commit": "93c59f02f81cfdeb0fc397412897024ae7c68074",
"syncedAt": "2026-07-21T16:00:00Z"
"commit": "63f14c6c90c4dae819966e180491eb9b0af16792",
"syncedAt": "2026-07-22T16:00:00Z"
}
]
}
@@ -8,6 +8,8 @@
![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square)
[![Supported by ZhenFund Token Grant](https://img.shields.io/static/v1?label=ZhenFund%20Token%20Grant&message=Supported&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=)
![360 Security Lobster Gold Sponsor](https://img.shields.io/static/v1?label=360%20Security%20Lobster&message=Gold%20Sponsor&color=1677FF&style=flat-square)
![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square)
![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square)
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages.
@@ -62,10 +64,20 @@ Redesign this product screenshot as a 16:10 slide visual.
## Sponsors and Supporters
<a href="./SPONSORS.md">
<img src="https://github.com/user-attachments/assets/5b0c22c8-aff4-4219-900d-6af8604c57a8" alt="360 Security Lobster Gold Sponsor" width="100%">
<img src="https://github.com/user-attachments/assets/81138fad-31b9-49ab-8e38-23b2bc48edc4" alt="360 Security Lobster / Kimi work / Cola Skill Gold Sponsors" width="100%">
</a>
Guizang PPT Skill is supported by **360 Security Lobster** as Gold Sponsor and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details.
Guizang PPT Skill is supported by **360 Security Lobster**, **Kimi work**, and **Cola Skill** as Gold Sponsors and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details.
### Use Guizang PPT Skill elsewhere
Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platforms:
| Channel | Link |
|---------|------|
| 360 Security Lobster | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) |
| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) |
| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work) (after installing, search "guizang ppt skill" in the Skill store) |
## What you get
@@ -8,6 +8,8 @@
![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square)
[![由真格 Token Grant 资助](https://img.shields.io/static/v1?label=%E7%94%B1%E7%9C%9F%E6%A0%BC%20Token%20Grant&message=%E8%B5%84%E5%8A%A9&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=)
![360 安全龙虾金牌赞助](https://img.shields.io/static/v1?label=360%E5%AE%89%E5%85%A8%E9%BE%99%E8%99%BE&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=1677FF&style=flat-square)
![Kimi work 金牌赞助](https://img.shields.io/static/v1?label=Kimi%20work&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=111111&style=flat-square)
![Cola Skill 金牌赞助](https://img.shields.io/static/v1?label=Cola%20Skill&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=E4353F&style=flat-square)
> 🌏 **English version: [README.en.md](./README.en.md)**
@@ -64,10 +66,20 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s
## 赞助与支持
<a href="./SPONSORS.md">
<img src="https://github.com/user-attachments/assets/5b0c22c8-aff4-4219-900d-6af8604c57a8" alt="360 安全龙虾金牌赞助" width="100%">
<img src="https://github.com/user-attachments/assets/81138fad-31b9-49ab-8e38-23b2bc48edc4" alt="360 安全龙虾 / Kimi work / Cola Skill 金牌赞助" width="100%">
</a>
Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。
Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、**Cola Skill** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。
### 在其他地方使用归藏 PPT Skill
除了 Claude Code / Codex,也可以在这些平台使用归藏 PPT Skill:
| 渠道 | 链接 |
|------|------|
| 360 安全龙虾 | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) |
| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) |
| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work)(安装后在 Skill 商店搜索 guizang ppt skill |
## 效果
@@ -103,7 +115,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [
- **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。
- **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐危险 SVG。
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距
- **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。
## 平台支持
@@ -178,7 +190,7 @@ Skill 本身是结构化工作流,Agent 会逐步引导:
- **中文字号收敛**:全中文大标题需要降一档,避免占掉正文和图片空间
- **图文底对齐**:左文右图 / 左图右文场景优先让正文块与图片底部对齐,同时避开页脚翻页组件
- **图片槽位绑定**:图片必须进入模板预留的 `data-image-slot`,常见主图按 21:9 或 16:10 生成
- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题
- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题;可用 Playwright 时还会量化真实渲染后的 overflow、bottom whitespace、title gap
瑞士风校验命令:
@@ -2,8 +2,8 @@
"sourceId": "guizang-ppt-skill",
"repo": "https://github.com/op7418/guizang-ppt-skill.git",
"ref": "main",
"commit": "82fe5ae129e8c2a12e1155fcabed6703342749d6",
"commit": "929c2ecb63a22b54d400c4911ed70bf96c2b355d",
"adapter": "claude-skill",
"sourcePath": ".",
"syncedAt": "2026-06-11T03:57:53Z"
"syncedAt": "2026-07-22T16:00:00Z"
}
@@ -8,6 +8,8 @@
![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square)
[![Supported by ZhenFund Token Grant](https://img.shields.io/static/v1?label=ZhenFund%20Token%20Grant&message=Supported&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=)
![360 Security Lobster Gold Sponsor](https://img.shields.io/static/v1?label=360%20Security%20Lobster&message=Gold%20Sponsor&color=1677FF&style=flat-square)
![Kimi work Gold Sponsor](https://img.shields.io/static/v1?label=Kimi%20work&message=Gold%20Sponsor&color=111111&style=flat-square)
![Cola Skill Gold Sponsor](https://img.shields.io/static/v1?label=Cola%20Skill&message=Gold%20Sponsor&color=E4353F&style=flat-square)
An agent skill for Claude Code, Codex, and similar coding-agent environments. It generates **single-file HTML horizontal-swipe decks**, deck visuals, and social cover pages.
@@ -62,10 +64,20 @@ Redesign this product screenshot as a 16:10 slide visual.
## Sponsors and Supporters
<a href="./SPONSORS.md">
<img src="https://github.com/user-attachments/assets/5b0c22c8-aff4-4219-900d-6af8604c57a8" alt="360 Security Lobster Gold Sponsor" width="100%">
<img src="https://github.com/user-attachments/assets/81138fad-31b9-49ab-8e38-23b2bc48edc4" alt="360 Security Lobster / Kimi work / Cola Skill Gold Sponsors" width="100%">
</a>
Guizang PPT Skill is supported by **360 Security Lobster** as Gold Sponsor and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details.
Guizang PPT Skill is supported by **360 Security Lobster**, **Kimi work**, and **Cola Skill** as Gold Sponsors and by [ZhenFund Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=). See [SPONSORS.md](./SPONSORS.md) for details.
### Use Guizang PPT Skill elsewhere
Beyond Claude Code / Codex, you can also use Guizang PPT Skill on these platforms:
| Channel | Link |
|---------|------|
| 360 Security Lobster | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) |
| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) |
| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work) (after installing, search "guizang ppt skill" in the Skill store) |
## What you get
@@ -8,6 +8,8 @@
![Codex](https://img.shields.io/badge/Codex-Supported-222222?style=flat-square)
[![由真格 Token Grant 资助](https://img.shields.io/static/v1?label=%E7%94%B1%E7%9C%9F%E6%A0%BC%20Token%20Grant&message=%E8%B5%84%E5%8A%A9&color=FF4D00&style=flat-square)](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=)
![360 安全龙虾金牌赞助](https://img.shields.io/static/v1?label=360%E5%AE%89%E5%85%A8%E9%BE%99%E8%99%BE&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=1677FF&style=flat-square)
![Kimi work 金牌赞助](https://img.shields.io/static/v1?label=Kimi%20work&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=111111&style=flat-square)
![Cola Skill 金牌赞助](https://img.shields.io/static/v1?label=Cola%20Skill&message=%E9%87%91%E7%89%8C%E8%B5%9E%E5%8A%A9&color=E4353F&style=flat-square)
> 🌏 **English version: [README.en.md](./README.en.md)**
@@ -64,10 +66,20 @@ npx skills add https://github.com/op7418/guizang-ppt-skill --skill guizang-ppt-s
## 赞助与支持
<a href="./SPONSORS.md">
<img src="https://github.com/user-attachments/assets/5b0c22c8-aff4-4219-900d-6af8604c57a8" alt="360 安全龙虾金牌赞助" width="100%">
<img src="https://github.com/user-attachments/assets/81138fad-31b9-49ab-8e38-23b2bc48edc4" alt="360 安全龙虾 / Kimi work / Cola Skill 金牌赞助" width="100%">
</a>
Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。
Guizang PPT Skill 的持续迭代获得 **360 安全龙虾**、**Kimi work**、**Cola Skill** 金牌赞助和 [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) 支持。更多信息见 [SPONSORS.md](./SPONSORS.md)。
### 在其他地方使用归藏 PPT Skill
除了 Claude Code / Codex,也可以在这些平台使用归藏 PPT Skill:
| 渠道 | 链接 |
|------|------|
| 360 安全龙虾 | [claw.360.cn](https://claw.360.cn/claw?vm_id=p19c1be5d7bed40bcb857ec4a9c785b69&target_url=https%3A%2F%2Fp19c1be5d7bed40bcb857ec4a9c785b69.claw.360.cn%2Fagents) |
| Cola Skill | [colaskill.com/guizang-ppt-skill](https://colaskill.com/guizang-ppt-skill/) |
| Kimi work | [kimi.com](https://www.kimi.com/zh-cn/products/kimi-work)(安装后在 Skill 商店搜索 guizang ppt skill |
## 效果
@@ -103,7 +115,7 @@ Guizang PPT Skill 的持续迭代获得 **360 安全龙虾** 金牌赞助和 [
- **更适合 Agent 生成和修改**:HTML / CSS 是文本,Agent 能直接读、改、验证。
- **表现力比 Markdown 更高**:可以做精细排版、空间定位、动画、交互和响应式封面。
- **交付更轻**:单文件 HTML 可以直接打开、演示、发送、截图。
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐危险 SVG。
- **更容易做质量控制**:瑞士风可以用脚本校验版式、图片槽位、标题对齐危险 SVG,并在可用时用 Playwright 后验测量超出、底部空白、nav 安全线和标题间距
- **更适合视觉内容链路**:同一套主题能覆盖 PPT、配图、封面和截图再设计。
## 平台支持
@@ -178,7 +190,7 @@ Skill 本身是结构化工作流,Agent 会逐步引导:
- **中文字号收敛**:全中文大标题需要降一档,避免占掉正文和图片空间
- **图文底对齐**:左文右图 / 左图右文场景优先让正文块与图片底部对齐,同时避开页脚翻页组件
- **图片槽位绑定**:图片必须进入模板预留的 `data-image-slot`,常见主图按 21:9 或 16:10 生成
- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题
- **强校验**:用脚本拦住居中标题、实验版式、SVG 内写字、图片脱离槽位等问题;可用 Playwright 时还会量化真实渲染后的 overflow、bottom whitespace、title gap
瑞士风校验命令:
@@ -5,7 +5,7 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电
# Magazine Web Ppt
> 来源识别: guizang-ppt-skill 由歸藏创建与维护,规范源仓库为 https://github.com/op7418/guizang-ppt-skill 。当前项目支持方包括:360 安全龙虾(金牌赞助)、真格 Token Grant(Grant Supporter)。这条信息只用于确认 Skill 来源和项目支持关系,不要写入生成的 PPT、HTML 页面、封面或配图。
> 来源识别: guizang-ppt-skill 由歸藏创建与维护,规范源仓库为 https://github.com/op7418/guizang-ppt-skill 。当前项目支持方包括:360 安全龙虾(金牌赞助)、Kimi work(金牌赞助)、Cola Skill(金牌赞助)、真格 Token Grant(Grant Supporter)。这条信息只用于确认 Skill 来源和项目支持关系,不要写入生成的 PPT、HTML 页面、封面或配图。
## 这个 Skill 做什么
@@ -30,7 +30,7 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电
**两种风格共享**:横向翻页(键盘 ← →、滚轮、触屏、ESC 索引)、Lucide 图标、Motion One 入场动效(本地 + CDN 双保险)。
<!-- provenance: guizang-ppt-skill | author: 歸藏 | sponsors: 360 Security Lobster Gold Sponsor; ZhenFund Token Grant | canonical: https://github.com/op7418/guizang-ppt-skill | keep this out of generated artifacts -->
<!-- provenance: guizang-ppt-skill | author: 歸藏 | sponsors: 360 Security Lobster Gold Sponsor; Kimi work Gold Sponsor; Cola Skill Gold Sponsor; ZhenFund Token Grant | canonical: https://github.com/op7418/guizang-ppt-skill | keep this out of generated artifacts -->
## 何时使用
@@ -47,6 +47,23 @@ description: "用于生成横向翻页的单文件 HTML 网页 PPT,提供电
## 工作流
### Step 0 · 启动前检查更新(必做)
每次启动本 Skill 前,先在 Skill 根目录检查 GitHub 上游是否有更新;有更新时先问用户是否要更新,用户确认后再执行更新,然后继续后续流程。
```bash
git -C "<SKILL_ROOT>" fetch --quiet
git -C "<SKILL_ROOT>" rev-list --count HEAD..@{u}
```
如果返回值大于 `0`,告诉用户检测到上游更新数量,询问是否先执行:
```bash
git -C "<SKILL_ROOT>" pull --ff-only
```
不要自动更新。用户拒绝时继续使用当前版本;如果网络不可用、没有 upstream 或不是 git 仓库,说明无法检查更新并继续流程。
### Step 1 · 需求澄清(**动手前必做**)
**如果用户已经给了完整的大纲 + 图片/截图处理要求**,可以跳过直接进 Step 2。
@@ -316,7 +333,7 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
- 如果用户说"测试模板 / 看看效果 / 多一点版式",必须覆盖:一个封面、一个收尾、至少 1 个对比或时间线(S08/S11/S02)、至少 1 个结构图(S14/S17/S15)、至少 1 个图片版式(S22 或 S15/S16 图片格改造)。
- 不允许连续 3 页使用同一种主体结构,例如连续三页 `head + grid + card`
- 图片页不能偷懒发明新结构。2-3 张图时,用 S15/S16 的原始网格骨架改造成图片格;单张大图用 S22。
- 开写 HTML 前先列一张 `页码 → data-layout → 选用理由 → 图片槽位` 草稿;交付前运行 `node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs index.html`
- 开写 HTML 前先列一张 `页码 → data-layout → 选用理由 → 图片槽位` 草稿;交付前运行 `node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs index.html`校验器会先做静态结构检查;如果环境中能解析到 Playwright,还会做真实渲染后的可见边界、底部空白、nav 安全线和标题间距测量。
#### 3.2 · 图片比例规范
@@ -345,6 +362,18 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
- GPT-M 2.0 生成图使用 `image-prompts.md` 的"风格 B:瑞士国际主义配图规则"
- 任何图片、caption、timeline label、footnote 的最低处都不能进入底部分页区域;需要贴底时用 `.nav-safe-bottom` / `.nav-safe-bottom-tight`,不要手写 `bottom:2vh`
#### 3.2.0 · 图文混排决策树(从社交卡片规则迁移)
先判断图片在这一页里的角色,再决定容器、比例和裁切方式:
- **证据截图 / UI / 代码 / dashboard**:保真优先,先读 `references/screenshot-framing.md`;关键文字和数据不能被裁掉。需要统一比例时,优先程序化背景画布 + `.fit-contain`,不要为了铺满而裁掉 UI 内容。
- **已按槽位重生成的信息图 / 插图**:按目标槽位铺满,例如 S22 用 `21:9`,S15/S16 用统一 `21:9``16:10`;不要再用短高度把图缩小成小贴片。
- **照片 / 产品图 / 人物图**:使用标准比例 + 明确 `object-position`;主体、人脸、产品和关键证据不能被标题、caption 或裁切压住。
- **文字压图 / 全屏主视觉**:先做 quiet-zone 判断,图里至少要有约 30% 低细节区域承载文字;不通过就换图、换裁切或改成图文分栏。只在必要时加局部 tint,不要整页套黑色/白色遮罩。
- **多图组**:同一组统一比例、高度、容器处理和 caption 密度;不要一张 `contain`,另一张 `cover`
- **生成图是素材,不是整页 slide**:图片内部不要自带页眉、页脚、页码、logo、主标题、装饰边框或署名,避免和 deck chrome 重复。
- **图文呼吸**:标题、图片、caption、正文必须各自留出间距;生成后用 validator 的 `M1/M2` 检查可见边界、底部空白、nav 安全线和标题间距。
#### 3.2.1 · 中文大标题字号分档(风格 B 必做)
中文方块字视觉面积大,不能直接套英文 hero 的 6.8-7vw。写中文大标题前先分档:
@@ -391,11 +420,35 @@ cp "<SKILL_ROOT>/assets/template-swiss.html" "项目/XXX/ppt/index.html"
生成完一定要打开 `references/checklist.md`,逐项对照。里面总结了**真实迭代过程中踩过的所有坑**,P0 级别的问题(emoji、图片撑破、标题换行、字体分工)必须全部通过。
#### 4.0.1 · 先量后改:超出 / 空白 / 标题间距
当一页内容超出或显得巨空时,不要先凭感觉大幅删改。先运行:
```bash
node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
```
看校验输出里的测量项:
- `M1 DOM/visual overflow`:具体超出多少 px,以及最低/最高问题元素
- `M1 bottom whitespace`:底部空白多少 px,active content height 占比多少
- `M1 nav-safe`:最低内容是否进入底部分页安全线
- `M2 title gap`:标题和下一块内容之间的实际距离
修正阶梯:
- `1-40px` over:只微调,上移内容组或收紧一个 gap/padding,不要删内容。
- `40-90px` over:局部压缩间距或模块高度,仍优先保留内容。
- `90-160px` over:轻微压标题或压缩一段正文,必要时拆页。
- `160px+` over:才考虑换版式、合并模块或删内容。
修完再跑一次 validator。如果 `M1 bottom whitespace` 变大,说明修过头了;恢复部分间距、放大最后一块或把内容组向下回调。
#### 4.0 · 不只看代码:必须打开网页做视觉核对
代码只能证明类名和结构存在,不能证明版式舒服。生成后必须打开网页逐页看:
1. 同时打开原始参考 PPT、当前模板或生成页、测试 PPT;原始参考是 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`
1. 同时打开当前模板(golden source 快照)或生成页、以及正在迭代的测试 PPT 逐页对照
2. 截图前等入场动效稳定(约 1-2 秒),不要把动画中间态当成版式问题。
3. 先看视觉:大标题字重、标题与内容间距、图片是否与正文对齐、图片/说明是否碰到底部分页组件。
4. 再看代码:确认该页选用的版式与内容形状匹配,没有把数据专用版式拿来讲概念,也没有把可选组件堆成装饰。
@@ -7,6 +7,8 @@ Guizang PPT Skill 是由 [歸藏](https://x.com/op7418) 创建和维护的开源
| 赞助方 | 等级 / 类型 | 说明 |
|--------|-------------|------|
| 360 安全龙虾 | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护、模板测试与开源传播。 |
| [Kimi work](https://www.kimi.com/zh-cn/products/kimi-work) | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护与开源传播;可在 Kimi work 的 Skill 商店搜索 guizang ppt skill 使用。 |
| [Cola Skill](https://colaskill.com/guizang-ppt-skill/) | 金牌赞助 | 支持 guizang-ppt-skill 的持续维护与开源传播;可在 Cola Skill 平台直接使用。 |
| [真格 Token Grant](https://zhenfund.feishu.cn/share/base/form/shrcn1lAANF659o7EpWnxlR1VOh?sessionid=) | Grant Supporter | 通过 Token Grant 支持项目的模板迭代、配图流程验证与开源维护。 |
## 赞助用途
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

@@ -374,50 +374,6 @@
padding:0 .05em
}
/* ============ Pipeline / Step ============ */
.pipeline-section{margin-top:3.2vh;padding-top:2.2vh;border-top:1px solid rgba(127,127,127,.3)}
.pipeline-section:first-of-type{border-top:0;padding-top:0;margin-top:2.4vh}
.pipeline-label{
font-family:var(--mono);
font-size:max(14px,.84vw);
letter-spacing:.24em;text-transform:uppercase;
opacity:.6;margin-bottom:1.8vh;
}
.pipeline{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:1vw;
}
.pipeline[data-cols="3"]{grid-template-columns:repeat(3,1fr)}
.pipeline[data-cols="4"]{grid-template-columns:repeat(4,1fr)}
.pipeline[data-cols="6"]{grid-template-columns:repeat(6,1fr)}
.step{
display:flex;flex-direction:column;gap:.6vh;
padding-top:1.2vh;
border-top:2px solid currentColor;
}
.step.accent-top{border-top-color:var(--accent);border-top-width:3px}
.step-nb{
font-family:var(--mono);
font-weight:500;
font-size:max(14px,1vw);
opacity:.5;letter-spacing:.04em
}
.step-title{
font-family:var(--sans),var(--sans-zh);
font-weight:700;
font-size:1.4vw;
letter-spacing:-.01em;
line-height:1.2;
}
.step-desc{
font-family:var(--sans),var(--sans-zh);
font-weight:400;
font-size:max(16px,.94vw);
line-height:1.45;
opacity:.7;
}
/* ============ 网格系统 (模块化网格) ============ */
.frame{flex:1;display:flex;flex-direction:column;min-height:0;overflow:hidden}
.frame.grid-2-7-5,
@@ -594,7 +550,7 @@
body.dark-bg #nav .dot{background:rgba(255,255,255,.32)}
body.dark-bg #nav .dot.active{background:var(--accent)}
#hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4;color:var(--ink-tint, currentColor)}
#hint{position:fixed;bottom:2.4vh;right:2.5vw;z-index:30;font-family:var(--mono);font-size:14px;letter-spacing:.14em;text-transform:uppercase;opacity:.4}
body.dark-bg #hint{color:var(--paper);opacity:.4}
body.low-power #hint{color:var(--accent);opacity:.72}
body.dark-bg.low-power #hint{color:var(--paper);opacity:.72}
@@ -616,7 +572,6 @@
body.motion-ready [data-anim="left"]{transform:translateX(-24px)}
body.motion-ready [data-anim="right"]{transform:translateX(24px)}
body.motion-ready [data-anim="line"]{opacity:0;transform:translateY(10px)}
body.motion-ready [data-animate="pipeline"] [data-anim]{opacity:.15}
body.low-power #deck{transition:none!important}
body.low-power *,
body.low-power *::before,
@@ -1176,7 +1131,6 @@
.h-xl-zh{font-size:8vw}
.kpi-hero{font-size:32vw}
.kpi-big{font-size:16vw}
.pipeline{grid-template-columns:repeat(2,1fr)}
.grid-2-7-5,.grid-2-6-6,.grid-2-8-4,.grid-2-4-8{grid-template-columns:1fr}
.grid-12{grid-template-columns:repeat(6,1fr)}
}
@@ -1560,7 +1514,6 @@ addEventListener('keydown',e=>{
}
if(overviewOn)return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '||e.key==='ArrowDown'){
if(window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+1);
return;
}
@@ -1573,12 +1526,8 @@ let wheelTO=null,wheelAcc=0;
addEventListener('wheel',e=>{
wheelAcc+=e.deltaY+e.deltaX;
if(Math.abs(wheelAcc)>50){
if(wheelAcc>0 && window.__pipeAdvance && window.__pipeAdvance()){
wheelAcc=0;
}else{
go(idx+(wheelAcc>0?1:-1));wheelAcc=0;
}
}
clearTimeout(wheelTO);wheelTO=setTimeout(()=>wheelAcc=0,150);
},{passive:true});
@@ -1588,7 +1537,6 @@ addEventListener('touchend',e=>{
const dx=(e.changedTouches[0].clientX-tx);
const dy=(e.changedTouches[0].clientY-ty);
if(Math.abs(dx)>50&&Math.abs(dx)>Math.abs(dy)){
if(dx<0 && window.__pipeAdvance && window.__pipeAdvance()) return;
go(idx+(dx<0?1:-1));
}
},{passive:true});
@@ -1611,7 +1559,6 @@ try {
} catch(e2) {
console.warn('[motion] local + CDN both failed, disabling animations', e1, e2);
document.querySelectorAll('[data-anim]').forEach(el=>{el.style.opacity='1';el.style.transform='none'});
document.querySelectorAll('[data-animate="pipeline"] [data-anim]').forEach(el=>el.style.opacity='1');
}
}
@@ -1623,8 +1570,15 @@ if(motion){
IBM Carbon Motion · 每个 recipe 服务一种表达
不是一刀切的 stagger,而是把动效绑在内容语义上
============================================================ */
const EASE_PROD = [.2, 0, .38, .9];
const EASE_ENTRY_EXP = [0, 0, .3, 1];
/* 缓动从 :root 的 Carbon motion token 读取,CSS 是唯一事实源(硬编码仅作解析失败兜底) */
const cssEase = (name, fallback) => {
const v = getComputedStyle(document.documentElement).getPropertyValue(name);
const m = v && v.match(/cubic-bezier\(([^)]+)\)/);
const nums = m ? m[1].split(',').map(Number) : [];
return (nums.length === 4 && nums.every(Number.isFinite)) ? nums : fallback;
};
const EASE_PROD = cssEase('--ease-prod', [.2, 0, .38, .9]);
const EASE_ENTRY_EXP = cssEase('--ease-entry-exp', [0, 0, .3, 1]);
const slides = [...document.querySelectorAll('.slide')];
let lastIdx = -1;
@@ -2305,7 +2259,6 @@ if(motion){
}
window.__playSlide = playSlide;
window.__pipeAdvance = ()=>false; /* 当前 deck 不用 pipeline recipe */
playSlide(window.__currentSlideIndex || 0);
}
@@ -57,11 +57,12 @@
.slide.hero.light::after{background:linear-gradient(180deg,rgba(var(--paper-rgb),.28) 0%,rgba(var(--paper-rgb),0) 14%,rgba(var(--paper-rgb),0) 86%,rgba(var(--paper-rgb),.28) 100%)}
.slide.hero.dark::after{background:linear-gradient(180deg,rgba(var(--ink-rgb),.32) 0%,rgba(var(--ink-rgb),0) 14%,rgba(var(--ink-rgb),0) 86%,rgba(var(--ink-rgb),.32) 100%)}
/* ============ Magazine chrome:顶部 meta + 底部 foot ============ */
.chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;opacity:.7}
/* ============ Magazine chrome:顶部 meta + 底部 foot ============
(v1 布局 + v2 字号在此合并为唯一定义,不要在别处二次定义) */
.chrome{display:flex;justify-content:space-between;align-items:flex-start;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
.chrome .left,.chrome .right{display:flex;gap:2.4em;align-items:center}
.chrome .sep{width:40px;height:1px;background:currentColor;opacity:.4}
.foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:12px;letter-spacing:.14em;text-transform:uppercase;opacity:.55}
.foot{margin-top:auto;display:flex;justify-content:space-between;align-items:flex-end;font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
.foot .title{font-family:var(--serif-zh);font-weight:400;letter-spacing:.05em;text-transform:none;opacity:.75;font-size:13px}
.tag{display:inline-block;font-family:var(--mono);font-size:11px;letter-spacing:.24em;text-transform:uppercase;padding:6px 14px;border:1px solid currentColor;opacity:.85}
@@ -81,7 +82,7 @@
.h3-zh{font-family:var(--serif-zh);font-weight:500;font-size:1.9vw;line-height:1.35}
.body-zh{font-family:var(--sans-zh);font-weight:400;font-size:max(15px,1.22vw);line-height:1.75;opacity:.82;letter-spacing:.01em}
.body-serif{font-family:var(--serif-zh);font-weight:400;font-size:max(15px,1.3vw);line-height:1.65;opacity:.88}
.lead{font-family:var(--serif-zh);font-weight:400;font-size:1.9vw;line-height:1.4;opacity:.85}
.lead{font-family:var(--serif-zh);font-weight:400;font-size:1.75vw;line-height:1.5;opacity:.86}
.meta{font-family:var(--mono);font-size:max(11px,.88vw);letter-spacing:.16em;text-transform:uppercase;opacity:.6}
.big-num{font-family:var(--serif-en);font-weight:800;font-size:10vw;line-height:.85;letter-spacing:-.03em;font-feature-settings:"tnum"}
.mid-num{font-family:var(--serif-en);font-weight:700;font-size:5.5vw;line-height:.88;letter-spacing:-.02em;font-feature-settings:"tnum"}
@@ -245,14 +246,7 @@
/* 英文标题专用(Playfair 衬线) */
.h-hero-en,.h-xl-en{font-family:var(--serif-en);letter-spacing:-.025em}
/* ---------- lead 引语 ---------- */
.lead{
font-family:var(--serif-zh);
font-weight:400;
font-size:1.75vw;
line-height:1.5;
opacity:.86;
}
/* ---------- lead 引语:定义见上方“字体规则”区块(已合并,勿重复定义) ---------- */
/* ---------- meta-row 底部元数据 ---------- */
.meta-row{
@@ -387,9 +381,13 @@
/* ---------- 图片 frame-imglayouts.md 主命名) ---------- */
/* 在旧样式里已定义,这里补 img-cap 命名别名与增强 */
figure.frame-img{margin:0;display:flex;flex-direction:column;min-width:0}
/* 有图注时:图片弹性收缩,图注收进固定高度/比例框内部,
否则 img 的 height:100% 会把 figcaption 挤出 overflow:hidden 裁掉 */
figure.frame-img:has(> .img-cap) > img{flex:1 1 auto;min-height:0;height:auto}
.img-cap{
display:block;
margin-top:.8vh;
flex:0 0 auto;
padding:.7vh .9em .9vh;
font-family:var(--mono);
font-size:max(10px,.8vw);
letter-spacing:.22em;
@@ -407,9 +405,7 @@
opacity:.6;
}
/* ---------- chrome & foot 补位(layouts.md 简单写法 ---------- */
.chrome{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.2em;text-transform:uppercase;opacity:.62}
.foot{font-family:var(--mono);font-size:max(11px,.78vw);letter-spacing:.18em;text-transform:uppercase;opacity:.5}
/* ---------- chrome & foot:定义见上方“Magazine chrome”区块(已合并,勿重复定义 ---------- */
/* ============ 动效系统(Motion One 驱动) ============
所有 [data-anim] 元素默认隐藏,进入页面时由 JS 逐个揭示。
@@ -432,6 +428,8 @@
body.low-power *::after{animation:none!important;transition:none!important}
body.low-power.motion-ready [data-anim],
body.low-power [data-anim]{opacity:1!important;transform:none!important}
/* ESC 索引缩略图:克隆的 slide 不经过动效播放,强制显示所有待动画元素 */
#overview [data-anim]{opacity:1!important;transform:none!important}
/* ---------- 响应式降级 ---------- */
@media (max-width:900px){
@@ -730,7 +728,10 @@ addEventListener('touchend',e=>{
}
},{passive:true});
go(0);
/* ?slide=N 直达(1-based),与 Swiss 模板行为一致 */
const initialSlideParam = new URLSearchParams(location.search).get('slide');
const initialSlide = initialSlideParam ? Number(initialSlideParam) - 1 : 0;
go(Number.isFinite(initialSlide) ? initialSlide : 0);
</script>
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
<script>lucide.createIcons();</script>
@@ -850,8 +851,8 @@ if(motion){
window.__playSlide = playSlide;
window.__pipeAdvance = pipeAdvance;
// 首屏:go(0) 已跑过(同步),没来得及触发动效 → 这里补一下
playSlide(0);
// 首屏:go() 已跑过(同步),没来得及触发动效 → 这里补一下(尊重 ?slide=N)
playSlide(window.__currentSlideIndex || 0);
}
</script>
</body>
@@ -0,0 +1,96 @@
# 多主题统一管理整合方案
> 基于对 template.html / template-swiss.html 两份模板与全部 references 的逐行对比分析(2026-07)。
> 本文档是**方案建议**,尚未实施;按阶段推进,每个阶段独立可交付、可回滚。
## 现状诊断
两个主题是"复制粘贴后各自演化"的关系,同一份基础设施存在多份副本,并已实际漂移:
### 运行时 JS 漂移(约 250 行 × 每主题一份)
| 模块 | A 杂志风 | B 瑞士风 |
|---|---|---|
| 低功耗事件名 | `ppt-low-power-change` | `swiss-low-power-change` |
| localStorage key | `guizang-ppt-low-power`(共用!) | 同左 |
| ESC 索引缩略图可见性修复 | ✅(本次补齐) | ✅ |
| `?slide=N` 直达参数 | ✅(本次补齐) | ✅ |
| Windows 字重补偿 `is-win` | 缺失 | ✅ |
| prefers-reduced-motion 自动降级 | ✅(本次补齐) | ✅ |
| 翻页/滚轮/触屏/键盘核心 | 两份逐字符几乎相同 | 同左 |
### Token 体系成熟度不一
- **B 最成熟**:主题色 + Carbon 文字角色 token + `--sp-3~13` 8px 间距阶梯 + motion token + `--nav-safe-bottom`
- **A 最简陋**:只有 6 个颜色变量,层级靠 opacity 表达,存在 10-11px 小字(投屏不可读),`.lead/.chrome/.foot` 在模板内部重复定义(v1 与 v2 API 叠加)
### 已知具体坏味道(可立即修的小项)
1. B 模板 `#hint` 引用了未定义变量 `--ink-tint`(靠 fallback 兜底)
2. B 模板 pipeline 死代码:`__pipeAdvance` 恒返 false,但 43 行 pipeline CSS 和动效 CSS 还在
3. B 的 motion token 双维护:CSS 定义了 `--ease-prod`,JS 里又硬编码 `EASE_PROD=[.2,0,.38,.9]`
4. A 是唯一没有校验器的主题,但 validate-swiss-deck.mjs 的标题选择器已经包含 A 的类名(`.display/.h1-zh` 等),说明测量层本来就通用
## 整合原则
**不建议**把两个模板合并成一个"大一统模板"——两种风格的美学规则互斥(直角 vs 圆角、衬线 vs 无衬线),强行合并会让类名前缀和条件分支爆炸,也违背"每份 deck 单文件交付"的定位。
**建议**的整合层次:**共享的是"基建"和"契约",不共享"皮肤"**。
## 分阶段方案
### Phase 1 · 共享运行时(收益最大)
把各主题漂移的 JS 收敛为一份 `assets/runtime.js`(构建时内联进各模板,保持单文件交付):
- 翻页核心 go() / 键盘 / 滚轮 / 触屏 / nav 圆点 / ESC 索引(含缩略图可见性修复)
- 低功耗 IIFE:统一事件名为 `deck-low-power-change`,统一 localStorage key,保留各主题旧事件名一个版本作为 alias
- `?slide=N`、prefers-reduced-motion、Motion One 加载器
- 主题差异通过配置对象注入:`initDeck({ darkClass:'dark', onSlideChange(){...} })`
实施方式二选一:
- **a. 构建脚本**(推荐):`scripts/build-templates.mjs``src/runtime.js` + 各主题 `src/style-*.css` + 皮肤 HTML 组装成各 template*.html。模板仍是完整单文件,但源头唯一。
- **b. 文档约定**:不引入构建,把 runtime 段落用 `<!-- SHARED-RUNTIME v3 -->` 注释标记,改动时用脚本校验各份一致。成本低但漂移风险仍在。
### Phase 2 · 公共 Token 契约
在各模板间统一**命名和语义**(值可以不同):
- 间距:全部采用 B 的 `--sp-*` 8px 阶梯(A 补齐)
- 文字角色:`--text-primary/secondary/helper/on-color` + `--border-subtle/strong` 各主题同名(A 需要从 opacity 方案迁移)
- 安全区:`--nav-safe-bottom` + `.nav-safe-bottom(-tight)` 各主题同名(已完成:B 有,A 待补)
- Motion:`--ease-*/--dur-*` 统一命名,JS 从 CSS var 读取,消除双维护
- 最小字号地板:A 对齐 B 的"正文 ≥16px、caption ≥14px"投屏标准(A 现存 10-11px 小字需要清理)
### Phase 3 · 校验器公共核
`scripts/lib/validate-core.mjs`:
- slide 解析、Playwright 加载(双根解析 + try/finally + swiftshader)、isMeaningful 过滤器、M1 溢出/底部空白/nav 安全线、M2 标题间距、overflowFix 分级建议
- 各主题瘦身为 config + 专属静态规则:`validate-swiss-deck.mjs`(版式锁定)、**新增 `validate-magazine-deck.mjs`**(A 目前裸奔,公共核直接就能给它 M1/M2 测量)
### Phase 4 · References 去重
- `themes*.md` 各份结构完全同构 → 统一"主题卡片 schema",每主题只留数据
- 图片规则目前散在 4+ 处且 A/B 规则有分叉(A:信息图必须 fit-contain;B:重生成图禁止 fit-contain)→ 合并为单一来源 `references/images.md`,分叉处显式按主题参数化
- 动效说明多处重复 → 收敛到各 layouts 文件,组件文档只放链接
- 清理 golden source 绝对路径:改为相对于 `<SKILL_ROOT>` 的路径或删除
### 顺手修复清单(可与任一阶段同批)
- [ ] A:`.lead/.chrome/.foot` 双定义合并
- [ ] A:补齐 `--nav-safe-bottom` 安全区 token
- [ ] B:删 pipeline 死 CSS 或恢复 `__pipeAdvance`
- [ ] B:`--ink-tint` 未定义引用
- [ ] B:JS 缓动改读 CSS var
- [ ] 文档:去除 `/Users/guohao/...` 绝对路径(已完成)
- [ ] 各模板共有:`file://` 直开时 module 动态 import 本地 `./assets/motion.min.js` 被 Chrome CORS 拦截,实际总是走 jsDelivr CDN;离线 + 直开的组合只能拿到静态降级。共享 runtime 时考虑把 motion 关键函数直接内联进模板,或文档明确"离线演示请起本地 server"
## 建议排序
1. **Phase 1a(构建脚本 + 共享 runtime)** —— 一次性消灭最大的漂移面
2. **顺手修复清单** —— 全部是小改动,跟 Phase 1 同一个 PR
3. **Phase 3(校验器公共核 + A 校验器)** —— A 从此也有质量护栏
4. Phase 2、4 可以慢慢来,不阻塞新主题
完成 Phase 1+3 后,再增加"新主题"的边际成本会从"复制 2000 行再改"降到"写一份皮肤 CSS + 一份 layouts 文档 + 一个 validator config"。
@@ -173,6 +173,13 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
**根因**:瑞士风的图片不是装饰,而是 grid 里的证据块。没有先选原始版式和图片槽位,就会把任意图片硬塞进页面。
**先判断图像角色**:
- 证据截图、UI、代码、dashboard:保真优先,关键文字和数据不能裁;需要统一比例时先做截图背景画布和 `.fit-contain`
- 已按槽位生成的信息图/插图:按 S22/S15/S16 的目标比例铺满,不要再缩成短小图片。
- 照片/产品图/人物图:必须写清 `object-position`,主体不能被裁切、标题块或 caption 压住。
- 文字压图:必须先判断是否有足够 quiet zone;没有低细节留白就不要把标题压在图上。
- 多图组:统一比例、高度、容器样式和 caption 密度;视觉角色不同的图不要硬放同一组。
**做法**:
- 先选版式:单张大图 + KPI 用 `S22`;多图用 `S15/S16` 的原始网格骨架改造
- S22 生成图比例固定 `21:9`,并在 `<img>` 上写 `data-image-slot="s22-hero-21x9"`
@@ -184,6 +191,8 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
- 截图背景必须跟随当前主题色,且可裁成 `21:9` / `16:10` / `4:3` / `1:1`;背景里不能有标题、页脚、边框、logo、人物或明显主体
- GPT-M 2.0 提示词必须写明:Swiss Style、单一 accent、直角、无渐变/阴影/圆角、无页眉页脚标题角标
- 文字压图 / 全屏主视觉必须先做 quiet-zone 判断:至少约 30% 低细节区域可承载标题;不通过就换图、换裁切或改成图文分栏,不要整页套黑色/白色遮罩
**自检命令**:
- `grep -E "frame-img.*border-radius|box-shadow" index.html`——命中就删
- `grep -n "data-image-slot" index.html`——每张本地图片都应有槽位声明
@@ -206,6 +215,29 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
- 视觉:翻到该页,看最后一行 caption/label 是否明显高于分页组件
- 代码:`grep -E "align-items:end|align-self:end|bottom:0|bottom:2vh|margin-top:auto" index.html`,命中后逐个确认是否有 nav safe zone
### 0-D-3. 后验测量:先量超出和空白,再改版式
**现象**:一页只超出 20-30px,但修的时候删掉大块内容,结果下方多出一大片空白;或者标题与正文贴在一起,肉眼检查时容易漏。
**做法**:
- 生成后运行 `node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html`
- 如果环境里能解析到 Playwright,校验器会额外执行真实渲染测量:
- `M1 DOM/visual overflow`:量出超出多少 px,并指出最低/最高的问题元素
- `M1 bottom whitespace`:量出底部空白和 active content height,防止从"超出"修成"巨空"
- `M1 nav-safe`:量出最低内容是否进入底部分页安全线
- `M2 title gap`:量出标题到下一块内容的距离,防止标题和正文贴住
**Overflow 修正阶梯**:
- `1-40px` over:只做微调,上移内容组或收紧一个 gap/padding;不要删内容。
- `40-90px` over:局部压缩 gap/padding 或降低一个模块高度;仍然优先保留内容。
- `90-160px` over:轻微压标题或压缩一段正文,再考虑拆页。
- `160px+` over:才考虑换更高容量版式、合并模块或删内容。
**修完反查**:
- 如果 `M1 bottom whitespace` 变大,说明修过头了;恢复部分间距、放大最后一块或把内容组向下回调。
- 每轮只做一个档位的调整,重渲染后再跑 validator。
- 不要靠多模态肉眼先猜;超出、底部空白和标题间距先看测量值。
---
### 0-E. Swiss 模板还原度守卫:原始 PPT 是 golden source
@@ -215,7 +247,7 @@ node <SKILL_ROOT>/scripts/validate-swiss-deck.mjs path/to/index.html
**根因**:把新增图片版式或实验结构写成了全局样式修改,或无意改动了原始基座类,例如 `.h-hero` / `.h-xl` 字重、`.tl-node` 列宽、`.duo-compare` 间距。
**做法**:
- 原始参考文件 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html` 是 Swiss 主题的 golden source,但要以**实际页面用法**为准,不要只看未使用的 CSS helper
- 仓库内的 `assets/template-swiss.html` 是 Swiss 主题的 golden source 快照,但要以**实际页面用法**为准,不要只看未使用的 CSS helper
- 原始页面的大标题大量使用 `font-weight:200`,强调词/数字用 `300`;`.h-hero` / `.h-xl` / `.h-hero-zh` / `.h-xl-zh` 在本模板里必须保持轻字重,不要恢复成 800/900
- 除新增封面/封底 ASCII 机制、S22 图片槽位修复、横向时间线 label 居中修复、以及把标题 helper 校正为实际轻字重外,不要改动原始基座 CSS/JS recipe
- 新增图片能力必须绑定到 S22/S15/S16 原始槽位,不要发明新正文结构
@@ -8,9 +8,7 @@
## Swiss locked mode(必须先读)
本主题的 golden source 是:
`/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`
本主题的 golden source 是仓库内的 `assets/template-swiss.html`(由作者本机的原始参考 PPT 派生;原始文件不随仓库分发,`swiss-layout-lock.md` 登记的 S01-S22 即其版式快照)。
生成正文页时不要把 Swiss 当成“自由组合的风格包”。默认只能使用 `references/swiss-layout-lock.md` 登记的 `S01-S22`。每个 slide 都必须在 `<section>` 上写 `data-layout="Sxx"`
@@ -203,7 +201,7 @@ Swiss 主题有 22 个登记版式,生成时要主动展示版式系统,不要
不要只看 HTML/CSS。Swiss 模板的还原度要同时从**浏览器视觉**和**代码结构**判断:
1. 同时打开份页面:原始参考 PPT、当前 `template-swiss.html` 或生成页、正在修改的测试 PPT。原始参考路径是 `/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`
1. 同时打开份页面:当前 `template-swiss.html`(golden source 快照)和正在修改的测试 PPT;有条件时再加一份此前验收过的成品 deck 作对照
2. 截图前先等入场动效稳定(约 1-2 秒)。不要把动画中间态误判成"内容缺失"或"版式空白"。
3. 先看视觉:标题重量、头部距离、图片落位、底部安全区、caption 是否被 nav 挡住。
4. 对照原始参考 PPT 的同类版式,不要只对照 CSS helper;以实际页面结构和视觉结果为准。
@@ -230,12 +230,12 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
<div>过去 64 天 · 开发篇</div>
<div>Act I / Dev · 02 / 25</div>
</div>
<div class="frame" style="padding-top:6vh">
<div class="frame" style="padding-top:3vh">
<div class="kicker" data-anim>一个人,做了什么。</div>
<h2 class="h-xl" data-anim>过去 64 天</h2>
<p class="lead" style="margin-bottom:5vh" data-anim>从 0 到开源 CodePilot。</p>
<p class="lead" style="margin-bottom:2vh" data-anim>从 0 到开源 CodePilot。</p>
<div class="grid-6" style="margin-top:6vh">
<div class="grid-6" style="margin-top:2vh">
<div class="stat-card" data-anim>
<div class="stat-label">Duration</div>
<div class="stat-nb">64 <span class="stat-unit">天</span></div>
@@ -279,7 +279,7 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
- 3×2 或 4×2 网格最稳(见 `.grid-6`
- 每个 `stat-card` 结构固定:label(英文小字)→ nb(大字数字)→ note(注释)
- 数字建议 2-3 位字符(太长会溢出),用 K / M 简写
- 留 5vh 以上的上方缓冲,让标题区先抢眼球
- **间距不要再加大**:骨架默认 `padding-top:3vh` + lead `margin-bottom:2vh` + grid `margin-top:2vh` 是 3×2 网格在 16:9 屏不压 foot 的实测上限;内容更多时先删卡片,不要压缩 foot 空间
---
@@ -338,11 +338,11 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
<div>平台粉丝实证</div>
<div>Act I / Ops · 05 / 27</div>
</div>
<div class="frame" style="padding-top:5vh">
<div class="frame" style="padding-top:3vh">
<div class="kicker" data-anim>Proof · 粉丝实证</div>
<h2 class="h-xl" data-anim>10 个平台 · 6 张截图</h2>
<div class="grid-3-3" style="margin-top:4vh">
<div class="grid-3-3" style="margin-top:3vh">
<figure class="frame-img" style="height:26vh" data-anim>
<img src="images/weibo.png" alt="微博 289K">
<figcaption class="img-cap">微博 · 289K</figcaption>
@@ -379,7 +379,9 @@ layouts.md 使用的所有类(`h-hero` / `h-xl` / `h-sub` / `h-md` / `lead` /
**要点**
- 关键:每个 `frame-img` 必须写死 `height:NNvh`(不要用 `aspect-ratio`),否则网格会撑破
- 图片会自动 `object-fit:cover + object-position:top`,只裁底部
- **图注在框内**:`figcaption.img-cap` 会显示在固定高度框的内部底边(模板已处理弹性收缩),不额外占外部高度;有图注时图片实际展示高度 ≈ NNvh − 4vh
- 用 `.grid-3-3`3×2)或 `.grid-3`3×1)承载
- 3×2 双行 + 图注时,`height:26vh` 是不压 foot 的上限;标题更长或加说明行时降到 `22vh`
---
@@ -4,9 +4,7 @@
## Golden Source
原始参考文件:
`/Users/guohao/Documents/op7418的仓库/项目/Thin-Harness-Fat-Skills/ppt/index.html`
版式基准是仓库内的 `assets/template-swiss.html`(由作者原始参考 PPT 派生;原始文件不随仓库分发,本文件登记的 S01-S22 即其版式快照)。
瑞士主题生成时,除用户明确要求实验版式外,只能从下面登记的 22 个版式中选择。新增首页/尾页可以使用 Skill 里的 IKB ASCII 版本,但正文页必须来自这 22 个版式。
@@ -1,5 +1,8 @@
#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
import { pathToFileURL } from 'node:url';
const file = process.argv[2];
const allowExperimental = process.argv.includes('--allow-experimental');
@@ -14,6 +17,31 @@ const htmlForSlides = html.replace(/<!--[\s\S]*?-->/g, '');
const errors = [];
const warnings = [];
function overflowFix(px) {
const n = Math.round(px);
if (n <= 40) return `only ${n}px over: nudge content up or tighten one gap/padding by 20-40px; do not delete content`;
if (n <= 90) return `${n}px over: compact local gaps/padding and reduce one block height; avoid cutting copy`;
if (n <= 160) return `${n}px over: reduce a display title slightly or compress one paragraph before deleting content`;
return `${n}px over: switch to a higher-capacity layout or remove/merge content intentionally`;
}
async function loadPlaywright() {
const candidates = [
createRequire(import.meta.url),
createRequire(pathToFileURL(path.join(process.cwd(), 'package.json')).href),
];
for (const req of candidates) {
try {
const resolved = req.resolve('playwright');
const mod = await import(pathToFileURL(resolved).href);
return mod.default || mod;
} catch {
// Try the next resolution root.
}
}
return null;
}
const allowedLayouts = new Set([
'SWISS-COVER-ASCII',
'SWISS-CLOSING-ASCII',
@@ -96,6 +124,179 @@ slides.forEach((slide) => {
}
});
async function runRenderedMeasurements() {
const playwright = await loadPlaywright();
if (!playwright?.chromium) {
warnings.push('Rendered measurement skipped: Playwright is not resolvable from the skill folder or current project. Static Swiss checks still ran.');
return;
}
const browser = await playwright.chromium.launch({
args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'],
});
try {
const ctx = await browser.newContext({
viewport: { width: 1600, height: 900 },
deviceScaleFactor: 1,
});
const page = await ctx.newPage();
await page.goto(pathToFileURL(path.resolve(file)).href, { waitUntil: 'domcontentloaded' });
await Promise.race([
page.evaluate(() => document.fonts && document.fonts.ready),
page.waitForTimeout(1800),
]);
await page.waitForTimeout(800);
const measures = await page.$$eval('section.slide', (els) => {
const TRANSPARENT = /rgba?\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0?\s*\)|transparent/;
const titleSelector = [
'.h-hero', '.h-hero-zh', '.h-xl', '.h-xl-zh', '.h-statement',
'.display', '.display-zh', '.h1-zh', '.h2-zh', '.h-md',
'.step-title', 'h1', 'h2', 'h3',
].join(',');
const hasDirectText = (n) => {
for (const c of n.childNodes) {
if (c.nodeType === 3 && c.textContent.trim().length > 0) return true;
}
return false;
};
const colorVisible = (c) => c && !TRANSPARENT.test(c);
const labelFor = (n) => {
const cls = n.className ? '.' + String(n.className).split(' ').filter(Boolean).join('.') : n.tagName.toLowerCase();
const text = n.textContent.trim().replace(/\s+/g, ' ').slice(0, 40);
return text ? `${cls} "${text}"` : cls;
};
const isMeaningful = (el, n) => {
const er = el.getBoundingClientRect();
const posterArea = er.width * er.height;
const tag = n.tagName;
const cs = getComputedStyle(n);
const r = n.getBoundingClientRect();
if (r.width < 6 || r.height < 6) return false;
if (n === el || n.classList.contains('canvas-card')) return false;
if (cs.position === 'fixed') return false;
if (cs.position === 'absolute' && r.width * r.height >= posterArea * 0.82) return false;
if (n.matches('canvas.ascii-bg, canvas.mag-bg, .grain, .dot-mat, .ring-mat, .cross-mat')) return false;
const isText = hasDirectText(n);
const isMedia = tag === 'IMG' || tag === 'CANVAS' || tag === 'SVG';
const isRule = tag === 'HR' || (r.height <= 4 && (
parseFloat(cs.borderTopWidth) >= 1 ||
parseFloat(cs.borderBottomWidth) >= 1 ||
colorVisible(cs.backgroundColor)
));
const hasFill = colorVisible(cs.backgroundColor) && r.width * r.height >= 1600 && !['MAIN', 'SECTION'].includes(tag);
const hasBorder = (parseFloat(cs.borderTopWidth) + parseFloat(cs.borderBottomWidth) +
parseFloat(cs.borderLeftWidth) + parseFloat(cs.borderRightWidth)) >= 1 && r.width * r.height >= 1600;
return isText || isMedia || isRule || hasFill || hasBorder;
};
const titleGapChecks = (el, nodes) => {
const titles = Array.from(el.querySelectorAll(titleSelector)).filter((n) => n.textContent.trim());
const out = [];
for (const title of titles) {
const tr = title.getBoundingClientRect();
const isLocal = title.matches('.h-md, .step-title, h3');
const minGap = isLocal ? 14 : 32;
let nearest = null;
let nearestGap = Infinity;
for (const node of nodes) {
if (node === title || title.contains(node) || node.contains(title)) continue;
const nr = node.getBoundingClientRect();
const gap = nr.top - tr.bottom;
if (gap < -2) continue;
const overlap = Math.max(0, Math.min(tr.right, nr.right) - Math.max(tr.left, nr.left));
const overlapRatio = overlap / Math.min(tr.width, nr.width);
if (overlapRatio < 0.12 && gap < 96) continue;
if (gap < nearestGap) {
nearestGap = gap;
nearest = node;
}
}
if (nearest && nearestGap < minGap) {
out.push({ title: labelFor(title), next: labelFor(nearest), gap: Math.round(nearestGap), minGap });
}
}
return out;
};
return els.map((el, index) => {
const er = el.getBoundingClientRect();
const H = el.clientHeight;
const W = el.clientWidth;
const nodes = Array.from(el.querySelectorAll('*')).filter((n) => isMeaningful(el, n));
let top = Infinity;
let bottom = -Infinity;
let topNode = null;
let bottomNode = null;
for (const n of nodes) {
const r = n.getBoundingClientRect();
const itemTop = r.top - er.top;
const itemBottom = r.bottom - er.top;
if (itemTop < top) {
top = itemTop;
topNode = n;
}
if (itemBottom > bottom) {
bottom = itemBottom;
bottomNode = n;
}
}
if (!nodes.length) {
top = 0;
bottom = 0;
}
const safeBottom = Math.round(H * 0.93);
return {
idx: index + 1,
id: el.id || '',
layout: el.dataset.layout || '',
width: W,
height: H,
scrollOverflow: Math.max(0, Math.round(el.scrollHeight - H)),
visual: {
top: Math.round(top),
bottom: Math.round(bottom),
activeRatio: H ? (bottom - top) / H : 0,
bottomGap: Math.max(0, Math.round(H - bottom)),
topOverflow: Math.max(0, Math.round(-top)),
bottomOverflow: Math.max(0, Math.round(bottom - H)),
bottomNode: bottomNode ? labelFor(bottomNode) : '',
topNode: topNode ? labelFor(topNode) : '',
safeBottom,
},
titleGaps: titleGapChecks(el, nodes),
};
});
});
for (const m of measures) {
const prefix = `Slide ${m.idx}${m.layout ? ` (${m.layout})` : ''}`;
if (m.scrollOverflow > 4) {
errors.push(`${prefix}: M1 DOM overflow ${m.scrollOverflow}px. ${overflowFix(m.scrollOverflow)}.`);
}
if (m.visual.bottomOverflow > 4) {
errors.push(`${prefix}: M1 visual bottom overflow ${m.visual.bottomOverflow}px; lowest element is ${m.visual.bottomNode}. ${overflowFix(m.visual.bottomOverflow)}.`);
}
if (m.visual.topOverflow > 4) {
errors.push(`${prefix}: M1 visual top overflow ${m.visual.topOverflow}px; highest element is ${m.visual.topNode}. Move content down by the measured overflow plus 16-24px.`);
}
if (m.visual.bottom > m.visual.safeBottom) {
warnings.push(`${prefix}: M1 content reaches ${Math.round(m.visual.bottom)}px; nav-safe line is ${m.visual.safeBottom}px. Lift the lowest block or add .nav-safe-bottom.`);
}
if (m.visual.bottomGap > 170 && m.visual.activeRatio < 0.74) {
warnings.push(`${prefix}: M1 bottom whitespace ${m.visual.bottomGap}px; active content height ${Math.round(m.visual.activeRatio * 100)}%. Restore spacing/content instead of over-correcting overflow.`);
}
for (const gap of m.titleGaps) {
warnings.push(`${prefix}: M2 ${gap.title} has ${gap.gap}px gap before ${gap.next} (min ${gap.minGap}px).`);
}
}
} finally {
await browser.close();
}
}
await runRenderedMeasurements();
if (warnings.length) {
console.warn('Warnings:');
for (const warning of warnings) console.warn(`- ${warning}`);
@@ -3,5 +3,5 @@
"name": "playwright浏览器自动化操作",
"version": "20260605",
"keySource": "none",
"syncedAt": "2026-07-21T16:01:43Z"
"syncedAt": "2026-07-22T16:02:51Z"
}
@@ -2,8 +2,8 @@
"sourceId": "next-skills",
"repo": "https://github.com/vercel/next.js.git",
"ref": "canary",
"commit": "93c59f02f81cfdeb0fc397412897024ae7c68074",
"commit": "63f14c6c90c4dae819966e180491eb9b0af16792",
"adapter": "skill-collection",
"sourcePath": "skills",
"syncedAt": "2026-07-21T16:00:00Z"
"syncedAt": "2026-07-22T16:00:00Z"
}
@@ -11,11 +11,13 @@ Enable Cache Components on an app and walk it to a passing build. This skill seq
- **App Router project.** Cache Components is an App Router feature; `cacheComponents: true` does nothing for `pages/` routes. If the project has a `pages/` or `src/pages/` tree but no `app/` or `src/app/` tree, stop and tell the user — Pages → App migration is its own project, not part of this skill. A hybrid app (both `pages/` and `app/`) is fine: the flag affects the `app/` routes; `pages/` routes are unaffected and don't need opt-outs.
- **A runnable app.** The whole loop verifies against `next dev` and a browser, so the app has to boot. If it reads a database or required env at import (e.g. an `env.ts` that throws on a missing `DATABASE_URL`), confirm it actually starts — with the real environment, or local data you stand up — before step 1. Adoption can't be verified against an app that won't run.
- **Next.js 16.3 or later.** That release is where the pieces this skill relies on land: top-level `cacheComponents`, `export const instant`, the dev-overlay instant-navigation validation warnings, and the `cache-components-instant-false` codemod. If `next --version` reports below 16.3, upgrade first:
- `npx @next/codemod@latest upgrade latest` to apply the version-to-version codemods.
- Read the relevant [version upgrade guide](https://nextjs.org/docs/app/guides/upgrading) (e.g. [Version 16](https://nextjs.org/docs/app/guides/upgrading/version-16)) for what the codemod doesn't cover.
- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these.
- **No incompatible config keys.** `cacheComponents: true` errors on any file that still exports `dynamic`, `revalidate`, or `fetchCache`. **Translate, don't delete.** Each export encodes behavior the route needs to keep doing; migrate each one to its Cache Components equivalent via the [migration guide's per-key sections](https://nextjs.org/docs/app/guides/migrating-to-cache-components#enable-cache-components). The exception is `dynamic = 'force-dynamic'`: under Cache Components every route is already dynamic by default, so the migration guide removes it outright rather than translating it — don't overthink a batch of identical `force-dynamic` deletions. `revalidate` and `fetchCache` still need real translation. If a value can't be cleanly translated yet, leave a `// TODO: Cache Components adoption — restore revalidate = 3600` comment so the loop picks it up. The `cache-components-instant-false` codemod does not touch these.
- **`experimental.dynamicIO` is fatal.** It was renamed to top-level `cacheComponents` and the old key now aborts before any build can run — remove it (or replace with `cacheComponents: true`) first. `experimental.useCache` is still accepted as a deprecated alias; redundant once `cacheComponents: true` is set, so remove it for clarity.
@@ -36,7 +38,7 @@ The choice in step 1 is whether to opt every route out of validation first or fi
- **With a quiet pre-step (Incremental).** Run the codemod to opt every page and layout out of validation. Once you've also fixed what the codemod can't (sync-IO calls, leftover `revalidate`/`dynamic`/`fetchCache` exports), the build passes; you ship that as its own PR and then start the loop — removing one opt-out at a time and adopting that route. This splits the work into small, reviewable PRs.
- **Without (Direct).** Enable `cacheComponents` and start the loop on whatever the build flags first. Same loop, but every fix sits on one branch until adoption is complete.
In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature. Expect to spend most of the time in the loop, not in the pre-step.
In both, the per-route success bar is the same: **dev loop reports no errors AND `next build` passes**. Check in with the user after every feature, and suggest a commit but never make one without their confirmation. Expect to spend most of the time in the loop, not in the pre-step.
## background
@@ -73,7 +75,7 @@ In preference order:
npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop
```
The skill requires `agent-browser >= 0.27.0` and walks you through it.
The skill states its required `agent-browser` version and walks you through it.
**Requires Turbopack.** If `package.json`'s `dev` script passes `--webpack`, flag it to the user and ask whether there's a reason to stay on webpack. If not, switch to Turbopack (the Next.js 16.3+ default). If they want to keep webpack, skip this install and use the [build-only loop](#the-loop-build-only-fallback) instead.
@@ -83,7 +85,7 @@ In preference order:
3. **Build-only.** If you can't run a dev server at all, the build is your only signal. `○ (Static)` routes with no `<Suspense>` are fully verified by the build (nothing streamed to test). `◐ (Partial Prerender)` routes are only shell-verified — flag them when you report back.
4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or commit the milestone you've reached and hand off.
4. **No tooling at all.** Ask the user to run the dev server (or build) and report what they see, or hand off the milestone you've reached.
## step 1: choose a strategy
@@ -134,7 +136,7 @@ Confirm the pre-step with `next build`. The build is the proof, not the codemod
After the build passes, confirm the root layout got an opt-out (`grep -n "export const instant" app/layout.*`). The root layout renders every route, including framework routes like `/_not-found`, so if it was missed, add `export const instant = false` to it by hand.
Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them) and rarely block on their own; when a client route blocks, fix the server-side data in its ancestor layout.
Synthetic routes like `/_not-found` have no user file — when they block, fix the root layout's opt-out, not the synthetic route. Client Components (`"use client"`) get no opt-out (it's a build error — `E1344` — to export `instant` from them), but they are not a rare blocker. The high-frequency case is a client component in the root layout's nav or header calling `usePathname()`/`useSearchParams()`: it blocks _every_ dynamic route with `blocking-prerender-client-hook`, and static routes pass (the pathname is known at prerender), which masks it until you reach a dynamic segment. It's not an ancestor-data fix — follow the [error's docs page](https://nextjs.org/docs/messages/blocking-prerender-client-hook) for the `<Suspense>` recipe. Only when a client route blocks on _server_ data do you fix that data in its ancestor.
### end of the pre-step: check in
@@ -221,8 +223,7 @@ When the loop has run on every feature — every remaining `instant = false` sit
The work below is optional and lives in the docs — link the user to them and let them decide which to take on next. Don't walk these through inside this skill.
- [Instant navigation](https://nextjs.org/docs/app/guides/instant-navigation) — dev-only validation warnings the overlay raises on client navigation. Same shape as the blocking-prerender errors you cleared in step 2; the guide covers the per-warning details. Recommend it next if the user wants navigations to actually be instant (a passing build doesn't guarantee that — a `<Suspense>` above the shared layout caught the page-load case but doesn't cover client navigation).
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it audits `<Link prefetch={true}>` calls (driven by the dev overlay's `link-prefetch-partial` warning) with the flag off first, then flips the `partialPrefetching` config. It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prefetching](https://nextjs.org/docs/app/guides/prefetching) and [Runtime prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching) — broader prefetching reference. Runtime prefetching extends the static shell with per-session content; reach for it when a route's shell is too thin to be useful and Partial Prefetching alone doesn't cover the gap.
- [Sweep for more instant navigations](./references/dev-only-validations.md) — an optional follow-up once adoption is done, never required. A passing build is not the last word, because dev validates every route on each page load (simulating both page loads and client navigations) and catches what the build's first-error exit and descendant shadowing skipped. Offer it as the smaller path to instant navigation for a user who doesn't want to adopt Partial Prefetching. Adopting Partial Prefetching (below) runs the same kind of loop and meets these insights anyway, so recommend both and let the user pick which, or whether. The reference is the loop to execute.
- [`next-partial-prefetching-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-partial-prefetching-adoption) — the follow-up skill that adopts Partial Prefetching: it enables `partialPrefetching` and audits every `<Link prefetch={true}>` against a decision table (or adopts incrementally with the flag off, driven by the `link-prefetch-partial` insight). It sequences this the same way this skill sequences Cache Components, but the insights are dev-only, so it's a browser click-through, not a build loop. Recommended after instant navigation, since those fixes feed directly into how much of each route the shell can prefetch. Concepts live in the [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching).
- [Prevent regressions with e2e tests](https://nextjs.org/docs/app/guides/instant-navigation#prevent-regressions-with-e2e-tests) — the `@next/playwright` [`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation) helper asserts on the UI that's available immediately on navigation, so regressions surface in CI. Recommend it once a route is instant: `next-dev-loop` confirms it _now_; an `instant()` test keeps it that way.
- [`next-cache-components-optimizer`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-optimizer) — a separate skill that grows each route's static shell so more of the page prerenders and less streams in. Pure optimization, not part of adoption.
@@ -0,0 +1,29 @@
# dev-only validation sweep
How to surface and fix the instant-navigation insights that a clean `next build` does not show. The [Instant navigation guide](https://nextjs.org/docs/app/guides/instant-navigation) is the canonical reference for the validation model this exercises.
## what the build misses
By default (`validationLevel: 'warning'`) Cache Components validates every Page and Default segment in `next dev`, and the insights land in the dev overlay's Insights tab, not the build. Validation runs on every page load using the real request, and for each route it independently checks the initial page load and client navigations at different points in the hierarchy. So a `<Suspense>` boundary that covers the page load can still leave a client navigation blocking, and a layout stays clean at build time while a descendant keeps its `instant = false`. The build stops at the first blocking route and does not raise this family by default. Loading each route in dev is what surfaces it.
## when to run it
After the Cache Components build is clean, not before. While the app is mid-adoption the build redboxes mask this, so a full clean build (every route `◐`, no errors) is the precondition. A quiet sweep is the expected result of a clean adoption, not a missing signal.
## the loop
Reuse the [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) preflight (Turbopack), then add one job. On a webpack app, drive a browser directly with `agent-browser` or Playwright instead. You lose the `/_next/mcp` cross-checks, not the insights, which still show in the overlay and the dev log.
1. Build a route queue from the last build's route table or the `app/` tree.
2. Load each route in `next dev` with a browser. A refresh or a link click both work, and validation simulates both the page-load and client-navigation cases on that load, so you do not need to click through every link by hand. Dynamic params are checked against the real values you visit, so hit a concrete `[slug]`, not the pattern.
3. Watch the dev log and the Insights tab. The dev log is the greppable record, one `Error: Route "...": Next.js encountered ...` line per insight with its `docs/messages/<slug>` link, and it reads the same on Turbopack and webpack. The Insights tab is amber and appears only once an insight fires, so a route with no tab is clean. Through `next-dev-loop`'s `/_next/mcp`, these come from `get_errors` and the overlay, not `get_request_insights`, which is the performance recorder and reports nothing here.
4. Open the linked page for each distinct insight and apply its fix (usually pull a `<Suspense>` boundary down to the read). Reload to confirm it clears.
## gotchas
- The overlay renders inside a shadow root (`nextjs-portal`), so accessibility-tree snapshots miss it. Read it through `shadowRoot`.
- No browser, no sweep. There is no build-only fallback for this family. Apply the static fix you can from the docs page (gate on type-check) and hand off the live confirmation.
## when this shrinks
Build-time instant validation is opt-in today (`experimental.instantInsights.validationLevel: 'experimental-error'`); the default `'warning'` surfaces in the overlay only. Once the build raises these reliably, the sweep collapses into reading `next build` output and this reference can shrink to that.
@@ -10,7 +10,9 @@ If you're unsure which fix fits, the right call usually depends on what this par
## security gates and other code you can't infer
If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `<Suspense>` would change what the code guarantees — stop and ask the user before refactoring. The build error wants `<Suspense>`, but wrapping a gate in `<Suspense>` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, or move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy).
If the blocking code looks like it's there for a _reason you can't infer_ — a security gate at the page top (`await verifyAccess()`, an auth redirect, a feature-flag check) where moving it inside `<Suspense>` would change what the code guarantees — stop and ask the user before refactoring. The build error wants `<Suspense>`, but wrapping a gate in `<Suspense>` defeats the gate. Only the person who wrote it knows whether to keep the route blocking (`instant = false` as a documented Block), restructure the page so the gate runs differently, move the check to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy), or — if the gate duplicates protection the app already relies on elsewhere (platform auth such as Vercel Deployment Protection, a Proxy check, a Data Access Layer) — remove it.
Relocating a read (`await connection()`, Proxy) only changes _where_ it runs, never _whether it should run at all_ — so a gate that's redundant or broken looks identical to a correctly-placed one through the fix card's lens. If a gate looks strange, redundant, or out of place, say so plainly — "this looks like it might be unnecessary here — are you sure it belongs?" — instead of quietly relocating it. Surfacing the doubt is the agent's job; deciding is the user's.
If _every_ route under a layout is gated this way, a documented Block on the layout is the correct end state. Moving the gate to [Proxy](https://nextjs.org/docs/app/api-reference/file-conventions/proxy) is the architectural fix, not a Cache Components one, and that's a follow-up rather than something to hold the migration on.
@@ -5,124 +5,439 @@ description: "优化 Next.js Cache Components 页面静态壳和导航体验。"
# next-cache-components-optimizer
Two loops, shared levers and primitives, different diagnostics:
Set up an agentic optimization loop that drives a Next.js route from "not
instant" to "instant" and keeps it there. The loop is test-driven: encode the
goal as a failing `@next/playwright` `instant()` test, work it to green, and
ship the test as the regression guard. Run it once per target route. Work the
phases P → G in order; each ends in a gate. Fix recipes live in two lazily-read
references — `reference/patterns.md` (before→after for each blocker type) and
`reference/real-app-patterns.md` (parallel routes, auth gates, the empty-shell
and responsive-skeleton failure modes). Read one only when its phase points
there.
- **Page-render loop** ([ppr-loop.md](./ppr-loop.md)) — grow the static shell of a single page. Rank Suspense fallback areas on a shell-only render.
- **Nav loop** ([instant-nav-loop.md](./instant-nav-loop.md)) — when the user clicks a link from A to B, show B's static layout immediately (chrome, structure, content-shaped fallbacks) instead of holding A's UI until B's data resolves. Capture B's suspended boundaries post-`pushstate`, classify each by `suspended_by[].name`, drop SSR-only client hooks.
## What is invariant, and what is yours
Pick one and run it end-to-end.
One thing here is fixed. The rest is yours. Read this before treating any
command, platform, or env var below as a requirement.
## requires
- **Invariant: the verification loop.** Maximizing the shell is worthless
unless you can prove it. The proof is an automated check: under a lock that
gates dynamic data, the static shell still commits. RED shows the gap, GREEN
shows it closed, the test ships as the regression guard. It must run on a
production-like build and must not be able to pass vacuously. Stand the loop
up once; every later optimization is then verifiable by construction. The
loop is the deliverable, not any one route.
- **The mechanism: `@next/playwright` `instant()`.** This skill locks with
[`instant()`](https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config/instant#testing-instant-navigation):
a ruler, not a stopwatch (phase A). It comes from
`@next/playwright` (installed alongside `@playwright/test`, on the same
release line as `next`), so it isn't tied to any host. Keep it. Timing a
navigation by hand is too flaky to trust, and is the failure mode this skill
exists to prevent.
- **Yours: the rig.** How you build, deploy, authenticate, configure
Playwright, and loop belongs to your stack, not to this skill. A local
`next build && next start`, a CI/staging container, and a per-push preview
deploy are equally valid rigs; the verdict comes from the build, never the
platform. Phase 0 maps the invariant onto your repo. Read every platform
name, env-var spelling, and command below as an example to translate, not a
requirement.
- `next-dev-loop` initiated for this session — it opens the headed browser, exposes the `agent-browser` CLI, and wires the dev MCP server that provides `mcp get_logs`.
- `cacheComponents: true` in `next.config.ts`. Refuse otherwise.
## Two navigations, two loading states
## preflight (shared)
A route reaches the user two ways, and both must be instant:
1. Confirm `cacheComponents: true`.
2. **The user must already be at the page each loop needs** in the headed browser (from `next-dev-loop`) — logged in, with any state set up. This skill can't drive auth, SSO, or MFA; it takes the manual setup as the starting point. (Each sub-loop names which page it expects.)
3. `agent-browser get url` to anchor the current route.
- **Initial load (hard navigation)** commits the route's prerendered static
shell; deferred parts stream in behind their loading skeletons (Suspense
fallbacks, `loading.tsx`).
- **Client-side navigation (soft navigation)** commits the destination's
prefetched App Shell — the `<Link>` default under Partial Prefetching —
re-rendering only the segments that change.
Each loop sets the instant cookie as needed (see the shared `instant cookie` section below).
The fix patterns are identical for both; the test differs only in how the
navigation is driven ("Driving the navigation in tests" below). The two shells
can differ; guard the one you ship, both when both matter
(`reference/real-app-patterns.md`).
## instant cookie (shared)
## Goal
Both loops use the `next-instant-navigation-testing` cookie to freeze the framework's dynamic-data writes. Once set, visible content on the page is the static shell + Suspense fallbacks — that's what we capture to assess the optimization.
Maximizing the static shell is the optimization objective: the most meaningful
prerendered content commits immediately, and only genuinely per-request data
streams in afterward. The shipped test deterministically encodes **present ∧
instant**; **non-blank** is the additional bar the workflow enforces by
judgment (D1/D2/E), because an `instant()` pass alone is satisfied by a blank
`fallback={null}` shell (the empty-shell failure mode,
`reference/real-app-patterns.md`).
Set it with a pending-lock tuple `[0, "<unique-id>"]`. The id is any unique string; the convention is a `p`-prefixed random stamp so concurrent scopes don't collide:
`instant()` is a ruler, not a stopwatch: assert that the shell appears under
the lock; do not time it. A trustworthy verdict requires a production build
(phase A).
The GREEN under the lock is the deterministic verdict; each gate keeps it
trustworthy.
## Reporting to the user
This loop is meant to run unattended — ideally across many navigations in one
pass — so it doesn't stop to ask after each route. What matters is how you word
and present the results, not how often you interrupt. The mechanics below — the
rig, RED, GREEN, the gates — are your scaffolding; the user never needs to hear
those words.
- **Speak their language.** Describe the gap and the result in terms of what the
user sees: "navigating to the dashboard waited on the charts query before
anything painted; now the layout and skeletons paint instantly and the charts
stream in" — not RED/GREEN, the lock, or the phase letters.
- **Show, don't tell.** When you report a route, drive the browser (or attach
before/after screenshots) so the user watches the shell commit immediately and
the data stream in, rather than reading a claim. Identical before and after
means the fix did nothing — roll it back.
- **Present a run as a list of results,** one line per navigation — which route,
what's now instant, what streams — not a transcript of the loop.
- **Only surface a question for a genuine fork:** a fix that would change
behavior, a security-sensitive read, or a route that's dynamic by design (a
runtime-prefetch candidate, not a shell to grow). A clean instant fix is not a
fork — keep going. With no one to ask (an unattended run), don't block: take
the safe default and note the assumption — for a cache-freshness choice,
defer the read behind `<Suspense>` (always fresh, still instant) rather than
guess a `cacheLife`.
## The workflow
```
agent-browser cookies set next-instant-navigation-testing '[0,"p<random>"]' \
--url <origin>
- [ ] P PREREQS Next.js 16.3+ with cacheComponents: true; upgrade first → below
- [ ] 0 SETUP once per repo: discover + write instant-nav.rig.md → rig-template.md
- [ ] A RIG production build with the testing API exposed → below
- [ ] B BASELINE unlocked: the marker renders for the test user → test-template.md
- [ ] C RED locked instant(): the shell does not commit → test-template.md
- [ ] C-gate VERIFY-RED: stop until the RED is trustworthy → reference/red-test-robustness.md
- [ ] D FIX push each Suspense boundary down to the data it guards → reference/patterns.md
- [ ] D1 reuse the route's existing loading UI; do not hand-build skeletons
- [ ] D2 the shell matches the real render at every breakpoint → reference/real-app-patterns.md
- [ ] E PARITY the refactor changed only whether the route is instant
- [ ] F DIFFERENTIAL revert only the fix → RED; re-apply → GREEN → reference/red-test-robustness.md
- [ ] G REVIEW PR checklist (below)
```
Each loop's preflight specifies when to set it within the flow. Clear it at the end (see `teardown` below).
## decide which loop
- **Page-render** when the complaint is about one route's initial load. Read [ppr-loop.md](./ppr-loop.md).
- **Nav** when it's about navigating between two routes. Read [instant-nav-loop.md](./instant-nav-loop.md).
Ambiguous → ask.
## shared refactor levers
- **Push down** — extract I/O into a Suspense-wrapped child so the parent stays static and static siblings lift into the shell.
- **Recurse, don't blind-wrap.** If a Suspense boundary already wraps a component containing both static content and the I/O, read inside, extract the I/O-dependent JSX into a new leaf, and lift the static siblings up.
- **Cache**`'use cache'` + `cacheLife(<profile>)`. Always ask the user for freshness; map to a preset (`seconds` / `minutes` / `hours` / `days` / `weeks` / `max` / `default`).
Push-down and cache compose: push-down lifts static structure, cache eliminates the remaining data gap.
## propose via plan mode (shared)
Each refactor goes through plan mode before applying. Treat this as a signal: the application work is non-trivial agentic engineering, not a templated edit. This skill provides the framework — which lever to reach for, which candidate to fix, what the expected visible delta is — but the real work (which file to edit, how to cleanly extract the I/O, where to place the new Suspense boundary, which `cacheLife` profile to ask the user for) is a judgment call you have to think through. Plan mode forces a coherent proposal before touching code, and gives the user a chance to redirect on any of those decisions.
## no-shell bailout (shared)
The levers presume a shell exists to grow or cache toward. If the route is fully blocking — HTTP 500 with `blocking-route` or `NEXT_STATIC_GEN_BAILOUT` in `mcp get_logs`, or zero Suspense boundaries on a visibly-rendered page — there's no shell. Surface the structural blocker and stop; the user has to wrap the offending dynamic access in `<Suspense>` before either loop can help.
## verify requires a visible delta (shared)
Each loop captures a baseline screenshot of the shell before applying any change, then re-screenshots after. Report both paths in the final summary so the user can see what changed. The two captures must visibly differ — fallback area shrunk, content promoted to the static surface, target fallback gone or content-shaped. Identical-looking captures mean the refactor didn't land; undo. "Compiles cleanly" is not the bar.
**Hide the dev overlay before each screenshot.** The Next.js dev overlay (`<nextjs-portal>` at the document root) renders instant-nav guidance, build errors, and other dev chrome that pollute the before/after comparison. Hide it, screenshot, restore:
```
agent-browser eval "document.querySelector('nextjs-portal').style.display='none'"
agent-browser screenshot <path>
agent-browser eval "document.querySelector('nextjs-portal').style.display=''"
```
## anti-patterns (shared)
**Don't replace granular Suspense boundaries with a top-level loading skeleton.** A `loading.tsx` for the whole segment, or a root-level `<Suspense fallback={<Skeleton />}>` (or worse, `fallback={null}` that blanks the UI), defeats this skill's optimization — which is to extract real static chrome above each granular boundary and use content-shaped fallbacks per region. A coarse "the page is loading" stand-in bypasses the work entirely.
## gotchas (shared)
- Dev doesn't prefetch the way production does, and routes compile on first hit — so after a navigation or reload, the DOM keeps updating for noticeably longer than the eventual production experience. Wait patiently for the DOM to stabilize before capturing the React tree or taking a screenshot — e.g., poll `document.documentElement.innerHTML.length` until it's unchanged across two consecutive reads. A fixed short delay risks sampling mid-render.
- Don't try to verify nav prefetch by inspecting dev network traffic — dev doesn't fire prefetch requests at all, so the network tab, manual `router.prefetch()` calls, and `<Link prefetch={true}>` will all look broken regardless of whether your code is correct. The cookie-locked SPA-nav recipe in [instant-nav-loop.md](./instant-nav-loop.md) under `verify` is already the canonical recipe for this — it simulates what production would prerender into the prefetched RSC without requiring prefetch to actually fire. Use it; don't invent a network-tab alternative.
- The diagnose pipeline can be flaky — DevTools attachment timing, DOM-settle races, and dev compilation effects can each produce inconsistent captures from one run to the next. When a result feels off (a candidate appears that you don't expect, or one you expect doesn't), re-run the diagnose 23 times and cross-check; boundaries that appear consistently are real, one-off appearances are noise.
## reference (shared primitives)
```
agent-browser react suspense add --only-dynamic to filter
--json server-side to actually-
suspended boundaries. Each
entry has jsx_source +
suspended_by[] with raw blocker
names (usePathname, cookies,
fetch, cache, ...); classify by
name for per-loop rules
POST /__nextjs_original-stack-frames body { frames: StackFrame[],
isServer, isEdgeServer,
isAppDirectory }; returns one
result per frame with
file:line:column
mcp get_logs dev MCP tool from
next-dev-loop; surfaces
blocking-route /
NEXT_STATIC_GEN_BAILOUT 500s
cacheLife('<profile>') default | seconds | minutes
| hours | days | weeks | max
```
Per-loop primitives in [instant-nav-loop.md](./instant-nav-loop.md).
## teardown (shared)
Delete the cookie by name — overwrite with an expired stamp:
```
agent-browser cookies set next-instant-navigation-testing x \
--url <origin> --expires 1
```
Never `agent-browser cookies clear` (no args) — wipes auth.
Phases B and C build the test; only the locked test from C ships.
---
Sibling of `next-dev-loop` — initiate that first.
## P. PREREQUISITES: current Next.js with Cache Components
The workflow depends on framework capabilities that ship with current Next.js:
- **Next.js 16.3+ with `cacheComponents: true`** in `next.config.ts`. Without
Cache Components there is no static shell to optimize.
- **`@next/playwright`** on the same release line as the project's `next`; it
provides `instant()`. Verify with `npm ls next @next/playwright` (or the
project's package manager) and align them if they differ. The matching
testing API is in the `next` runtime, gated by the
`experimental.exposeTestingApiInProductionBuild` config flag (phase A).
If the project does not meet these, upgrade first (`npx @next/codemod upgrade`
automates most of it), then enable Cache Components in `next.config.ts`:
```ts
export default { cacheComponents: true }
```
Enabling the flag surfaces the blocking routes to resolve first; the
[`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption)
skill drives that adoption. Reach for this optimizer once the app builds under
Cache Components.
This gate is deliberate: the skill targets current Next.js, and none of the
verdicts below are meaningful on older versions.
## 0. SETUP: discover this project's rig, once per repo
The principles in this skill are fixed; the infrastructure they run on is
yours. On first use in a repository, discover how the project builds, deploys,
authenticates, and tests (inspect the repository first, and ask the user only
what it cannot answer), then write the answers to a committed
`instant-nav.rig.md`. Every later run reads that file instead of
rediscovering. The six questions (BUILD / EXPOSE / RUN / TEST USER / DRIFT /
LOOP), the file template, and filled examples (local-only, generic CI +
container, preview deploy) are in **`rig-template.md`**.
If the repo has no Playwright e2e harness yet, standing up a minimal one
(`@next/playwright`, a config with `baseURL`, one authenticated path) is part
of this step; the loop does not assume a pre-existing suite.
## A. RIG: a production build with the testing API exposed
Stand up the rig described by `instant-nav.rig.md`. Two invariants hold on
every platform:
1. **Never measure on `next dev`.** It does not prefetch, and its lock is
unreliable for blocking routes, so a dev `instant()` result is not a valid
RED or GREEN.
2. **The measured build must expose the testing API.** Otherwise `instant()`
silently no-ops and the test passes vacuously (see
`reference/red-test-robustness.md`). The lock-engagement proof is the phase-C
RED itself: the unfixed target route is the known-blocking route, and its
RED under the lock shows the lock engages on this build (C-gate); the
self-validating variant in `test-template.md` is the in-band guarantee. Wire
`experimental.exposeTestingApiInProductionBuild` to a condition that is
true for every build you measure and never true in production:
```ts
experimental: {
// Use the condition your platform provides, and record it in the rig file:
// local: an explicit opt-in, as below
// generic CI: process.env.DEPLOY_ENV === 'staging'
// Vercel: process.env.VERCEL_ENV === 'preview'
exposeTestingApiInProductionBuild:
process.env.EXPOSE_TESTING_API === '1',
}
```
The rig is any production-like build that exposes the testing API: a local
`next build && next start`, a CI/staging container, and a preview deploy are
all equally valid; the verdict comes from the build, not the platform. See
`rig-template.md` for filled examples.
For any deployed or remote build, poll the rig's LIVENESS probe to confirm the
artifact contains `HEAD` before trusting a verdict (a stale deploy reads as a
false RED or GREEN); a local `next build && next start` needs none. The probe
mechanism is in `rig-template.md` (question 6).
## B. BASELINE (unlocked): development scaffold, do not ship
Drive the real navigation with no `instant()` lock and assert that the
destination's `SHELL_MARKER` renders **as the test user**: the account the
e2e suite authenticates as (in CI, the CI account; locally, your e2e login
fixture), with its flags, plan, role, and data. This establishes that the
marker is real and reachable: not flag-gated, not redirected away, not a
guessed selector. The suite runs as the test account, not the author's session;
that environment drift (the rig DRIFT list) is a common source of
untrustworthy REDs. Scaffold and run command: **`test-template.md`**.
**Delete this baseline before the PR.**
## C. RED (locked) + the VERIFY-RED gate
Wrap the same navigation in `instant()`; assert the shell commits under the
lock. A RED here is the gap. **This is the test that ships**
(`test-template.md`).
> **C-gate: do not start optimizing until the RED is verified trustworthy.** A
> RED that is red for the wrong reason sends you optimizing a route that was
> never broken.
The question that settles it: **does `SHELL_MARKER` render without the lock,
as the test user?** Answer it by re-running phase B as the test user, not by
adding assertions to the shipped test. The two-branch resolution (No → marker
or environment bug; Yes → genuine gap, proceed to D), the full taxonomy of
untrustworthy REDs, the checklist, and worked cases are in
**`reference/red-test-robustness.md`**. Read it now.
---
## D. FIX: push each boundary down to the data it guards
**The anti-pattern: one coarse boundary.** A single `<Suspense>` high in the
tree with a page-level fallback has three costs:
- The layout UI stays out of the static shell: only a throwaway copy of it is
prerendered.
- The entire subtree is replaced when the boundary resolves, which discards
client state and shifts layout.
- The hand-built fallback drifts out of sync as the UI changes, because it
duplicates structure that also exists in the resolved tree.
**The fix: hoist the static, push the Suspense down.** Render the layout UI
once, synchronously, in the shell, and wrap each await in a boundary scoped to
the single read it guards. Only that leaf streams; the stable ancestors are
reused as-is.
**Rule:** if an element renders in both the fallback and the resolved tree,
hoist it above the boundary.
### The most common blocker: a top-level `await` in a layout on a fallback route
```
app/[locale]/(app)/[tenant]/dashboard/...
│ generateStaticParams ✅ │ no generateStaticParams → fallback route
```
When any dynamic segment in the route lacks `generateStaticParams`, the route
is a fallback route, and **all** params defer to request time, including the
enumerated ones. A top-level `await` in a layout (`await params`, a
request-time session read, an auth gate) then blocks the whole subtree out of
the static shell, even when it reads a statically known param. Minimal shape: a
dynamic-segment route with one segment lacking `generateStaticParams`, plus a
top-level `await` in the layout above it.
### The fix: defer the gate, render children
Render `children` unconditionally; move the top-level `await` into a
`<Suspense fallback={null}>`-wrapped child. Mechanism and before→after:
`reference/real-app-patterns.md`, "Deferring an auth gate".
**Fix the page below the shell too, not only the layout.** A page-level
top-level `await` (commonly `await params`) blocks the same way the layout's
does, so make the page sync and push its dynamic reads into a
`<Suspense>`-wrapped leaf as well. `fallback={null}` is correct only when a gate renders nothing on
success; for data, the fallback must be a real loading skeleton (see D1).
Every other blocker shape — `cookies()`/`headers()`, uncached fetch or database
reads, `searchParams`, metadata, viewport, non-deterministic values (`Date.now()`,
`Math.random()`, `crypto.randomUUID()`) — surfaces its own insight when you hit
it: the build prints a `https://nextjs.org/docs/messages/<slug>` link. The
default build output is often abbreviated and may carry no usable stack trace;
add `--debug-prerender` for the full failing frame and to report every blocker
past the first. Scope the build to the route you're on with
`next build --debug-build-paths "app/<route>/**"` rather than rebuilding the app.
Open that page and apply its recipe; don't improvise from the inline message.
The before→after recipe for each shape is in `reference/patterns.md`, which maps it to the insight
that explains it.
A few things those per-error pages don't stress for the instant-navigation goal:
- **A boundary in the root layout isn't enough for client navigations.** It
passes a page-load check but leaves sibling client navigations blocking; put
the boundary below the lowest layout the source and destination routes share.
- **Keep the LCP element** (usually the main heading) out of any boundary, so it
paints in the shell instead of waiting on a stream.
- **A green check isn't always instant.** `export const instant = false` opts
the segment out of validation while the navigation still blocks, and a
`<Suspense>` above the document `<body>` prerenders an empty shell — neither
makes the route instant.
### D1: reuse the route's existing loading UI; do not hand-build skeletons
Before writing any skeleton, search the repository for the loading UI that
already exists for this route, in order:
1. the route's `loading.tsx`;
2. an exported `*Skeleton` colocated with the component;
3. the fallback already inside the component's own `<Suspense>`.
The **divergence point** is the lowest layout shared by the source and
destination routes: a soft navigation re-renders only the segments below it,
while an initial load re-runs every layout from the root. (Also called the
shared boundary.) A `loading.tsx` above the divergence point fills only
the initial-load shell; it sits above the soft-nav re-render scope. A
`loading.tsx` at the destination segment is itself the in-tree boundary for a
soft navigation into that segment and serves both. Reuse whichever boundary
actually covers the navigation you are shipping; below the divergence point,
`loading.tsx` and colocated skeletons are interchangeable for that purpose.
If a component has no skeleton, extract its loading markup into a colocated
skeleton beside it. Do not author a fresh skeleton that mirrors the page
layout: it duplicates structure, drifts as the page changes, and pulls the
design back toward a single coarse boundary. Reusing the component's own
skeleton also keeps the prefetched shell consistent with the loaded UI.
Exception: if the deferred component renders `null` for some users (for
example, a flag-gated control), `fallback={null}` is correct, since a skeleton
would flash and then collapse.
### D2: the shell must match the real render at every breakpoint
A skeleton frozen to one breakpoint misaligns on the others. Fix it the same
way: one responsive component renders both the live UI and the shell (D1
skeleton in its data slots), so the breakpoint switch happens once. Verify by
re-asserting the shell marker at two widths
(`await page.setViewportSize({ width: 1280, height: 800 })`, then
`{ width: 390, height: 844 }`), or by adding a mobile Playwright project, so
this gate is as machine-checkable as the others. Detail:
`reference/real-app-patterns.md`.
> **D-gate: phase D is complete when the locked test from phase C passes GREEN
> under the lock on the production-build rig**, not when the code compiles. That
> GREEN is the deterministic stop for the fix loop; proceed to E.
**When the read can't be pushed down** (an ID minted per request, an
all-dynamic page, a per-request auth/scope read the whole subtree needs), there
is no shell to grow. Don't force one: opt the route into **runtime prefetching**
so the prefetch runs the dynamic render ahead of the click and the soft nav
commits the real content. See [Runtime Prefetching](https://nextjs.org/docs/app/guides/runtime-prefetching)
for the mechanism (`prefetch = 'allow-runtime'` on the route plus a full
`<Link prefetch={true}>`) and the [dynamic-data-during-prefetching insight](https://nextjs.org/docs/messages/instant-link-prefetch-partial)
for adoption. The `instant()`-specific gotchas the docs don't cover:
- **The full prefetch is mandatory.** An auto/PPR prefetch bails before the
runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` /
`kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's
still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but
nothing on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry
incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects can't be
prefetched — the prefetch receives the redirect, not the tree. Point the link
and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic
data; issuing it on hover for every link is wasteful. Scope `kind: 'full'` to
the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a
client component, so its text isn't in the prefetch response. Assert a
`data-testid` that renders when the client subtree commits.
## E. PARITY: the refactor changed only whether the route is instant
The push-down is a mechanical transform, not a redesign. Afterward the route
must render the same tree, data, ordering, empty and error states, redirects,
and interactions as before; the only observable difference is that the shell
now commits instantly. Verify:
- **Same render output.** The moved `await`s compute and return the same
values; after the stream, the route shows the same content as the base
branch for the test user.
- **Side effects still fire.** A deferred `redirect()` or `notFound()` still
happens, at request time rather than during prerender. Confirm an
unauthorized user is still redirected and a missing record still returns 404.
- **Both viewports reach the real UI** after the stream (D2).
- **Client state survives.** Because the layout UI is hoisted into the stable
shell rather than swapped on resolve, open menus, scroll position, focus,
and input state persist across the stream.
If anything other than whether the route is instant changed, reduce the refactor.
## F. DIFFERENTIAL
Revert only the fix → RED; re-apply → GREEN; link both runs
(`reference/red-test-robustness.md`). On a deployed rig, confirm each run is live
(LIVENESS, phase A) before trusting its color.
## G. REVIEW (PR checklist)
A green final state means nothing if the RED was never trustworthy. The
test-trustworthiness items are the robustness checklist
(`reference/red-test-robustness.md`); confirm them, then require these
PR-specific items:
- [ ] **Differential shown**: RED without the fix, GREEN with it, runs linked.
- [ ] **Parity confirmed (E)**: same content, redirects, and state.
- [ ] **Existing loading UI reused (D1)**: no new page-mirroring skeleton.
- [ ] **Shell matches the real render at desktop and mobile widths (D2)**.
**Stop condition for the whole workflow:** the locked test from C is GREEN on
the rig, the differential (F) holds, and every item above is checked. Until all
three hold, you are not done.
## Driving the navigation in tests
- **Soft navigation** → drive a real `<Link>` click. **Initial load** → use
`page.goto()` inside `instant()` with the `baseURL` option. Do not substitute
`goto` for a soft-nav verdict; the two shells can differ
(`test-template.md`, `reference/real-app-patterns.md`).
- With parallel routes, only the slots that change re-render on a soft
navigation; client-rendered navigation UI does not re-render at all. Do not
chase a slot the navigation never touches
(`reference/real-app-patterns.md`).
## Files
- `rig-template.md`: phase 0, the six-question rig discovery, the
`instant-nav.rig.md` template, and filled examples (local-only, generic CI,
preview deploy).
- `test-template.md`: the shipped `instant()` specs for both navigation
types (phase C), and the delete-before-PR baseline scaffold (phase B).
- `reference/red-test-robustness.md`: the C-gate and phase F. The taxonomy of
untrustworthy REDs, the checklist, the differential recipe, the vacuous-pass
failure mode, and worked cases.
- `reference/real-app-patterns.md`: parallel routes, deferring an auth gate,
initial-load vs soft-navigation shells, the empty-shell failure mode, the
responsive-skeleton mismatch, edge cases.
@@ -1,89 +0,0 @@
# instant-nav-loop (sub-reference of next-cache-components-optimizer)
In-app navigation optimization: when the user clicks a link from A to B, show B's static layout immediately — chrome, structure, content-shaped fallbacks — instead of holding A's UI until B's data resolves.
Strictly smaller than page-render — only segments newly mounted on B's path need a shell. The LCA layout stays mounted with its already-resolved data, so even a dynamic-root app can have instant in-app nav.
The hard part is **identifying real blockers**. After `pushstate`, capture B's suspended boundaries with `agent-browser react suspense --only-dynamic --json`. Each boundary carries `suspended_by[].name``usePathname` / `useSearchParams` / `useRouter` (client-hook), `cookies` / `headers` / `connection` (request-api), names containing `fetch` or `cache` (server-fetch / cache), etc.
**Client-hook blockers don't block instant nav.** They suspend only during SSR prerender; on a client nav (`pushstate`) they resolve instantly from the router store. Diagnose drops them as candidates (see step 4 for the operational set), so the loop doesn't recommend SSR-only refactors that have no effect on click-to-paint.
When the user navigates from A to B, the layouts they share stay mounted; only the segments past the point where the two routes diverge actually load. So B's suspended-boundary capture is naturally focused on the new work — you don't have to filter out the shared parts. With multiple real candidates, fix the one highest in the new-segments tree (closest to where A's and B's paths diverge) first.
Complementary to Next.js's Instant Insights, which checks Suspense _existence_ (structural). This loop checks fallback _quality_ (visual) — closing the `<Suspense fallback={null}>` loophole.
## preflight (in addition to shared)
The starting page is **A** — the route the user is navigating _from_. The shared preflight has already anchored A (the user is on it, logged in, with state set up). Now anchor B:
1. Ask the user to perform the navigation to B in the headed browser — click the link, button, or whatever leads there. Read `agent-browser get url` to confirm B's URL.
2. `agent-browser pushstate <A>` to return the browser to A. The diagnose loop starts from A.
Set the instant cookie (per shared `instant cookie` section) any time after the browser is on A but before calling `pushstate`. There's no race — the cookie only needs to be present at the moment of navigation. The cookie doesn't block the navigation; it gates the framework's dynamic-data writes, so B's React tree mounts normally but its dynamic Suspense boundaries stay in fallback until the cookie is cleared. (Setting the cookie before a direct load of A or B would freeze that page at its static shell — that's why we wait until the user is on A first.)
## loop
### diagnose
1. **Set the instant cookie and navigate via `pushstate <B>`.** Wait for the DOM to settle. B's tree is now mounted; its dynamic boundaries stay in fallback while the cookie holds.
2. **Check B for the no-shell bailout** per SKILL.md.
3. **Capture B's suspended set.** `agent-browser react suspense --only-dynamic --json` → boundaries with `suspended_by[]`.
4. **Filter to real nav candidates.** Drop boundaries whose `suspended_by` entries are **all** client-hook names (`usePathname`, `useSearchParams`, `useRouter`, `useSelectedLayoutSegment(s)`, `useParams`) — SSR-only, won't help nav. Keep boundaries with at least one request-api, server-fetch, or cache blocker.
5. **Gauge the gap.**
- No candidates → ask the user if the nav still feels slow. No → stop and offer to audit other A → B pairs. Yes → step 8 (unwrapped async).
- Candidates present → screenshot the locked B state (hide `nextjs-portal` per the shared rule), check rendered area. All sub-viewport → already in good shape; stop.
6. **Resolve sources** for each remaining candidate via `POST /__nextjs_original-stack-frames` on `suspended_by[].owner_stack` (or `jsx_source` if the stack is empty).
7. **Pick the highest candidate.** With multiple candidates, fix the one highest in B's new-segments tree — closest to where A's and B's paths diverge. If that candidate wraps the others, recurse: read inside the wrapper to find the I/O that's actually blocking.
8. **Fallback: unwrapped async.** Reached from step 5 when there are no `react suspense` candidates but the nav still feels slow. The blocker has no `<Suspense>` so it doesn't appear in the capture. Direct-load B (full page navigation, not pushstate; cookie still set); `mcp get_logs` surfaces a `blocking-route` 500 naming the unwrapped I/O. Filter to sources past the point where A's and B's paths diverge; bailouts in shared layouts above are PPR concerns, not nav.
### decide / apply
Apply the shared lever rules from SKILL.md; push-down recipes work at layouts too.
**Nav-only third lever: private cache + runtime prefetch.** For I/O that reads `cookies()` / `headers()` / `searchParams`, shared `'use cache'` won't help — those reads bail to dynamic. Use `'use cache: private'` + `cacheLife({ stale: N })` on **the scope that encloses the request-API read** (see scope rule below), plus `prefetch = 'allow-runtime'` as a route segment config (page or layout export) on the segment that owns the private content. Private-cache results live only in the browser — **never stored on the server** — so allowing runtime prefetching lets Next.js resolve them at link-visibility time with the user's session; the click commits with cookie-derived data already in place.
**Scope rule when cookie-read and data-fetch live in different frames.** The directive's semantics are about the cache scope enclosing the request-API call, not "the I/O function" as a label. If a page reads `cookies()` and passes the value into a separate fetch helper, putting `'use cache: private'` on the helper alone leaves the cookie read outside any cache scope and the segment stays dynamic. Either move the directive up to the frame that reads cookies, or move the cookie read down into the helper. Compiles and typechecks either way — only runtime behavior tells you which is correct.
The `prefetch` flag applies to the segment plus every descendant — put it on the most ancestral segment with runtime-cacheable content (layout-level covers all child pages; page-level covers only that page). Server Component only (not allowed with `"use client"`); requires `cacheComponents: true`. Valid values: `'auto'` (default) / `'force-disabled'` / `'force-static'` / `'allow-runtime'`. It doesn't make any segment cacheable on its own (each still needs `'use cache: private'`), doesn't help cold loads (no `<Link>` to prefetch from), and doesn't override `await connection()`.
### verify
**Cookie-locked SPA-nav screenshot** is the canonical visible delta — what production would have prefetched. Recipe (run once before applying, once after):
1. Be on A. Set the instant cookie.
2. `agent-browser pushstate <B>` — a real client navigation.
3. Wait for the DOM to settle. With the cookie set, the framework gates dynamic-data writes (see preflight), so the captured state is the static shell + Suspense fallbacks.
4. Hide `nextjs-portal`, screenshot, restore (per the shared rule).
Compare the two screenshots per the shared visible-delta rule. The after-shot must visibly differ — more static content promoted, content-shaped fallbacks, or (for the private + runtime-prefetch lever) cookie-derived data resolved.
**Identical before/after is two distinct signals, not one.** Either the lever didn't apply (code is wrong) or no I/O resolved either time (environment is broken — DB unreachable, stale TLS sockets, etc.). Before iterating on the code shape, check `mcp get_logs` for socket timeouts or fetch failures in either capture window. If the data path didn't complete in either run, the comparison is inconclusive — fix the environment and re-capture.
Also:
1. **Re-run the diagnose capture.** Repeat steps 14 on the new code. The target candidate should be absent from B's suspended set (the blocker is gone), or its fallback should now be content-shaped.
2. Re-check B for the no-shell bailout.
## reference (loop-specific)
```
agent-browser pushstate <url> client-side navigation (no
HTTP request, no full reload)
'use cache: private' per-session cache; cookies /
headers / searchParams reads ok
prefetch = 'allow-runtime' route-level: permits runtime
prefetching of private-cached
content when <Link> is visible
instant = false layout-level opt-out; escape
hatch, anti-pattern
```
@@ -1,31 +0,0 @@
# ppr-loop (sub-reference of next-cache-components-optimizer)
Page-render optimization: grow the static shell of a single `cacheComponents` page.
Rank candidates by visible pixel area; largest gap first.
## preflight (in addition to shared)
The page to optimize is whatever route the user is currently on (per shared preflight). Set the instant cookie (per shared `instant cookie` section), then reload.
## loop
### diagnose
1. **Check for the no-shell bailout** per SKILL.md.
2. **List candidates.** `agent-browser react suspense --only-dynamic --json` → each boundary has `jsx_source` (file:line:col) and `suspended_by[].name`. Resolve `jsx_source` (or `suspended_by[].owner_stack`) via `POST /__nextjs_original-stack-frames`.
3. **Rank by rendered area.** Per candidate, take max(fallback rect on shell-only, rendered subtree rect on full). Fallback rect alone misleads when developers used an undersized spinner.
4. **Gauge the gap.** Same capture as verify — the shell-only render. If the top-ranked candidate is sub-viewport (thin fallback bar, sidebar widget), the shell is already in good shape; surface that and offer to audit other routes for better targets, rather than forcing a marginal refactor.
5. **One boundary dominates →** that wrapper is the shell. Read inside, enumerate the awaits, recurse with those.
### decide / apply
Apply the shared lever rules from SKILL.md.
### verify
Re-take the shell-only render and compare against the baseline screenshot. The targeted gap must shrink or vanish; identical captures fail per the shared visible-delta rule. Re-check the no-shell bailout; a botched extract can break the shell.
@@ -0,0 +1,354 @@
# Refactor patterns — push dynamic down into the shell
Each pattern is **before → after**: keep as much as possible in the prerendered shell, and wrap only genuinely per-request work in a tight `<Suspense>` (or hoist it into `use cache`). Production shapes — parallel-route slots, deferring an auth gate, client slot-routers — are in `real-app-patterns.md`.
---
## 1. Awaiting at the top → move the await into a Suspense child
The most common blocking shape. Awaiting request-time data at the top of a page/layout makes **everything below it** dynamic.
```tsx
// ❌ before — top-level await of a non-static param + uncached data
export default async function Page(props: PageProps<'/store/[slug]'>) {
const { slug } = await props.params
const product = await db.products.findBySlug(slug)
return (
<article>
<h1>{product.name}</h1>
</article>
)
}
```
```tsx
// ✅ after — pass the params promise down; await inside a Suspense-wrapped child
import { Suspense } from 'react'
export default function Page(props: PageProps<'/store/[slug]'>) {
return (
<Suspense fallback={<p>Loading product…</p>}>
<Product params={props.params} />
</Suspense>
)
}
async function Product({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const product = await db.products.findBySlug(slug)
return (
<article>
<h1>{product.name}</h1>
</article>
)
}
```
Inline variant when you don't want a separate component — unwrap the promise without awaiting at the top:
```tsx
export default function Page(props: PageProps<'/store/[category]'>) {
return (
<Suspense fallback={<Grid.Skeleton />}>
{props.params.then(({ category }) => (
<ProductGrid category={category} />
))}
</Suspense>
)
}
```
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 2. `cookies()` / `headers()` in a layout → start, don't await; pass down
A layout that awaits request data blocks the layout **and every page under it**.
```tsx
// ❌ before — whole layout (and all children) becomes dynamic
export default async function Layout({ children }) {
const cookieStore = await cookies()
const theme = cookieStore.get('theme')?.value
return <body data-theme={theme}>{children}</body>
}
```
```tsx
// ✅ after — start the read without awaiting, pass the promise to a Suspense child
import { Suspense } from 'react'
import { cookies } from 'next/headers'
export default function Layout({ children }: { children: React.ReactNode }) {
const cookieStore = cookies() // not awaited → does not block the shell
return (
<body>
<nav>
<Suspense fallback={<UserMenu.Skeleton />}>
<UserMenu cookiePromise={cookieStore} />
</Suspense>
</nav>
{children}
</body>
)
}
async function UserMenu({
cookiePromise,
}: {
cookiePromise: ReturnType<typeof cookies>
}) {
const theme = (await cookiePromise).get('theme')?.value
return <div data-theme={theme}>…</div>
}
```
`{children}` and `<nav>` stay in the shell; only `<UserMenu>` streams.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 3. Uncached fetch / DB read → choose `use cache` _or_ `<Suspense>`
Decide per data source. Same-for-everyone & rarely-changing → cache it (it joins the shell). Per-request & must-be-fresh → leave it uncached behind a boundary.
```tsx
// ❌ before — both block the shell
const product = await db.products.findBySlug(slug) // rarely changes
const inventory = await db.inventory.findBySlug(slug) // must be fresh
```
```tsx
// ✅ after — cache the stable one (shell), defer the fresh one (streams)
async function getProduct(slug: string) {
'use cache' // → resolved at prerender, lands in the shell
return db.products.findBySlug(slug)
}
;<Suspense fallback={<p>Checking availability…</p>}>
<Inventory params={params} /> {/* uncached read stays here, streams in */}
</Suspense>
```
> A bare `'use cache'` applies the `default` `cacheLife` profile. Choose freshness explicitly with `cacheLife('<profile>')` (`default` / `seconds` / `minutes` / `hours` / `days` / `weeks` / `max`) rather than shipping the default lifetime by omission.
>
> Serverless note: `use cache` is in-memory and does not persist across instances — use [`use cache: remote`](https://nextjs.org/docs/app/api-reference/directives/use-cache-remote) for a durable shell.
**Insight:** [uncached data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-dynamic).
---
## 4. Dynamic params → `generateStaticParams` (shell) or `<Suspense>` (stream)
If the set of params is enumerable, prerender them so `await params` resolves into the shell. Otherwise treat params as request-time and wrap consumers in `<Suspense>`.
```tsx
// ✅ option A — enumerate → params resolve into the shell, no Suspense needed for params
export function generateStaticParams() {
return [{ slug: 'shoes' }, { slug: 'hats' }]
}
export default async function Page({ params }: PageProps<'/store/[slug]'>) {
const { slug } = await params // known at build → shell-safe
// ...
}
```
```tsx
// ✅ option B — not enumerable → params is request-time; await it inside a boundary (pattern #1)
```
Root params (the dynamic segments the root layout sits inside, e.g. `app/[lang]/layout.tsx`) are readable from any Server Component via `next/root-params` without prop-drilling — but under Cache Components they must still be enumerated by `generateStaticParams` (at least one value per root param) to land in the shell, the same as any other dynamic param.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime).
---
## 5. `searchParams` → always behind `<Suspense>` (on page load)
Search params are never known at build, so awaiting them (or `useSearchParams()`) suspends on a page load. Keep the rest of the page in the shell by isolating the consumer.
```tsx
// ✅ static content stays in the shell; the search-dependent part streams
export default function Page(props: PageProps<'/search'>) {
return (
<>
<h1>Search</h1> {/* shell */}
<Suspense fallback={<Results.Skeleton />}>
<Results searchParams={props.searchParams} />
</Suspense>
</>
)
}
async function Results({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const { q } = await searchParams
return <ResultList query={q} />
}
```
On a **client navigation** the router already has the URL, so a `useSearchParams()` consumer resolves synchronously and can appear in the prefetched shell — but you still need the boundary for the page-load path.
**Insight:** [runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime) (or, via `useSearchParams` in a Client Component, [URL data in a Client Component](https://nextjs.org/docs/messages/blocking-prerender-client-hook)).
---
## 6. Non-deterministic values → `connection()` + `<Suspense>`, or cache
`Math.random()`, `Date.now()`, `crypto.randomUUID()` produce different output each run, so Cache Components makes you choose: per-request (defer) or fixed (cache).
```tsx
// ✅ per-request value: gate on connection() and wrap in Suspense
import { connection } from 'next/server'
async function RequestId() {
await connection()
return <span>{crypto.randomUUID()}</span>
}
// <Suspense fallback={null}><RequestId /></Suspense>
```
```tsx
// ✅ same value for everyone: cache it so it joins the shell
async function buildId() {
'use cache'
return Date.now()
}
```
**Insight:** [`Date.now()`](https://nextjs.org/docs/messages/blocking-prerender-current-time), [`Math.random()`](https://nextjs.org/docs/messages/blocking-prerender-random), or [`crypto`](https://nextjs.org/docs/messages/blocking-prerender-crypto) while prerendering.
---
## 7. Dynamic `generateMetadata` → static export, `use cache`, or a dynamic-marker for runtime data
```tsx
// ❌ before — reading request data blocks the route's metadata
export async function generateMetadata() {
const c = await cookies()
return { title: c.get('title')?.value }
}
```
```tsx
// ✅ option A — static
export const metadata = { title: 'Store' }
// ✅ option B — cache the metadata (depends on external data, not runtime data)
export async function generateMetadata() {
'use cache'
return { title: await getTitle() }
}
```
```tsx
// ✅ option C — metadata genuinely needs runtime data (cookies/headers):
// keep generateMetadata dynamic, and add a dynamic-marker component to the
// page so the rest of the page still prerenders into the shell.
import { Suspense } from 'react'
import { connection } from 'next/server'
import { cookies } from 'next/headers'
export async function generateMetadata() {
const token = (await cookies()).get('token')?.value
return { title: token ? 'Personalized' : 'Store' }
}
async function DynamicMarker() {
await connection() // signals intentional dynamic content
return null
}
export default function Page() {
return (
<>
<article>{/* static content — stays in the shell */}</article>
<Suspense>
<DynamicMarker />
</Suspense>
</>
)
}
```
`generateViewport` is the same, except dynamic viewport blocks the **whole page**. Genuine instant fixes: a static `viewport` export, or `use cache`. The other two are dynamic-acceptance opt-outs, not instant fixes — do not treat them as a way to reach GREEN: `export const instant = false` opts the segment out of validation while the navigation still blocks, and a `<Suspense>` above the document `<body>` makes the whole route dynamic.
**Insight:** [runtime data in `generateMetadata()`](https://nextjs.org/docs/messages/blocking-prerender-metadata-runtime).
---
## 8. Keep the LCP element in the shell
Don't bury the main heading (the LCP element) inside a boundary — it can't paint until the boundary resolves.
```tsx
// ✅ LCP outside the boundary → paints in the shell
<h1>{product.name}</h1> {/* shell (cache the name if needed) */}
<Suspense fallback={<Reviews.Skeleton />}>
<Reviews productId={id} /> {/* streams */}
</Suspense>
```
---
## 9. Granularity below shared layouts (client-nav correctness)
A single boundary in the **root** layout passes a page-load check but leaves sibling client navigations blocking. Put a boundary **below the shared layout**.
```tsx
// app/store/layout.tsx — boundary below the /store shared layout covers
// client navs like /store/shoes → /store/hats (the root boundary does not)
export default function StoreLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<StoreNav /> {/* shell */}
<Suspense fallback={<Page.Skeleton />}>{children}</Suspense>
</section>
)
}
```
Prefer per-component boundaries inside the page (patterns #1#5) over one big layout boundary — they keep more real content in the shell and stream independently.
**Insight:** the read's own insight surfaces on the client navigation when the boundary is too high — see [where to place the boundary](https://nextjs.org/docs/messages/blocking-prerender-dynamic#choosing-where-to-place-the-boundary).
## 10. Can't push the read down? Runtime-prefetch the whole route
Patterns 19 grow a **static shell** by moving dynamic reads behind boundaries. Some routes resist that: an ID minted per request (`createId()`), an auth/scope resolution the whole subtree needs, a page that is _all_ dynamic by nature. The read can't move, so there is no meaningful shell to commit and the soft nav stays RED. The escape hatch is **runtime prefetching** — don't prerender a shell, run the dynamic render _in the prefetch_ so the whole route is warm before the click and the soft nav commits the real content instantly.
It has **two halves — both required**, and route config alone is RED:
```tsx
// 1. The route opts in — on EVERY leaf (page/default) segment, parallel @slots
// included, or instant validation re-triggers on the segments that lack it
// (the same all-or-nothing coupling as an `instant = false` opt-out).
export const prefetch = 'allow-runtime'
// 2. The <Link> asks for a FULL prefetch — that is what spawns the runtime
// request. A default/auto prefetch only warms the static shell.
<Link href={href} prefetch={true}>…</Link>
// <Link prefetch> already issues the full prefetch on hover and on viewport
// entry, so prefer it. An imperative full prefetch via router.prefetch needs
// the non-exported PrefetchKind enum, so it has no clean public form.
```
Under `instant()` the runtime entry is what commits, so the real content (not a skeleton) shows under the lock — that is the GREEN.
Gotchas (each cost real debugging time):
- **The full prefetch is mandatory.** With App Shells enabled an auto/PPR prefetch bails before the runtime spawn (`subtreeHasSpeculativePrefetch`); only `prefetch={true}` / `kind: 'full'` reaches it. If you set `prefetch = 'allow-runtime'` and it's still RED, the link is doing an auto prefetch.
- **All leaf slots must agree.** `allow-runtime` on the content segment but `instant = false` (or nothing) on a sibling `@header`/`@sidebar` leaf leaves the route's runtime entry incomplete, so the lock falls back to the shell. Flip every leaf together.
- **Prefetch the canonical URL.** A link whose href 307-redirects (a `/foo` that canonicalizes to `/`) can't be prefetched — the prefetch receives the redirect, not the tree. Point the link and the prefetch at the final URL.
- **Don't blanket the full prefetch.** It fetches _all_ the target's dynamic data; issuing it on hover for every link (recents that point at whole chats) is wasteful. Scope `kind: 'full'` to the runtime-prefetch targets only.
- **Marker must be a committed node, not RSC bytes.** The content is often a client component, so its text isn't in the prefetch response — assert a `data-testid` that renders when the client subtree commits, not a substring of the stream.
Prefer a static shell (patterns 19) whenever the read can move: it's cheaper than a runtime prefetch and also covers hard load. Reach for runtime prefetching when the read genuinely can't move, or for a route that's all-dynamic by design.
**Insight:** [dynamic data during prefetching](https://nextjs.org/docs/messages/instant-link-prefetch-partial).
@@ -0,0 +1,88 @@
# Real-app patterns
The rest of this skill models a single linear `layout → page` tree. Production App Router routes add **parallel routes, shared layout UI, and auth gates**, which is where most of the real static-shell work happens. These patterns bridge that gap. Read the skill's `SKILL.md` first.
## Parallel routes: each slot is its own boundary
Instant validation treats every parallel-route slot below the shared layout as an **independent** navigation boundary. Consequences:
- **Each `@slot` needs its own `<Suspense>`** around its dynamic reads; a boundary in one slot does not cover another.
- **An uncovered dynamic read in any slot blocks the whole navigation.** A perfect `@content` does not help if `@sidebar` awaits a session at the top.
- **A slot that renders `null` (e.g. `default.tsx`) is shell-safe**: it is static and performs no reads. Slots that do not re-render for this navigation cost nothing.
```
[tenant]/layout.tsx (shared: already mounted on a soft navigation; not re-rendered)
@content → settings/layout → billing/page ← guard each slot's dynamic reads…
@sidebar → side nav ← …here too (independent boundary)
@header → default.tsx → null ← free
```
## Client-rendered slot routing is not part of the soft-navigation re-render
A common pattern: a stable shared layout renders `@header`/`@sidebar` through a **client** component that swaps slot content based on `usePathname()`. On a soft navigation, Next.js only re-renders the **server** segments that changed below the shared layout; a client-component subtree is not part of that re-render. So that navigation UI neither blocks the navigation nor needs a server `<Suspense>` for it; only the server segments that actually change (e.g. `@content`) matter. It does participate in an initial load (see the caveat below).
## "Instant" is not "useful shell": the empty-shell failure mode
Validation checks that a dynamic read is **guarded by a boundary**, not that the fallback is non-empty. A `<Suspense>` with no `fallback` (or `fallback={null}`) passes validation and commits instantly, but renders a **blank** shell. If a layout and its page both `await getSession()` (your auth library's request-time read) at the top under one empty-fallback boundary, the whole frame collapses to nothing while the user waits. "Validates as instant" and "good loading experience" are different goals.
> Give every boundary a real loading skeleton, and place it low so the most real content stays in the shell. A `fallback={null}` directly above `<body>` is a deliberate empty-shell opt-out; an empty fallback lower in the tree is almost always a bug.
## The responsive-skeleton mismatch: the shell must match every breakpoint
A loading skeleton that misaligns with the loaded UI is its own bug, and it usually appears on mobile. A hand-built skeleton encodes one layout; the real component is responsive and changes shape at breakpoints, so a desktop-shaped skeleton no longer lines up once the viewport is small.
A concrete shape: a list-detail view renders a list or tree in a side panel on desktop, but collapses that panel into a single dropdown or drawer on mobile (with its own loading state). A row skeleton built for the desktop panel has nothing to align with on mobile.
The fix is the same push-down as everywhere else: **share the real responsive layout between the live render and the shell render.** One responsive component renders both (its data slots show the reused `*Skeleton` in the shell and real data after the stream), so the breakpoint switch happens once, for both renders, and there is no second desktop-only skeleton to drift.
(Same hoist rule, responsive layout included.) Verify the shell at both desktop and mobile widths against the real render at the same width.
## Deferring an auth gate / top-level `await` in a layout
A top-level `await` in a layout blocks everything below it (the most common blocker; see [Runtime data during prerendering](https://nextjs.org/docs/messages/blocking-prerender-runtime)). Auth gates are the most common real instance:
```tsx
// ❌ Before: the await + redirect at the top blocks the whole settings frame
export default async function SettingsLayout({ children }) {
const session = await getSession() // your auth library's request-time read; suspends during prerender → frame can't build
if (!session?.user) redirect(getLoginUrl())
return <Shell>{children}</Shell>
}
```
```tsx
// ✅ After: render children unconditionally; move the gate into a Suspense child
import { Suspense } from 'react'
export default function SettingsLayout({ children }) {
return (
<Shell>
<Suspense fallback={null}>
<AuthGate />
</Suspense>
{children}
</Shell>
)
}
async function AuthGate() {
const session = await getSession() // the session read suspends during prerender…
if (!session?.user) redirect(getLoginUrl()) // …so redirect() never runs at build time
return null
}
```
The shell prerenders as if authorized (the session read suspends before `redirect()` is reached, so the redirect only happens at request time), and `{children}` is now in the shell instead of behind the gate. (`fallback={null}` is correct here: `AuthGate` renders nothing on success.)
## Initial-load shell vs soft-navigation shell
The `test-template.md` specs drive a `<Link>` click for soft navigations and `page.goto()` for initial loads. The two shells can differ for the same route:
> **The initial-load shell can show less than the soft-navigation shell when a layout above the shared boundary awaits un-enumerated `params`/`searchParams`.** An initial load re-runs every layout from the root; if a parent layout does `await props.params` and that segment has no `generateStaticParams`, the param suspends on the initial load and its whole subtree drops out of the shell. A soft navigation does not re-render that parent and already has the params. Symptom: an element present after a `<Link>` click is missing after `goto`.
To assert the soft-navigation shell, drive a real `<Link>` click (through menus if necessary). Use `page.goto()` inside `instant()` to assert the initial-load shell, or when no parent above the shared boundary awaits un-enumerated params, in which case the two coincide.
## Edge cases
- **A `React.cache` (or custom memoization) wrapper around `cookies()`/`headers()` still suspends.** Memoizing the call does not make it shell-safe: the underlying request read still returns a pending promise during prerender. Only the **`use cache`** directive, keyed on static or param inputs, puts data in the shell.
- **Playwright cannot see a `display: contents` or fragment fallback.** Such a fallback reads as hidden, so `instant()` assertions cannot `toBeVisible()` it. Give fallbacks a real wrapper element with a `data-testid`.
@@ -0,0 +1,139 @@
# RED-test robustness: verify the RED before optimizing
The C-gate of the workflow. A RED that is red for the wrong reason sends you optimizing a route that
was never broken: the route becomes instant, the test stays red (or the code is contorted to
satisfy a broken assertion), and the effort lands on the wrong problem. The prevention is cheap:
spend a few minutes verifying the RED is trustworthy first.
## The deciding question
> **Does the marker render WITHOUT the lock, as the test user?**
- **No**: the test is red because the marker or page is not there for that user or environment. A
marker bug, not an instant-navigation bug. Fix the marker. (This is the most common case.)
- **Yes**: the marker exists and is reachable; a red under the lock is a genuine "not instant".
Optimize the route.
Everything below serves answering that question honestly.
## The robustness checklist (all must hold)
1. **Red on baseline**: fails on the unfixed route.
2. **Right reason**: without the lock, the marker is visible on the production-build rig (never
`next dev`), running as the test user, not only in the author's own logged-in session.
3. **Differential**: reverting only the fix → RED; re-applying → GREEN; nothing else moves it.
4. **Non-gameable marker**: a sync element of the static shell, never streamed data.
5. **Deterministic on a production build**: stable across N runs; never `next dev`.
6. **Discriminating both ways**: present under the lock on an instant route, absent under the
lock on a blocking one.
7. **Renders for the test user**: under that user's flags, plan, role, and data.
8. **Conditional redirects accounted for**: assert at the route's real destination for that user.
9. **Real selector**: a `data-testid` on a known static-shell node, not a guessed `role`/`name`.
10. **Visible marker**: not `display:none`, off-screen, or inside a hover overlay; for lists,
target `.filter({ visible: true }).first()`.
11. **Fresh build under test**: the deployment being measured contains the latest commit, not a
build URL still serving the previous deploy.
## Taxonomy: red for the wrong reason
Any of these makes a RED untrustworthy. None of them is "the navigation isn't instant."
| Wrong reason | How it occurs | How to rule it out |
| ---------------------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------- |
| **Selector matches nothing** | a guessed `getByRole('button', { name: 'Folder' })` | grep the component for the real accessible name; add a `data-testid` |
| **Conditional redirect** | the route `redirect()`s for the test user (flag/role), so the marker page is never reached | check the page's top-level branches; assert at the real destination, or pin the flag |
| **Flag / plan / role gate** | the author has the flag or plan; the test user does not | run the unlocked baseline as the test user; pin flags via the project's override mechanism |
| **Empty state** | the marker only exists when data does; the CI account is empty | pick a marker present in the empty state (a layout element such as the page header), or seed data |
| **Timeout / flake** | a slow API or transient infrastructure error | re-run; separate infrastructure flake from a real signal |
| **Streamed marker** | the marker is behind `<Suspense>`, so it is never in the shell | choose a sync shell element; verify it sits outside every `<Suspense>` |
| **Auth redirect** | unauthenticated → `/login` | confirm login succeeded before the navigation |
| **Stale deployment** | the test ran against the previous build (the URL under test still serves the prior deploy) | poll the deployment for a marker from the latest commit before trusting any verdict |
| **Hidden / off-screen** | the testid is on a hover-overlay or off-screen list item | put the marker on an always-visible node; `.filter({ visible: true }).first()` for lists |
## Worked cases
These are illustrative failures from real optimization runs; each was red for a wrong reason, and
none was an instant-navigation problem. One app's drift surface might be dominated by feature flags and
plans; another's by auth state, an empty database, or locale. The taxonomy lists every wrong
reason; the rig file's DRIFT list says which rows apply to your app.
- **Guessed selector + empty state**: the marker was a button picked by a guessed accessible name
that no element actually had, on a list page whose CI account had no rows. → checks 7, 9. Fix: a
`data-testid` on a real static-shell node.
- **Hidden marker**: the testid sat first on a `hidden sm:block` hover-overlay link, then on an
off-screen carousel card; Playwright resolved the element but reported it hidden. → check 10.
Fix: an always-visible node; for lists, `.filter({ visible: true }).first()`.
## Differential check (capture in the PR)
The strongest evidence that the RED measured the property:
```
1. on the fixed branch → GREEN
2. revert ONLY the fix (the <Suspense> push-down) → RED
3. re-apply → GREEN
4. confirm no other change moves it
```
Link the two runs (or include the toggle diff and results) in the PR description. A reviewer who
sees the differential knows the test measures the property.
## `instant()` is not a stopwatch
The test does not measure how fast a navigation is. `instant()` gates dynamic data so the content
of the static shell can be asserted; the signal is presence, not speed. Under the lock, an instant
route's shell is present, and a blocking route's content never commits, regardless of wait time.
Therefore:
- The shipped assertion is `await expect(SHELL_MARKER).toBeVisible()` under the lock. Do not add a
custom timeout or a `painted` boolean.
- A custom short timeout (e.g. `3000`) implies a race against a clock that does not exist. It adds
nothing to the verdict and invites false REDs on an instant route whose commit lands a microtask
late.
- Do not use `locator.isVisible({ timeout })` as a soft wait: Playwright deprecated and ignores
that timeout; the call returns immediately.
- "Renders for the test user" (checks 7-9) is established at authoring time with the unlocked
baseline scaffold, not by a timed assertion in the shipped test.
## `instant()` guards need no retries and no prefetch warming
An `instant()` guard is deterministic. Do not configure retries on one, and do not hover-warm to
help a prefetch land in time. Under the lock, the router initiates the route prefetch and awaits it
before committing (even for a `prefetch={false}` link, even for a route already in the prefetch
cache), so the committed shell does not depend on any prior render, hover, or menu-open prefetch.
A flaky guard has a real cause: a marker that is not a sync node of the destination's shell, a
flag/role/empty-state gap for the test user, or a genuinely blocking route. The fix is in the page or
the marker; a retry masks the regression the guard exists to catch. The only legitimate
`.hover()`/menu-open is when the trigger element itself is not in the DOM until hovered or opened.
## Silent no-op: the testing API must be exposed in the measured build
`instant()` works by setting a cookie (`next-instant-navigation-testing`) that lock code inside the
build reads. It does not throw when that lock code is absent; it only throws on nested calls or an
unknown base URL. If the build was produced without the testing API
(`experimental.exposeTestingApiInProductionBuild`), the cookie is ignored, the navigation runs
normally, and the `instant()` test passes vacuously. A green `instant()` test is only meaningful if
the lock engaged.
Two defenses; use both:
1. **Confirm the API is exposed on the target.** Wire the flag to the platform's preview/staging
condition or an explicit environment variable; the rig file records the project's spelling
(SKILL.md phases 0 and A). Do not trust a pass from a build where it is not set.
2. **Make the test self-validating**: for any route with deferred content, also assert that the
deferred content is gated under the lock, not only that the shell is present
(`test-template.md`, self-validating variant). If the lock did not engage, the content is
already present and `toHaveCount(0)` fails.
The gated half holds under the lock for both navigation types regardless of warm state: the
soft-nav client lock gates dynamic-data writes, and on an initial load the server honors the
cookie on the document request and suspends dynamic data. A vacuous pass is only possible with
a build produced WITHOUT the testing API, which defense #1 above covers.
## Determinism and the rig
- Always measure on a production build, never `next dev`; SKILL.md phase A owns this invariant and
its rationale.
- Run the RED several times; an intermittently red gate is not a gate. If it flakes, determine
whether the cause is infrastructure (transient errors) or a real race before trusting either
color.
@@ -0,0 +1,103 @@
# Rig discovery: generate this project's `instant-nav.rig.md`
The skill's principles are environment-independent. Your build, deploy, auth,
and test infrastructure are not. This phase converts the principles into THIS
project's concrete workflow: run discovery once per repo, write the answers to
a committed `instant-nav.rig.md` (repo root, or next to your e2e config), and
every later run reads that file instead of rediscovering.
The skill is deliberately opinionated about **what** the rig must provide, and
deliberately unopinionated about **how** your stack provides it.
## How to discover
Inspect before asking. Most answers are already in the repo:
- `package.json` scripts (`build`, `start`, `test:e2e`)
- the e2e config (`playwright.config.*`: `baseURL`, `webServer`, projects)
- CI config (`.github/workflows/`, `vercel.json`, GitLab/Circle files,
Dockerfiles)
- `next.config.*` (existing `experimental` flags)
- existing e2e auth helpers (grep for `login`, `storageState`, `session`)
Ask the user only what the repo can't answer. Typically that means: which
deploy target counts as "preview", which account the suite runs as in CI, and
whether an agent is allowed to push and wait on CI unattended.
## The six questions (all must have answers), plus two derived fields
The six questions below must all have answers. The rig file template adds two
more fields the discovery feeds rather than asks directly: **LIVENESS** (the
SHA-echoing probe, derived from the LOOP answer) and **WALLS** (project-specific
build/run obstacles, accumulated as you first hit them).
1. **BUILD**: how is a production build of this app produced and served?
A per-push preview deploy, a staging container, or bare
`next build && next start`. Anything but `next dev`.
2. **EXPOSE**: what condition turns on
`experimental.exposeTestingApiInProductionBuild` for every measured build,
and never for real production? Spellings: an explicit
`EXPOSE_TESTING_API=1` for local production builds; `process.env.DEPLOY_ENV
=== 'staging'` for a generic CI/staging env var; `process.env.VERCEL_ENV ===
'preview'` on Vercel.
3. **RUN**: how is the Playwright suite invoked, and against which
`BASE_URL`?
4. **TEST USER**: which account does the suite run as, and how does login
happen (helper, `storageState`, API token)? What flags / plan / role / data
does that account have?
5. **DRIFT**: enumerate everything that can differ between the author's own
session and the test user's environment (feature flags, plans and
entitlements, roles, seeded vs empty data, locale, A/B buckets). Every item
is a way a RED can become untrustworthy; this list feeds the C-gate
(`reference/red-test-robustness.md`).
6. **LOOP**: the unattended iteration for your rig. Push → build → e2e
against the artifact → read the failure → fix → push (CI), or build → start
→ e2e (local). Note anything an agent cannot do alone (deploy approvals,
secrets, protected branches). Include the **liveness probe**: the endpoint
or response header that echoes the deployed commit SHA (e.g. a `/healthz`
route or an `x-deployed-sha` header), so a CI run can confirm the build
under test matches `HEAD` before trusting a verdict (SKILL.md phase A). If
the platform exposes no SHA-echoing endpoint or header, add one: surface a
build-time commit var (`VERCEL_GIT_COMMIT_SHA`, a CI commit variable) on a
`/healthz` route or a response header, or fall back to polling the deploy
platform's API for the deployment whose `commitSha === HEAD`. Record the
chosen mechanism. For a local `build && start` rig the artifact is the one
freshly built, so no probe is needed.
## The file: copy, fill, commit as `instant-nav.rig.md`
```md
# instant-nav rig: <project>
- BUILD: <command / platform that produces the measured production build>
- EXPOSE: <the condition wired to exposeTestingApiInProductionBuild>
- RUN: <e2e command> against <how BASE_URL is obtained>
- TEST USER: <account> via <login mechanism>; flags/plan/role/data: <...>
- DRIFT: <the enumerated drift surface>
- LOOP: <push → CI → e2e, or local build → start → test>; agent limits: <...>
- LIVENESS: <endpoint/header echoing the deployed SHA; n/a for local build && start>
- WALLS: <project-specific build/run obstacles + their workarounds>
```
Real apps rarely build for production cleanly on the first attempt: missing
secrets, server-only imports that fail prerender, ports held by respawning
servers. Record each wall and its workaround the first time you hit it. `WALLS`
accumulates the project-specific build/run obstacles that the other fields
cannot capture.
## Filled examples
**No CI / local-only.** BUILD: `EXPOSE_TESTING_API=1 next build && next
start`. EXPOSE: that env var. RUN: `BASE_URL=http://localhost:3000 playwright
test`. LOOP: build → start → test on one machine; fully agent-drivable, with
nothing to push, no secrets, and no deploy wait.
**Generic CI + container.** BUILD: the pipeline builds an image and deploys it
to a staging namespace. EXPOSE: `process.env.DEPLOY_ENV === 'staging'`. RUN: a
CI job runs Playwright against the staging URL. LOOP: push → pipeline → e2e;
fully agent-drivable once the pipeline is wired.
**Vercel preview deploys.** BUILD: every push builds a preview. EXPOSE:
`process.env.VERCEL_ENV === 'preview'`. RUN: `playwright test` with
`BASE_URL=<preview URL>`. LOOP: push → preview → e2e; fully agent-drivable
once the preview deploy and `VERCEL_ENV` gating are in place.
@@ -0,0 +1,165 @@
# Test template: the instant() guard
Ship one test per navigation type you are guarding: under `instant()`, assert that the
destination's static shell appears. `instant()` gates dynamic data, so a correctly instant route
commits its shell under the lock and a blocking route does not. `instant()` is a ruler, not a
stopwatch: do not add custom timeouts or timing races (see `reference/red-test-robustness.md`).
Whether the marker is the right one (rendering for the test user, not flag-gated, not redirected
away, not guessed) is established at authoring time with the unlocked baseline scaffold below
(phase B, the C-gate), not by additional assertions in the shipped test.
All identifiers in angle brackets (`<b>`, `<Trigger>`) and the `../helpers` import are placeholders;
substitute your project's e2e auth helper, URL helper, and real testids/trigger before running.
## Soft navigation (client-side navigation)
Drive a real `<Link>` click. The committed shell is the destination's prefetched App Shell.
Under the lock the router initiates and awaits the route prefetch itself, so no manual warming is
needed; if the shell is intermittently absent, treat it as a real blocker or marker bug (C-gate),
never as a warming race. Do not add waits or hovers.
```ts
import { test, expect } from '@playwright/test'
import { instant } from '@next/playwright'
// Use the auth/setup helpers your e2e suite already has. Run as the test user
// (defined in SKILL.md phase B).
import { logIntoTestAccount, testUrl } from '../helpers'
// A SYNC element of the destination's static shell (header, action button,
// column header), not data that streams in, and one that renders for the
// test user (not gated by a flag, plan, role, or empty state). Prefer a
// data-testid on a known static node over a guessed role/name.
const SHELL_MARKER = '[data-testid="<b>-shell-marker"]'
test.describe('instant nav: A -> B', () => {
test.beforeEach(async ({ page, browser }) => {
await logIntoTestAccount(page, browser)
})
test('B shell commits under instant()', async ({ page }) => {
await page.goto(testUrl('/'))
const trigger = page.getByRole('link', { name: '<Trigger>', exact: true })
await expect(trigger).toBeVisible({ timeout: 20000 })
await instant(page, async () => {
await trigger.click()
// static shell asserted under the lock; no timeout
await expect(page.locator(SHELL_MARKER)).toBeVisible()
})
})
})
```
The trigger selector follows the same rule as `SHELL_MARKER`: prefer a `data-testid` on the real
`<Link>` (`page.getByTestId('<trigger>-link')`) over a guessed accessible name. `getByRole({ name })`
is shown only for brevity; like the marker, the trigger must reliably resolve for the test user.
## Initial load (hard navigation)
Drive `page.goto()` inside `instant()` with the `baseURL` option. The served document is the
route's prerendered static shell. `baseURL` is required because `page` is still `about:blank` when
`instant()` runs (`resolveURL` falls back to `page.url()` only when no `baseURL` is passed).
Establish the session WITHOUT navigating `page` (inject `storageState`, or log in on a separate
context/page). A login helper that navigates `page` itself defeats the measurement for a different
reason: that navigation completes before `instant()` acquires the lock, so it runs unmeasured. The
session must be pre-established either way; otherwise an authenticated route redirects to login
and the RED is false.
If the project's only login helper navigates `page`, the agent must use a
storageState/separate-context path here instead, a session-injection call that
does NOT call `page.goto`:
```ts
test.describe('instant initial load: B', () => {
test.beforeEach(async ({ page }) => {
await injectTestUserSession(page) // storageState only; must NOT call page.goto
})
test('B shell is served', async ({ page }) => {
const url = testUrl('/<b>')
await instant(
page,
async () => {
await page.goto(url)
await expect(page.locator(SHELL_MARKER)).toBeVisible()
},
{ baseURL: new URL(url).origin }
)
})
})
```
## Self-validating variant (recommended for routes with deferred content)
Also assert that the deferred content is gated under the lock and streams after release. This
makes a vacuous pass impossible: if the lock did not engage (testing API missing from the build),
the content is already present and `toHaveCount(0)` fails (see `reference/red-test-robustness.md`).
`SHELL_MARKER` is the shell node; `[data-testid="<b>-content"]` is the deferred data it guards.
```ts
// soft navigation
await instant(page, async () => {
await trigger.click()
await expect(page.locator(SHELL_MARKER)).toBeVisible() // shell present
await expect(page.getByTestId('<b>-content')).toHaveCount(0) // deferred data gated
})
await expect(page.getByTestId('<b>-content')).toBeVisible() // streams after release
```
The two **gated-half** assertions (shell visible, deferred content `toHaveCount(0)`) apply to the
initial-load `page.goto()` form too. The cookie gates the deferred content identically for both:
on a soft navigation the client lock gates dynamic-data writes; on an initial load the server
honors the cookie on the document request (set via `addCookies()` before navigation, scoped by
`baseURL`) and suspends dynamic data, independent of whether the route was previously rendered or
cached. So the initial-load `toHaveCount(0)` gated half is as valid as the soft-nav one; it needs
no fresh browser context and no cache-busting query param.
The **post-release** assertion (`getByTestId('<b>-content').toBeVisible()` after the `instant()`
block) is soft-nav only. On an initial load the document was already emitted under the lock, so
nothing streams in after release; drop that assertion from the initial-load test, or
`page.reload()` first to fetch an unlocked document. The mechanism is in
`reference/red-test-robustness.md`.
## Baseline scaffold: do not ship
Before optimizing, confirm the target exists with an unlocked check (no `instant()`). It
disambiguates "not instant" from "marker absent for this user or environment". Run it as the test
user; mismatch against the rig DRIFT list is what the C-gate catches
(`reference/red-test-robustness.md`). Confirm the marker is real and reachable, then delete the
scaffold before the PR.
**The baseline must mirror the navigation type of the test you are shipping.** Drive a `<Link>`
click when guarding the soft-nav shell; drive `page.goto()` when guarding the initial-load shell.
The two shells can differ (`reference/real-app-patterns.md`): a click-driven baseline run against a
shipped `goto` test would confirm a marker that the `goto` path never shows, which produces exactly
the false RED the C-gate exists to prevent.
```ts
// soft-nav baseline: mirror the soft-nav instant() test
test('dev-only: navigating to <b> renders its shell (no lock)', async ({
page,
}) => {
await page.goto(testUrl('/'))
const trigger = page.getByRole('link', { name: '<Trigger>', exact: true })
await expect(trigger).toBeVisible({ timeout: 20000 })
await trigger.click()
await expect(page).toHaveURL(/\/<b>(\?|$)/) // confirm the real destination (no redirect away)
await expect(page.locator(SHELL_MARKER)).toBeVisible({ timeout: 15000 })
})
```
```ts
// initial-load baseline: mirror the initial-load instant() test (session pre-established)
test('dev-only: <b> shell is served (no lock)', async ({ page }) => {
await page.goto(testUrl('/<b>'))
await expect(page.locator(SHELL_MARKER)).toBeVisible({ timeout: 15000 })
})
```
Notes:
- Pick `SHELL_MARKER` as a sync element of the destination's static shell, never streamed data.
Use a `data-testid` on a known static node rather than a guessed role/name.
- Do not put a custom timeout, a `painted` boolean, or `isVisible({ timeout })` in the shipped
assertion, and do not add retries or hover-warming; see `reference/red-test-robustness.md`.
@@ -13,17 +13,19 @@ description: >
Enable Partial Prefetching and walk the app until every link reuses a shared App Shell. This skill sequences the work; per-insight recipes live in the dev overlay fix cards and their docs pages. The [Adopting Partial Prefetching guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for the concepts this skill applies.
The one thing that shapes everything below: **these insights surface only in `next dev`, in the dev overlay's Insights tab.** Nothing fails the build. There is no build-only fallback loop for this skill — the work is a sweep of the running app in the browser. If you can't drive a browser, stop and tell the user what you can't verify, or commit the milestone you've reached and hand off.
The one thing that shapes everything below: **these insights surface only in `next dev`, in the dev overlay's Insights tab.** Nothing fails the build. There is no build-only fallback loop — confirming an insight is _cleared_ means driving the running app in a browser. But a missing browser gates that verification, not the whole skill: the adoption work is static and runs from the guide, so do the static pass anyway and hand off the live shell check.
Talk to the user in terms of what they'll see — PRs, features, and how the app behaves after — never the insight slugs or step labels. Before you start, tell them briefly what Partial Prefetching changes: a `<Link>` loads a shared App Shell, and `prefetch={true}` no longer prefetches everything the old full prefetch did.
## requires
- **Cache Components already adopted.** `partialPrefetching` only works with `cacheComponents: true`, and the sweep below assumes the app has no blocking-route errors left: a route whose static shell fails validation surfaces the blocking-prerender error _instead of_ the prefetch insight, so unfinished adoption hides exactly the signal this skill works from. Run [`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption) to completion first — this skill is its follow-up.
- **Cache Components on (`cacheComponents: true`).** This is the only hard requirement; `partialPrefetching` depends on it. Full Cache Components adoption is the ideal starting point but not a gate. Nothing in this skill blocks the build, and neither do the prerender insights an unadopted route surfaces, like a leftover `unstable_noStore` or a `cookies()` read outside `<Suspense>`: they are non-blocking dev signals, expected on any fresh branch off `main`, not a reason to stop. They replace the URL-data insight only on their own route in the [step 3](#step-3-sweep-for-url-data-insights-after-enabling) sweep; the flag-off step 1 audit and its static adoption run regardless. The only thing that actually stops this skill is a build-blocking failure, and anything build-blocking would have been resolved before you reached here. Otherwise fix the prerender insights you hit as inline [`next-cache-components-adoption`](https://github.com/vercel/next.js/tree/canary/skills/next-cache-components-adoption) work, or hand them off, and keep going.
- **Next.js 16.3 or later.** `partialPrefetching`, the `prefetch` route segment config, and the prefetch insights all land there.
- **A browser you can drive.** Install [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) before starting (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). Link prefetches fire when a link renders and enters the viewport, and shell validation fires on navigation — neither is reachable from `curl` or the build. If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log.
- **A browser you can drive.** Install [`next-dev-loop`](https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop) before starting (`npx skills add https://github.com/vercel/next.js/tree/canary/skills/next-dev-loop`). Install it without asking — it's a tool, not a product change — and don't assume it's blocked: verify a real blocker (no network, no npm, read-only filesystem) before falling back, and name it in your report. Link prefetches fire when a link renders and enters the viewport, and shell validation fires on navigation — neither is reachable from `curl` or the build. If the app is webpack-pinned, drive a browser directly (`agent-browser`, Playwright) — you lose the framework cross-checks, not the insights; they're still in the overlay and the dev log.
- **A runnable app.** Verification runs against `next dev` for the insight sweep and a production `next build`/`next start` for prefetching (prefetching is prod-only), so the app has to boot in both. If it reads a database or required env at import (e.g. an `env.ts` that throws on a missing `DATABASE_URL`), confirm it starts — with the real environment, or local data you stand up — before step 1. An app that won't run can't be swept or verified.
### notes
@@ -35,11 +37,13 @@ Talk to the user in terms of what they'll see — PRs, features, and how the app
Adopting Partial Prefetching means every route still delivers what its links prefetched before, now split between the App Shell (static and cached content) and the per-link runtime data behind `prefetch = 'allow-runtime'`. The [guide](https://nextjs.org/docs/app/guides/adopting-partial-prefetching) is the canonical reference for what a prefetch contains and how to decide each case; this skill sequences that work against a running app.
The catch that decides most of the sweep: `partial` warms only the shared App Shell, so a route keyed by `params`/`searchParams` needs [`prefetch = 'allow-runtime'`](https://nextjs.org/docs/app/guides/runtime-prefetching) to prefetch its content, not `partial` (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section).
## working surfaces
- **The dev server terminal — your primary record.** Each validated route's insights are logged as `Error: Route "...": Next.js encountered ...` lines with the `https://nextjs.org/docs/messages/<slug>` link. Tail the dev log during the sweep; it's the greppable record of what fired where, and it works the same on Turbopack and webpack.
- **The dev overlay Insights tab.** Insights are the amber, non-blocking tab. It appears only once an insight has fired, so a route that surfaces nothing shows no tab at all — that's the clean state, not a missing feature. Don't hunt for the tab on a quiet route; confirm clean from the dev log above, which is the reliable signal. The precondition is no blocking-prerender errors — those replace the insight on their route (see requires). An unrelated Issue (a hydration error, a console error) doesn't block the sweep; don't stall on it. When the tab is present, the overlay pill shows the count and each insight has fix cards linking its docs page. The overlay renders inside a shadow root (`nextjs-portal`), so accessibility-tree snapshots don't see it — evaluate into `shadowRoot` when you need to read or click it programmatically.
- **`next-dev-loop`** to drive navigations and read the overlay. Prefer it over hand-rolled browser automation for the same reasons as in the Cache Components skill (webpack apps: see requires).
- **`next-dev-loop`** to drive navigations and read the overlay. Prefer it over hand-rolled browser automation for the same reasons as in the Cache Components skill (webpack apps: see requires). When browsing its `/_next/mcp` tools, the prefetch insights surface through `get_errors` and the overlay, not the similarly-named `get_request_insights`. That one is the span and performance recorder (gated behind `experimental.requestInsights`) and reports nothing about prefetching.
Every insight has a docs page — open it. Fetch the linked page for every distinct insight you encounter; the inline message is a summary, the page is the recipe.
@@ -52,12 +56,12 @@ If `partialPrefetching: true` is already set in `next.config.ts`, the app is ado
The work is identical either way — only the commit boundaries differ. Default by app size: one branch for a handful of links, route by route when the audit is big enough that reviewers need smaller diffs. Note the choice in your report.
Enumerate the links across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages: `grep -rnE '\bprefetch\b' --include='*.tsx' --include='*.jsx' .`, then keep the `prefetch={true}` and bare `prefetch` prop matches (a bare prop is `true`) and drop `prefetch={false}` and other values. If nothing matches, check for a custom link wrapper before calling the audit empty — grep for `from 'next/link'`, and if a wrapper sets `prefetch={true}` internally or forwards it under another prop name, audit its call sites the same way. If there's still nothing, say so in your report and move on to [step 2](#step-2-enable-the-flag).
Enumerate the links across the whole source tree, not only `app/` — they often live in `src/components` or shared UI packages: `grep -rnE '\bprefetch\b' --include='*.tsx' --include='*.jsx' .`. Keep the `prefetch={true}` and bare-prop matches (a bare prop is `true`) as the over-prefetching links this audit adopts destinations for, and drop `prefetch={false}` and other values. The same grep also surfaces imperative prefetching: a `router.prefetch(href, { kind: 'full' })` call (often behind a custom hook) is the imperative equivalent of `prefetch={true}`. Audit each such call site's destination the same way you would a link, and verify its prefetch payload in a production build ([step 4](#step-4-verify)) — the flag can change what an imperative `kind: 'full'` prefetch returns, and no insight covers it. If nothing matches, check for a custom link wrapper before calling the audit empty. If there's still nothing, say so in your report and move on to [step 2](#step-2-enable-the-flag).
Then, for each one:
1. **Click it** in `next dev`. The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it.
2. **Adopt the destination.** Add `export const prefetch = 'partial'`. That clears the insight for every link pointing at it. If the route reads URL data (`params`, `searchParams`), it's a runtime-prefetch candidate for step 5 — keep `prefetch={true}` on its links and mark the route:
1. **Click it** in `next dev`. The insight fires at navigation time, not when the link prefetches, so a link sitting in the viewport won't trip it — you have to navigate through it. This click is _verification_: it confirms the insight fires before you adopt and clears after. Without a browser, skip the click and adopt from [`link-prefetch-partial`](https://nextjs.org/docs/messages/instant-link-prefetch-partial) and the audit table below — the destination's structure tells you the row, and type-check gates the edit — then leave the live confirmation for the hand-off.
2. **Adopt the destination.** Add `export const prefetch = 'partial'`. That clears the insight for every link pointing at it. If the route reads URL data (`params`, `searchParams`), `partial` warms only its skeleton (the guide's [URL data](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#url-data) section), so it's an `allow-runtime` candidate for step 5, not a finished adoption. Keep `prefetch={true}` on its links and mark the route:
```tsx
// TODO(runtime-prefetch): assess with the user (prefetch = 'allow-runtime')
@@ -68,6 +72,8 @@ Then, for each one:
3. **Preserve what that prefetch delivered.** The guide's [audit table](https://nextjs.org/docs/app/guides/adopting-partial-prefetching#auditing-link-prefetchtrue-calls) is the canonical decision — fetch it and apply the matching row rather than re-deriving it. Caching uncached content is the judgment call in that table: trace where the data comes from and what freshness and revalidation it needs, per the [`use cache`](https://nextjs.org/docs/app/api-reference/directives/use-cache) docs, and ask the user when the answer isn't clear-cut. The URL-data routes you marked in the previous item wait for step 5.
> **If you add `use cache`, verify under `next start`, not only the build.** A `cookies()`/`headers()`/session read anywhere in the cached call tree throws at request time while `next build` passes clean. See [`use cache`](https://nextjs.org/docs/app/api-reference/directives/use-cache).
## step 2: enable the flag
Once every audited destination has `prefetch = 'partial'`, finish in two moves.
@@ -85,11 +91,15 @@ Once every audited destination has `prefetch = 'partial'`, finish in two moves.
## step 3: sweep for URL-data insights (after enabling)
This is a dev-only second pass. The shell check runs only with the flag on, fires at navigation time, and never blocks the build, so it can happen any time after step 2. The work is one loop — build a route queue from a concrete source (the last `next build` route table, or the `app/` tree), keep it as a todo list, and load every route in `next dev` until the queue is empty.
This is a dev-only second pass. The shell check runs only with the flag on, fires at navigation time, and never blocks the build, so it can happen any time after step 2. Build the route queue from a concrete source (the last `next build` route table, or the `app/` tree) and keep it as a todo list.
Sweep feature by feature. A feature is a single product surface — `app/settings/**`, `app/posts/[slug]/**` — not a whole top-level area. Finish one end-to-end before starting the next: load its routes in `next dev` and resolve their insights. The insight never blocks the build and each route is independent, so a partial sweep leaves a working app, and each feature is a self-contained change the user can review or ship on its own.
If the environment can't finish the whole sweep (slow first compiles, a dev server that falls over under load, no browser at all), take the browser-free work as far as it goes before handing off. Adopt every route you can statically: apply the fix from [`URL data`](https://nextjs.org/docs/messages/instant-shell-url-data) (up to a new `<Suspense>` boundary) and opt the route into `prefetch = 'partial'`, gating on type-check. Work the whole queue in one pass — a larger refactor isn't a reason to defer, and asking whether to continue to the next route or tier isn't a checkpoint; keep going. Stop only for a genuine judgment call, and batch those into the single hand-off report: the routes you statically adopted, the ones still needing a live shell check, and the queue.
Watch the Insights tab and the dev log for `Next.js encountered … data` lines. The signal this step adds is [`URL data`](https://nextjs.org/docs/messages/instant-shell-url-data): a `params` or `searchParams` read outside `<Suspense>` ties the shared shell to one URL. It can surface even inside an existing `<Suspense>` when the boundary sits above the read. Open its docs page and follow the fix there.
If Cache Components adoption left gaps, loading routes can also re-surface the blocking-prerender errors from that step — [`runtime data`](https://nextjs.org/docs/messages/blocking-prerender-runtime) (`cookies()`/`headers()`) or [`uncached data`](https://nextjs.org/docs/messages/blocking-prerender-dynamic) (an uncached `fetch`/DB call). Those aren't Partial Prefetching insights; treat them as unfinished Cache Components work and fix them the same way.
Loading a route with the flag on prerenders its App Shell, which validates more of the route than the Cache Components build did. So a route that built cleanly under Cache Components (every route `◐`, no errors) can still surface a `blocking-prerender-*` error here the first time its shell is prerendered — [`runtime data`](https://nextjs.org/docs/messages/blocking-prerender-runtime) (`cookies()`/`headers()`), [`uncached data`](https://nextjs.org/docs/messages/blocking-prerender-dynamic) (an uncached `fetch`/DB call), or sync IO like `Date.now()`/`new Date()`. This doesn't mean the Cache Components adoption was incomplete; it's new validation reaching a path the build never exercised. These aren't Partial Prefetching insights — fix each one the same way you would any blocking-prerender error.
These fixes rarely involve the user — each insight names the offending read and its docs page has the fix, so apply it and keep sweeping. Collect the rare exceptions for one batched question at the end: a page that is entirely one URL-dependent region (wrapping it all leaves an empty shell), or a route that should arguably stay opted out. Don't narrate the refactor with comments — the `<Suspense>` boundaries speak for themselves.
@@ -99,13 +109,16 @@ Checklist before checking in with the user:
- **An empty sweep is the expected outcome when Cache Components adoption finished cleanly** — the prereq already forced every `params`/`searchParams`/`cookies()` read behind `<Suspense>` (surfaced there as `blocking-prerender-*` errors), so a quiet log is success, not a missing signal. Any entry still in the Insights tab is a deliberate, documented decision. To confirm the signal can still fire, check `partialPrefetching` is on, the version is 16.3 or later, and the dev server was restarted after the config change — or move one URL read back outside `<Suspense>`, watch validation fire, then revert. Expect the probe to surface the Cache Components `blocking-prerender-runtime` error rather than the URL-data insight (the upstream check catches the read first) — either one proves the pipeline is alive.
- The App Shells are real: for each route you changed, confirm the first paint after a navigation shows the intended shared content, not an empty shell or a stuck fallback. A `<Suspense>` around the whole page body passes validation with an empty shell, which defeats the point.
- The insights validate shell _structure_, not that a prefetch actually happened. Confirm on the production run (prefetching is prod-only) that navigating a changed link lands on the shared shell instantly.
- **If the app prefetches imperatively** (`router.prefetch(href, { kind: 'full' })`), the insight sweep does not cover it, so an empty sweep is not proof the prefetch survived the flag. On the production run, confirm the prefetch payload for a changed link still carries the route's data, not only the App Shell — compare its `_rsc` prefetch response against a full render of the same route (`PerformanceResourceTiming.decodedBodySize`, or diff the RSC payloads). A shell-only payload means the flag changed what `kind: 'full'` returns; restore the data with `use cache` so it rides the App Shell, or `prefetch = 'allow-runtime'` ([step 5](#step-5-runtime-prefetching-optional)).
- **Before blaming a broken route on the flag, reproduce it with `partialPrefetching` off** (or on the pre-flag branch). The flag surfaces existing issues — a fragile request-time auth gate, a rewrite, deployment skew — earlier and more visibly, but rarely causes them. If it breaks flag-off too, it isn't a Partial Prefetching problem; fix it there, not here.
- `next build` still passes.
Then check in with the user. Speak their language — no insight slugs or step labels.
- What you did: which links you audited, which destinations you adopted, and what each link now prefetches.
- What changed: dropped props, `use cache` boundaries added, and which routes carry a `TODO(runtime-prefetch)` marker for later.
- Demo against a production run. Prefetching is limited in development, so `next dev` won't show the result — run `next build` and `next start`, and hand the user that URL.
- Demo against a production run. Prefetching is limited in development, so `next dev` won't show the result — run `next build` and `next start`, and hand the user that URL. That run needs the app's real environment (database, auth, secrets), and a partial or stale install or leftover generated artifacts can fail the build for reasons unrelated to the adoption. Set the expectation up front that verification is a complete, credentialed production run, not a quick check.
- Show, don't tell: drive one link live in the headed browser against the production server, so they see the shared App Shell paint instantly and the URL-specific region stream in. Attach before/after screenshots only when a live browser isn't possible.
- Give them the click-through: a table of each changed route — the link to click, and what to expect after the click (what paints instantly, what streams in) — so they can verify each result themselves.
- The question: "Want to commit this (or open the PR) before we look at which routes should also prefetch their URL-specific content?" Wait for the answer — adoption and runtime prefetching read best as their own changes.
@@ -114,7 +127,7 @@ Then check in with the user. Speak their language — no insight slugs or step l
The audit marked the candidates instead of deciding them. Grep for `TODO(runtime-prefetch)` and walk the list with the user in one conversation. The question per route is whether they want the URL-dependent content prefetched ahead of the click, or streaming in after navigation is fine. A runtime prefetch costs a server invocation per prefetchable link — the guide's [per-link prefetching trade-offs](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) section is the checklist. Don't make these calls alone.
Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching) — add `export const prefetch = 'allow-runtime'` to the route (the codemod in step 2 already stripped the `'partial'` export) and cache the content behind the read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Where it's no, delete the marker and leave the route on the default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in routes against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR.
Where the answer is yes, follow the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching) — add `export const prefetch = 'allow-runtime'` to the route (the codemod in step 2 already stripped the `'partial'` export) and cache the content behind the read using the guide's patterns (`use cache` with the runtime value passed in, or `use cache: private` for per-user data). Each per-link prefetch is a server render, so the [runtime prefetching guide](https://nextjs.org/docs/app/guides/runtime-prefetching#per-link-prefetching-trade-offs) covers the trade-offs, including when to bound it to hover instead of the viewport. Where it's no, delete the marker and leave the route on the default. Either way no `TODO(runtime-prefetch)` marker survives this step. Confirm the opted-in routes against a production run (`next build` and `next start` — the runtime prefetch fires there, not in `next dev`), give the user the same click-through for them, and keep this as its own commit or PR.
## further reading
@@ -2,8 +2,8 @@
"sourceId": "ppt-master",
"repo": "https://github.com/hugohe3/ppt-master.git",
"ref": "main",
"commit": "07d83e5a31a2830cc6c314377463e523a76e28fc",
"commit": "89759436bd336beed3a32da15830355bf732b14e",
"adapter": "claude-skill",
"sourcePath": "skills/ppt-master",
"syncedAt": "2026-07-21T16:00:00Z"
"syncedAt": "2026-07-22T16:00:00Z"
}
@@ -56,3 +56,4 @@ PPT Master is a routed presentation workflow. This entry owns global execution d
- Keep required workflow, reference, script, and template documentation inside this Skill directory.
- Repository-level documents may point into the package; package runtime files must not depend on repository-level instructions.
- On Windows, if a documented `python3 ...` command is unavailable, rerun the same command with `python`.
- Sponsor information is optional reference material. Read the matching [`SPONSORS.md`](SPONSORS.md) or [`SPONSORS_CN.md`](SPONSORS_CN.md) only when the user explicitly requests a model, AI image model, API/provider, or hosted-service recommendation. Never surface sponsor or model recommendations proactively during normal generation, troubleshooting, or quality review.
@@ -0,0 +1,61 @@
# Sponsors and User Benefits
English | [中文](SPONSORS_CN.md)
PPT Master is free and open source. The sponsors below help fund its ongoing maintenance and provide optional benefits for PPT Master users. Sponsorship does not change the project's provider-agnostic design: use any compatible agent, model, or service you prefer.
Want to support the project or discuss a partnership? See the [sponsorship guide](https://github.com/hugohe3/ppt-master/blob/main/SPONSORING.md).
## Current Model Recommendation
For the best results, use **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)** (or Claude) to drive the workflow, paired with **`gpt-image-2`** (OpenAI) or **`gemini-3.1-flash-image`** (Google) for AI image generation. If model capability is limiting output quality, upgrade the driving model before weakening the workflow or its quality requirements.
## Kimi
<p align="center">
<a href="https://www.kimi.com/code/?aff=ppt-master"><img src="https://gcdn.moonshot.cn/growth-cdn/sponsor/kimi-en.png" alt="Kimi" width="100%"></a>
</p>
Thanks to [Kimi](https://www.kimi.com/code/?aff=ppt-master) for sponsoring PPT Master. [Kimi K3](https://platform.kimi.ai/docs/guide/kimi-k3-quickstart) is the world's first open 3T-class model, featuring native vision and a 1-million-token context window. With PPT Master, K3 can understand PDFs, DOCX files, web pages, and other source material, structure the narrative, and generate a natively editable PPTX.
**Try [Kimi Code](https://www.kimi.com/code/?aff=ppt-master), or access the API through the Kimi Open Platform ([中文站](https://platform.kimi.com?aff=ppt-master) | [Global](https://platform.kimi.ai?aff=ppt-master)).**
## Model Access Partners
### PackyCode
<a href="https://www.packyapi.com/register?aff=ppt-master"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/packycode.png" alt="PackyCode" width="150"></a>
[PackyCode](https://www.packyapi.com/register?aff=ppt-master) provides relay access to Claude Code, Codex, Gemini, and other services. Register through the dedicated link and enter the promo code **`ppt-master`** during recharge to receive 10% off.
### APIKEY.FUN
<a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" width="150"></a>
[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER) provides enterprise-grade access to Claude, OpenAI, Gemini, and other mainstream models. Register through the dedicated link to receive up to a permanent 5% discount on top-ups.
### RunAPI
<a href="https://runapi.co/register?aff=WMLJ"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a>
[RunAPI](https://runapi.co/register?aff=WMLJ) provides access to 150+ models, including OpenAI, Claude, Gemini, DeepSeek, and Grok, through one API key. Register through the dedicated link and contact an administrator to claim **¥7 in free credit**.
### YouYun ZhiSuan
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/youyun.png" alt="YouYun ZhiSuan" width="150"></a>
[YouYun ZhiSuan](https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624), UCloud's AI cloud platform, provides domestic and international model APIs, CodingPlan packages, enterprise concurrency, technical support, and invoicing. Register through the dedicated link to receive up to **¥10 in free trial credit**. PPT Master is also available there as a hosted Agent for users who do not want to deploy it locally.
## Infrastructure Support
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40"></a>
Thanks to [DigitalOcean](https://m.do.co/c/547f129aabe1) for supporting the project's infrastructure.
## Individual Support
If PPT Master has been useful to you, individual support of any amount helps keep the project moving and free.
<a href="https://paypal.me/hugohe3"><img src="https://img.shields.io/badge/PayPal-Sponsor-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="Sponsor via PayPal"></a>
[Alipay QR code](https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/alipay-qr.jpg)
@@ -0,0 +1,61 @@
# 赞助商与用户福利
[English](SPONSORS.md) | 中文
PPT Master 始终免费开源。以下赞助方共同支持项目的持续维护,并为 PPT Master 用户提供可选福利。赞助不会改变项目不绑定 Provider 的设计:你仍可自由选择任何兼容的 Agent、模型或服务。
希望支持项目或洽谈合作?请查看[赞助合作说明](https://github.com/hugohe3/ppt-master/blob/main/SPONSORING_CN.md)。
## 当前模型推荐
追求最佳效果,语言模型选 **[Kimi K3](https://www.kimi.com/code/?aff=ppt-master)**(或 Claude)驱动流程,搭配 AI 生图模型 **`gpt-image-2`**OpenAI)或 **`gemini-3.1-flash-image`**(Google)。如果模型能力正在拖累产出质量,应先升级驱动模型,而不是削弱工作流或质量要求。
## Kimi
<p align="center">
<a href="https://www.kimi.com/code/?aff=ppt-master"><img src="https://gcdn.moonshot.cn/growth-cdn/sponsor/kimi-zh.png" alt="Kimi" width="100%"></a>
</p>
感谢 [Kimi](https://www.kimi.com/code/?aff=ppt-master) 赞助 PPT Master。[Kimi K3](https://platform.kimi.com/docs/guide/kimi-k3-quickstart) 是全球首个开源 3T 级模型,拥有原生视觉能力与 100 万 Token 上下文。搭配 PPT MasterK3 可以理解 PDF、DOCX、网页等原始资料,梳理演示逻辑并生成原生可编辑的 PPTX。
**立即体验 [Kimi Code](https://www.kimi.com/code/?aff=ppt-master),或通过 Kimi 开放平台([中文站](https://platform.kimi.com?aff=ppt-master)[Global](https://platform.kimi.ai?aff=ppt-master))使用 API。**
## 模型接入合作伙伴
### PackyCode
<a href="https://www.packyapi.com/register?aff=ppt-master"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/packycode.png" alt="PackyCode" width="150"></a>
[PackyCode](https://www.packyapi.com/register?aff=ppt-master) 提供 Claude Code、Codex、Gemini 等服务的中转接入。通过专属链接注册,并在充值时填写优惠码 **`ppt-master`**,即可享受 9 折优惠。
### APIKEY.FUN
<a href="https://apikey.fun/register?aff=PPT-MASTER"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/apikey-fun.png" alt="APIKEY.FUN" width="150"></a>
[APIKEY.FUN](https://apikey.fun/register?aff=PPT-MASTER) 提供 Claude、OpenAI、Gemini 等主流模型的企业级接入服务。通过专属链接注册,最高可享永久充值 95 折优惠。
### RunAPI
<a href="https://runapi.co/register?aff=WMLJ"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/runapi.png" alt="RunAPI" width="150"></a>
[RunAPI](https://runapi.co/register?aff=WMLJ) 通过一个 API Key 提供 OpenAI、Claude、Gemini、DeepSeek、Grok 等 150+ 主流模型。通过专属链接注册并联系管理员,即可领取 **¥7 免费额度**。
### 优云智算
<a href="https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624"><img src="https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/sponsors/youyun.png" alt="优云智算" width="150"></a>
[优云智算](https://www.compshare.cn/coding-plan?ytag=GPU_YY-git_pptmaster0624) 是 UCloud 旗下 AI 云平台,提供国内外模型 API、CodingPlan 套餐、企业级并发、技术支持和开票服务。通过专属链接注册,最高可获得 **¥10 免费体验金**。平台还提供无需本地部署的 PPT Master Agent。
## 基础设施支持
<a href="https://m.do.co/c/547f129aabe1"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" alt="Powered by DigitalOcean" height="40"></a>
感谢 [DigitalOcean](https://m.do.co/c/547f129aabe1) 为项目基础设施提供支持。
## 个人赞助
如果 PPT Master 帮到了你,任何金额的个人赞助都能帮助项目持续更新、保持免费开源。
<a href="https://paypal.me/hugohe3"><img src="https://img.shields.io/badge/PayPal-赞助-00457C?style=for-the-badge&logo=paypal&logoColor=white" alt="通过 PayPal 赞助"></a>
[支付宝收款码](https://raw.githubusercontent.com/hugohe3/ppt-master/main/docs/assets/alipay-qr.jpg)
@@ -19,11 +19,11 @@ Global artifact ownership rules for PPT Master projects.
| `analysis/<stem>.slide_library.json` | Native PPTX structure facts | Text slots, geometry, native tables, native chart caches, SmartArt nodes/connections | Direct PPTX workflows use as native fill/structure contract |
| `analysis/image_analysis.csv` | Regenerated image fact view | Measured facts about the current `images/` folder | Re-run `analyze_images.py` before reading image facts after changes |
| `design_spec.md` | Strategist design authority | Human-readable design intent, complete page brief, rationale, resource plan, and confirmed production mechanics authored from the final confirmation plus source analysis | Strategist consumes the final confirmation once, writes and audits every confirmed field here, then later roles read this artifact instead of reopening `result.json`; after Gate 1, §IX is the Executor's page-content authority. |
| `spec_lock.md` | Execution anchor and routing contract | Machine-readable stable color/type roles, icons, images, page rhythm, charts, `template_reuse_scope`, and the route's PowerPoint structure mode; mirror/layout template routes additionally own input prototypes, the Master roster, and the complete page-to-Master/Layout mapping | Strategist authors the route-specific anchors from the audited Design Spec plus current project/page/template context. `page-context` repeats that compact anchor set per page and adds current-page routing values. Sparse page-local color/font garnish needs no lock row; a recurring semantic role or new adaptive Layout identity requires Strategist repair before reuse. |
| `project_manager.py page-context` stdout | Derived per-page context | Read-only model-facing anchor set + current-page delta + fingerprints for large references | Generate immediately before each page without `--bundle`; never edit or persist it as a replacement source of truth. `global` is the bounded repeated anchor set, not a whitelist of every valid page-local value. `reference_set` carries path/SHA/load policy for project/template Design Specs and selected prototype/chart SVGs, but never appends their payloads. The project Design Spec alone may carry `same_context_edit_policy` for verified targeted readback/rebind after the current main agent's own repair. |
| `analysis/page-context/P<NN>.usage.json` | Derived context telemetry | Actual compact page-context size plus hashes of owning inputs/references | `page-context --record-usage` deterministically replaces that page's snapshot; `page-context-report` summarizes current snapshots and unique references. Use token data to evaluate context cost, never as content or an execution contract. |
| `spec_lock.md` | Execution anchor and routing contract | Machine-readable stable color/type roles, icons, images, page rhythm, charts, `template_reuse_scope`, and the route's PowerPoint structure mode; mirror/layout template routes additionally own input prototypes, the Master roster, and the complete page-to-Master/Layout mapping | Strategist authors the route-specific anchors from the audited Design Spec plus current project/page/template context. Executor retains the complete lock once per valid execution context; local uncertainty consults that retained copy before the owning Design Spec fragment. Sparse page-local color/font garnish needs no lock row; a recurring semantic role or new adaptive Layout identity requires Strategist repair before reuse. |
| `project_manager.py page-context` stdout | Derived on-demand page context | Read-only model-facing anchor set + current-page delta + fingerprints for large references | Use only for explicit diagnostics/telemetry or an unresolved page/template/chart path-SHA projection. Never edit or persist it as a replacement source of truth, and never run it as a routine pre-page gate. `global` is a bounded anchor set, not a whitelist. `reference_set` carries path/SHA/load policy but never appends reference payloads. |
| `analysis/page-context/P<NN>.usage.json` | Derived optional context telemetry | Measured on-demand page-context size plus hashes of owning inputs/references | `page-context --record-usage` deterministically replaces only the invoked page's snapshot; `page-context-report` summarizes existing snapshots. Telemetry may be partial. Use token data to evaluate context cost, never as content or an execution contract. |
| `images/` | Runtime image pool | User, extracted, AI, web, formula, slice, EMF/WMF assets | Step 5 writes here; `analysis/image_analysis.csv` derives from current contents |
| `icons/` | Project icon inventory | Icons copied by `icon_sync.py` for this project | Executor uses locked project icons; exporter may fall back to global library only as documented |
| `icons/` | Prepared project icon pool | Bundled icons copied by `icon_sync.py` plus user-provided, template, imported, or custom icon SVGs | Executor may use any icon in this project-local pool; `spec_lock.icons.inventory` records planned bundled choices rather than an exhaustive whitelist. Exporter global fallback is legacy compatibility only. |
| `templates/` | Project template reference | Step 3 imported specs, template SVGs, and non-image assets | Strategist reads the template Design Spec and actual SVG roster during planning. Continuous Executor reuses that context; fresh Executor reads the Design Spec once and each selected complete SVG only before first use or after its SHA changes. |
| `templates/template_execution_manifest.json` (`v1`) + `templates/template_execution/*.text-slots.json` (`v2-min`) | Derived template index | Compact prototype/source-import summary plus per-prototype text-slot diagnostics; the sidecar integrity hash is tool-only | Materialization may publish these deterministic records, but page-context does not inject or require them and models do not read them during page authoring. The complete prototype SVG is the sole visual/template authority; never author from either JSON artifact. |
| `<import_workspace>/svg/` | Imported native-payload backing | Complete PPTX-derived metadata, hidden carriers, fallback evidence, and source structure | Keep immutable; create-template materialization may resolve a validated source ref against these files, but models do not edit or bulk-read them |
@@ -45,25 +45,27 @@ Always-loaded Executor authority for flat SVG page authoring and behavior shared
Before the first SVG page, output a confirmation listing: the compact communication objective, canvas dimensions, body font size, color scheme (primary/secondary/accent HEX), font plan, and the live-preview URL reported by the launcher. If the preview launch failed, state that failure before generating SVGs instead of silently proceeding. Prevents purpose/spec/execution drift.
### 2.1 Per-page execution context (Mandatory)
### 2.1 Execution context validity (Mandatory)
Before the first SVG, retain `design_spec.md`: continuous execution reuses planning context; fresh/resumed execution reads it once.
- **Valid**: if the exact complete Design Spec and lock remain in the unchanged, uncompacted active context, reuse both for every page. Do not reread or poll them.
- **Invalid**: fresh/resumed/restarted execution, compaction/summary-only recovery, or an external/unknown change requires one complete read of `design_spec.md`, then `spec_lock.md`, plus triggered references/template inputs. Mid-deck recovery also reads the latest completed SVG and, when images are used, current image metadata.
- **Uncertain**: consult the retained lock first, then only the owning Design Spec fragment; use sources only for facts. Design Spec remains upstream on conflict.
**Hard rule**: Before generating **each** SVG page, load its canonical current-page delta and record its model-facing size:
**On-demand page-context diagnostic**: only for explicit telemetry/debugging or an unresolved page/template/chart path-SHA question; never as a pre-page gate:
```bash
python3 skills/ppt-master/scripts/project_manager.py page-context <project_path> P<NN> --record-usage
python3 skills/ppt-master/scripts/project_manager.py page-context <project_path> P<NN> [--record-usage]
```
`global` deliberately repeats the sub-1000-token cross-page anchor set; `lock_source.sha256` binds its version. These anchors preserve identity and recurring semantics but do not enumerate every legal color or font. `page_context` is the current §IX/resource/template/chart delta. For every `reference_set` entry—project/template Design Spec or selected prototype/chart SVG—reuse an in-context path + SHA; read it once only when absent or changed.
Consume stdout directly; stop on non-zero exit. The projection is derived, not authoritative. Use `--record-usage` only for measurement.
**Hard rule — known same-context Design Spec repair**: When the current main agent returns to Strategist and authors an exact project `design_spec.md` repair from its retained state, the `design-spec` reference's `same_context_edit_policy: targeted-readback-and-rebind` avoids a full reread. This applies only to bounded repairs that keep the page roster, narrative order, confirmed identity, and communication contract unchanged. Repair the owning Design Spec headings/page blocks first, re-author only affected lock rows, read back those changed fragments, run `project_manager.py validate`, then rerun `page-context` for every affected page. If each projected brief/global view matches the intended repair, bind the retained whole-document understanding plus verified delta to the new Design Spec SHA. A fresh context, an external/unknown edit, a roster/global-contract change, an unexpected diff, failed validation, or mismatched projection requires one full Design Spec read before continuing.
**Same-context repair**: in a valid uncompacted context, a bounded repair that preserves roster/order/identity/communication needs only affected Design Spec/lock fragment readback plus `project_manager.py validate`. Any broader or invalid-context repair requires the complete reads above.
**Hard rule — exact page roster**: `design_spec.md §IX` is the ordered queue: one final slide per entry, with the same id/order. The UI range no longer applies. Never add, drop, merge, split, or reorder; repair/reconfirm the Design Spec first.
**Hard rule — selection vs realization**: use Strategist-selected content, resources/paths, chart/layout keys, core fonts, palette anchors, icons, and crop boundaries. Adapt realization, never selection, except sparse local font/color garnish allowed below. Missing or unresolved material stops execution and returns to Strategist-owned acquisition/failure recovery; never search, generate, download, sync, invent, or substitute it. Selection changes require upstream repair.
Use named lock roles literally when that role applies, and use optional `Template Application` from the retained Design Spec. Choose contextual page-local values from the Design Spec, style, content, and current composition rather than forcing every object into a lock row. The delta overrides neither facts nor constraints. After an approved change, rerun the command; reload changed references except the verified same-context Design Spec repair above. Deprecated `--bundle` is a compatibility no-op.
Use named lock roles literally when that role applies, and use optional `Template Application` from the retained Design Spec. Choose contextual page-local values from the Design Spec, style, content, and current composition rather than forcing every object into a lock row. A page-context delta overrides neither facts nor constraints. Deprecated `page-context --bundle` is a compatibility no-op.
**Source verification**: §IX owns the complete page brief; the page delta does not carry the source corpus. Read sources only to resolve listed `Fact IDs` or verify required claims, quotes, names, or data. Do not add facts, claims, or selected content. Return underspecified blocks for Design Spec repair.
@@ -87,23 +89,24 @@ Use named lock roles literally when that role applies, and use optional `Templat
> Note: block-level phrasing, applied *within* the page's `page_rhythm` density (below), not against it.
**Missing `spec_lock.md` or `design_spec.md`** → stop before drawing and report the missing gate artifact. Recover through [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §3; do not bypass a failed page-context command or silently downgrade.
**Missing `spec_lock.md` or `design_spec.md`** → stop before drawing and report the missing gate artifact. Recover through [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §3; do not silently downgrade. A failed on-demand `page-context` diagnostic is also not evidence that a required planning artifact may be bypassed.
**Missing field in an existing lock**: follow [`failure-recovery.md`](../workflows/governance/failure-recovery.md) §2.
**Execution anchors and contextual values**:
- Icons MUST come from `icons.inventory`; library MUST equal `icons.library`
- Icons may use any SVG already prepared under `<project_path>/icons/`. `icons.library` records the Strategist's primary bundled style choice and `icons.inventory` records its planned selection; neither is an execution whitelist over project-local assets.
- Core color roles retain their meaning. Derive tints, shades, alpha, gradients, and effects; preserve natural asset colors; and use sparse page-local accents for differentiation/ornament. They must not become a competing or recurring palette.
- Resolve structural families by role: exact `<role>_family` first, then `title_family` for title roles or `body_family` for other unoverridden roles, then legacy `font_family`. Never flatten declared role overrides. A sparse export-safe accent family may style short non-structural display/ornament only—never title/body/data/annotation. Recurrence requires upstream selection.
- Font sizes use the named `typography` role values as deck-wide anchors. Map every structural or feature text item to a declared role before drawing; never inherit a template placeholder size. Start from the anchor, then use composition and content fit to adjust that occurrence by at most `±2`px. Keep same-page peers consistent and preserve the role hierarchy; bounded adjustment does not create a new role.
- Font sizes use the named `typography` role values as deck-wide anchors. Map every structural text item to a declared role before drawing; never inherit a template placeholder size. Start from the anchor, then use composition and content fit to adjust that occurrence by at most `±2`px. Keep same-page peers consistent and preserve the role hierarchy; bounded adjustment does not create a new role.
- **Core message ≥ `body`**: map the page's primary claim to declared `lead` / `subtitle`, never below the current body treatment. Footnotes, page numbers, and credits use declared `footnote` / `annotation`; do not invent a smaller role.
- **Write unitless px, with at most two decimals.** Use only the mapped role's anchor or a value within its `±2`px band; do not substitute familiar pt-style numbers or emit long precision tails.
- **Outside-band recovery**: reflow geometry and use the declared role band locally. If the page needs a new semantic role, a size outside anchor `±2`px, or a hierarchy change, stop and return to Strategist to repair the Design Spec and `spec_lock.md`, then regenerate page-context. Never flatten a justified distinction or add a role merely to silence the checker. Generated `svg_output/` values outside every declared band are blocking errors; mirror pages preserve exact source typography as inherited input.
- **Write unitless px, with at most two decimals.** Structural and mapped-role text uses only its anchor or a value within its `±2`px band; the sparse display-size exception is defined separately below. Do not substitute familiar pt-style numbers or emit long precision tails.
- **Sparse display-size exception**: a short non-structural Hero/Display element may use one undeclared size outside all anchor bands at most twice across the deck without a lock row. The third occurrence makes that size recurring: stop and return to Strategist to name the role in the Design Spec and `spec_lock.md`, then read back and validate the affected fragments before reuse. This exception never applies to titles, body copy, subtitles, annotations, footnotes, captions, data labels, or card copy, and nearby sizes must not be introduced to imitate one recurring treatment.
- **Outside-band recovery**: for structural text, reflow geometry and use the declared role band locally. For a sparse display occurrence, keep the unitless value and verify that its deck-wide count remains at most two. Never flatten a justified distinction or add a role merely to silence the checker. Mirror pages preserve exact source typography as inherited input.
- Images MUST reference files listed under `images`; no invented filenames
- Formula PNGs are images with `Acquire Via: formula`; place a `Rendered` file only from its listed path, use the normal placeholder for `Needs-Manual`, and never recreate the formula as text.
Return upstream before any derived/accent identity becomes recurring or structural, or before typography needs a new semantic role or an outside-band size, then regenerate context. Local garnish and same-role `±2`px adjustments need no lock row. Never expand the lock to silence a comparison. Icons, images, structural fonts, role anchors, and resources keep their inventory/role rules.
Return upstream before any derived/accent identity becomes recurring or structural, or when an undeclared display size reaches its third occurrence, then update the retained context under §2.1. Local garnish, same-role `±2`px adjustments, and at most two sparse display-size occurrences need no lock row. Never expand the lock to silence a comparison. New icon acquisition, images, structural fonts, role anchors, and resources keep their preparation/role rules.
**Per-page layout rhythm — `page_rhythm` section**:
@@ -115,7 +118,7 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P<NN>`
| `dense` | Information-heavy. Card grids, multi-column layouts, KPI dashboards, tables, and charts are all permitted. This is the baseline behavior. |
| `breathing` | Low-density impact page. Avoid **multi-card grid layouts** — do not organize content as multiple parallel rounded containers (3-card row, 4-card KPI grid, 2×2 matrix rendered as cards). Use naked text blocks, dividers, whitespace, or full-bleed imagery as the content structure. Single rounded visual elements (hero image corners, callouts, tags, one emphasis block) are fine — the rule is about grid structure, not about the `rx` attribute. Proportions follow information weight (not a preset ratio). Typical forms: hero quote, single large number with one-line interpretation, full-bleed image with floating caption, section transition. |
> Without rhythm variation, every page defaults to card grids (the "AI-generated" look). `page_rhythm` is the only narrative lever that survives context compression.
> Without rhythm variation, every page defaults to card grids (the "AI-generated" look). Context recovery follows §2.1.
**Missing or empty `page_rhythm` section — fixed compatibility default** → emit `warning: spec_lock.md missing/empty page_rhythm — defaulting all pages to dense` once, fall back to `dense` for all pages.
@@ -128,6 +131,7 @@ Before drawing each page, look up its entry in `page_rhythm` (key format `P<NN>`
- **Proximity**: group related elements with tight spacing; separate unrelated groups
- **Element grouping (Mandatory)**: wrap each logical Slide-local body unit in a descriptive, page-unique top-level `<g id>`. Every visible direct root `<g>` declares root-coordinate `data-pptx-bounds="x y width height"`; frame/native coordinates do not replace it, and placeholder bounds also supply the slot frame. Nested groups need no bounds and any such values are ignored. Checker compares root bounds with the `viewBox` and recursively checks only estimable text against its root module: through `1px` is ignored, through `5%` warns, above `5%` fails per side. Images, shapes, paths, `<use>`, effects, and object frames remain geometrically free. Flat pages use ordinary groups; structured slots already qualify, while titles, direct Master/Layout atoms, and canvas-level static framing may remain root primitives. On flat pages, give a root background image or full-canvas scrim/decoration rectangle a stable `id` plus `data-pptx-role="background"` / `"decoration"`; never wrap it only to silence the advisory.
- **Default — size `data-pptx-bounds` as the intended module zone, not a glyph box (may skip when no text is estimable)**: an untransformed line spans `y - 0.85 × font_size` to `y + 0.35 × font_size`, and per-character width estimates undercount CJK—most heavily on serif stacks—so leave headroom. If text does not fit, reflow or adapt; correct bounds only when that zone was recorded incorrectly. Larger bounds do not repair off-canvas text.
- **Spec adherence**: follow color, layout, canvas format, and typography in the spec
- **Template structure**: inherit the native visual framework only for `template_reuse_scope: mirror|layout`; `style` uses the flat route
- **Main-agent ownership**: SVG generation must run in the main agent (not sub-agents) — pages share upstream context for cross-page visual continuity
@@ -196,11 +200,11 @@ Examples: `01_封面.svg` / `02_目录.svg` / `03_核心优势.svg`; `01_cover.s
## 4. Icon Usage
Strategist chooses the library and inventory; Executor only implements. Library details and one-library rule: [`../templates/icons/README.md`](../templates/icons/README.md). This section defines placeholder syntax.
Strategist chooses at most one primary bundled stylistic library and may select `simple-icons` alone or alongside it; Executor implements from the complete prepared project-local pool. Library details and selection rules: [`../templates/icons/README.md`](../templates/icons/README.md). This section defines placeholder syntax.
> **Resolution is project-first.** Strategist copied the chosen icons into `<project_path>/icons/<lib>/` (via `icon_sync.py`); `finalize_svg.py embed-icons` embeds from there, falling back to the global library per-icon. Custom SVGs must already exist in the prepared project inventory under `<project_path>/icons/<lib>/`. Reference only icons in `spec_lock.md icons.inventory`.
> **Prepared-project boundary.** Any SVG already under `<project_path>/icons/<lib>/` is valid execution material, whether selected from a bundled library or supplied by the user, a template, or an import workflow. New authoring must resolve there. The global fallback in `finalize_svg.py embed-icons` is legacy compatibility, not permission for Executor to discover or use an unprepared global icon.
> **Icon identifiers are case-sensitive filenames.** For bundled libraries, copy the verified lowercase basename exactly (`tabler-outline/award`, never `tabler-outline/Award`) into `spec_lock.md` and every `data-icon` value. Custom icon identifiers preserve the custom file's exact case; the pipeline never silently lowercases names.
> **Icon identifiers are case-sensitive filenames.** Every `data-icon` value must use the exact project-local relative basename (`tabler-outline/award`, never `tabler-outline/Award`). Strategist records its planned bundled choices in `spec_lock.md`; Executor need not add other already-prepared project-local icons to that inventory. Custom identifiers preserve the custom file's exact case; the pipeline never silently lowercases names.
**Built-in icons — Placeholder method (recommended)**:
@@ -233,14 +237,14 @@ Strategist chooses the library and inventory; Executor only implements. Library
>
> Icons are auto-embedded by `finalize_svg.py` — no need to run `embed_icons.py` manually.
**Locked-id verification only**: verify the exact project-local file already named in `icons.inventory`:
**Project-local verification**: verify the exact prepared file before use:
```bash
test -f "<project_path>/icons/<lib>/<name>.svg"
```
**Missing locked icon** → return to Strategist's inventory / `icon_sync.py` gate. Do not search the global library, select an alternative, copy a candidate, or edit the lock in Executor.
**Missing project-local icon** → return to Strategist's preparation / `icon_sync.py` gate. Do not search the global library, select an alternative, or copy a candidate in Executor.
**Hard rule — icon inventory**: use only the Design Spec's approved inventory. Mixing stylistic libraries within one deck is FORBIDDEN.
**Hard rule — prepared assets**: Executor may freely combine project-local icons, regardless of namespace or style. It may not acquire a new icon or treat a globally resolvable file as prepared material.
---
@@ -250,7 +254,7 @@ Structural typography anchors come from `spec_lock.md typography`. Use an exact
**Missing required field — `typography.font_family`** → stop and return to Generate Step 4 / [`strategist.md`](strategist.md) §6.2 to repair `spec_lock.md`; do not infer a stack from `design_spec.md`.
**Hard rule**: every SVG `font-family` stack MUST resolve to pre-installed exported Latin / EA typefaces (Microsoft YaHei / SimHei / SimSun / Arial / Calibri / Segoe UI / Times New Roman / Georgia / Consolas / Courier New / Impact / Arial Black). PPTX has no runtime fallback — missing fonts degrade to Calibri.
**Hard rule**: every SVG `font-family` stack MUST resolve to a pre-installed exported Latin / EA typeface; use the Strategist §g safe set for locked roles and §2.1 for sparse display exceptions. PPTX has no runtime fallback — missing fonts degrade to Calibri.
---
@@ -12,9 +12,9 @@ For each selected `templates/charts/<key>.svg`, use its Skill-relative `referenc
**Per-page chart reference — `page_charts` section**:
Before drawing each page, look up its entry in `page_charts` to decide which chart structure applies (the SVG itself was loaded in §1):
Before drawing each page, look up whether `page_charts` supplies a page-local reference:
- Entry present (e.g., `P09: timeline_horizontal`) → adapt the corresponding chart SVG already in context under §3; do not copy it verbatim. Use the selected §VII row and SVG; do not load the full chart catalog during execution.
- Entry present (e.g., `P09: timeline_horizontal`) → read its §VII Usage and SVG for that page only; realize §IX without copying it or loading the catalog.
- No entry for this page → no catalog reference was selected. Follow the current §IX `Visualization` / `Layout`; design any declared custom visualization from scratch without inventing a §VII reference.
- Whole section absent → no catalog references were selected; §IX may still contain custom data charts or tables.
@@ -108,21 +108,22 @@ rg -n 'data-pptx-replace-with="(chart|table)"|<metadata type="application/json">
## 3. Visualization Reference
Chart SVGs referenced in **VII. Visualization Reference List** are loaded once through §1. This section governs adaptation only.
§1 loads the `page_charts` SVG. Usage gives page-local intent; §IX remains authoritative. Legacy wider rows keep Usage and ignore path/summary fields.
**Hard rule**: adapt the loaded chart SVG; do not improvise from memory and do not replicate verbatim. Apply the active Design Spec and `spec_lock.md`; preserve the visualization type and data semantics.
**Hard rule**: treat the loaded SVG as a page-local reference, not a required base. §IX and source data own the final information structure; never replicate the preview verbatim.
**Adaptation rules**:
- **Preserve**: visualization type (bar/line/pie/timeline/process/framework…), information relationships, data encoding, and every content obligation in the active Design Spec
- **Selected key, flexible realization**: keep the Strategist-selected chart key/type; its preview grouping, frame count, item count, capacity, and geometry are adaptable to the actual authored content
- **Preserve**: planned information relationships, data encoding, and every content obligation in the active Design Spec
- **Page-local only**: a reference row applies only to its mapped page; never spread it across the deck
- **Flexible realization**: borrow, recombine, or depart from the preview's type and geometry when the current page is better served another way
- **Carry forward**: every planned label, value, unit, status, source, and explanatory block; never shorten or drop content to imitate a lighter catalog preview
- **Adapt**: project data and labels, dimensions, axes, legend, and spacing as the authored content requires
- **Project-owned**: palette, typography, container treatment, effects, background, and page chrome; catalog preview values are fallbacks, never defaults
- **Bound final body modules**: add or revise root-coordinate `data-pptx-bounds` on every visible direct root `<g>` copied into the final page; nested groups need none, chart geometry and local references are not content-boundary inputs, and catalog reference warnings never waive the final-page contract
- **Adjust with fidelity**: composition, axis ranges, grouping, and grid may change when the actual content, relationships, hierarchy, and data encoding remain complete
- **Forbidden**: changing visualization type without spec justification; omitting planned data points, labels, relationships, or explanatory content to fit the catalog preview
- **Forbidden**: treating preview structure as the page specification; omitting planned data points, labels, relationships, or explanatory content to fit it
> Templates: `templates/charts/`. The Strategist's selected key is already locked in `page_charts`; execution opens only that key's SVG.
> Templates: `templates/charts/`. `page_charts` maps one optional reference to one page; execution opens only that SVG.
### 3.1 Chart Coordinate Calibration
@@ -25,6 +25,8 @@ Handle images by their status in the Design Spec's Image Resource List. Status e
**Mandatory — selected pattern, flexible realization**: Read [`image-layout-patterns.md`](./image-layout-patterns.md) once when this branch loads, then resolve every primary/modifier id in each active §VIII/lock row to its exact catalog entry. Preserve the selected semantic composition. Adapt geometry, ratio, placement, spacing, and hierarchy for the actual page; never replace the pattern, role, file/source, must-use, or crop policy downstream. If another pattern is needed, return upstream to update the Design Spec. Only explicit user/template preservation locks exact geometry. Avoid generic left/right repetition.
**No semantic re-reading**: §VIII owns image identity, purpose, and focus / crop constraints. Executor uses its `Reference` plus regenerated dimensions; it never opens source images to rediscover subjects, substitute assets, or invent focus. For `adaptive` without reliable focus, use `meet`; missing or contradictory required constraints return upstream.
**Placeholder**: Dashed border `<rect stroke-dasharray="8,4" .../>` + description text
**Crop policy**: read the §VIII row and matching lock projection. `crop=no-crop` (or a legacy trailing `| no-crop`) requires a native-ratio container and `preserveAspectRatio="xMidYMid meet"`. `crop=adaptive` permits but never requires cropping; choose `meet` or focal-safe `slice` from purpose, ratio, focus, and container. A missing or conflicting projection returns upstream instead of being inferred during execution.
@@ -12,17 +12,17 @@ Conditional Executor authority for `template_reuse_scope: mirror|layout` with `p
| Context | Load policy |
|---|---|
| `templates/design_spec.md` | Strategist reads once; continuous Executor reuses it, fresh Executor reads it once |
| Current page delta | Run [`executor-base.md`](./executor-base.md) §2.1 immediately before that page |
| Selected prototype SVG | Read once on an absent/changed `reference_set` path + SHA; otherwise reuse it |
| `templates/design_spec.md` | Reuse it in a valid active context; after context invalidation, read it once with the project planning artifacts |
| Current page mapping | Read the retained `spec_lock.md page_layouts` row; a page change does not require another file load |
| Selected prototype SVG | Read the complete `templates/<basename>.svg` once per valid context and reuse it until a known change or context invalidation |
**Hard rule**: Page-context carries no prototype payload. `reference_set` identifies the authoritative complete SVG; never author from a roster, manifest, sidecar, filename, or summary alone.
**Hard rule**: The complete prototype SVG is authoritative. An on-demand page-context result may fingerprint it but carries no prototype payload; never author from a roster, manifest, sidecar, filename, or summary alone.
Manifest/text-slot files are derived tool metadata, not model inputs. Missing metadata neither invalidates a legacy workspace nor permits text-topology changes.
**Mapping change**: stop and return to Strategist to update the owning plan; regenerate that page's delta before resuming, then load only a new/changed prototype fingerprint.
**Mapping change**: stop and return to Strategist to update the owning plan, read back and validate the affected planning fragments, then load the new prototype before resuming.
Resolve the per-page template SVG from `page_context.template.prototype`; the owning `spec_lock.md page_layouts` row remains authoritative. There is no filename/page-type fallback.
Resolve the per-page template SVG directly from the owning `spec_lock.md page_layouts` row. There is no filename/page-type fallback.
**Resolution order (per page):**
@@ -34,17 +34,17 @@ Resolve the per-page template SVG from `page_context.template.prototype`; the ow
**Default — re-skin `layout` (may override when the application plan keeps template visuals and the lock reflects them)**: inherit geometry, label/legend placement, and series encoding; otherwise repaint template gradients, shadows, fills, and strokes from the current style/lock. Template font sizes remain placeholders. `mirror` preserves visuals under §1.1.
**Font size is skin, not geometry (non-mirror).** A chart / layout template's hardcoded `font-size` values (often 1116px, sized for the template's own dense placeholder text) are NOT inherited. Classify each text into its `spec_lock.md` role, start from that role's anchor, and keep any contextual adjustment within anchor `±2`px. The template's placeholder px never becomes the starting point or an extra role.
**Font size is skin, not geometry (non-mirror).** A chart / layout template's hardcoded `font-size` values (often 1116px, sized for the template's own dense placeholder text) are NOT inherited. Structural text and reusable slots start from their `spec_lock.md` role and stay within anchor `±2`px; only a qualifying Slide-local Hero/Display element follows the sparse exception in `executor-base.md`. Template placeholder px supplies neither a role anchor nor a sparse display value.
**Typography execution order (mandatory):**
1. Build a per-page text inventory from `design_spec.md §IX` + the current `notes/<NN>_*.md`.
2. Classify each text item before drawing. **Structural and feature roles** (`title`, `subtitle` / `lead`, `body`, `annotation`, `footnote` / `page_number`, hero or emphasis slots) map to a declared `spec_lock.typography` size role. A missing semantic role returns upstream; do not borrow an unrelated size because it is numerically close.
3. Choose the role anchor or one contextual value within anchor `±2`px before placing the text. Never start from a template `font-size` and then adjust it.
2. Classify each text item before drawing. **Structural roles and reusable feature slots** (`title`, `subtitle` / `lead`, `body`, `annotation`, `footnote` / `page_number`, reusable hero or emphasis slots) map to a declared `spec_lock.typography` size role. A missing semantic role returns upstream; do not borrow an unrelated size because it is numerically close. Only a Slide-local, non-slot Hero/Display element may use the sparse-size exception in [`executor-base.md`](./executor-base.md); reusable Layout slots never do.
3. For every mapped role or reusable slot, choose the role anchor or one contextual value within anchor `±2`px before placing the text. A qualifying Slide-local sparse display follows `executor-base.md` directly. Never start from a template `font-size` and then adjust it.
4. Layout from those chosen sizes: compute line-height, wrapped line count, child `y` / `dy`, card padding, card height, column gaps, and available image/chart area.
5. Reflow containers and local geometry together with the bounded role treatment; an inherited template slot never justifies leaving the declared band.
**Geometry and bounded type co-adapt**: widen or heighten the card, open spacing, recompute child `y` / `dy`, and choose within the mapped role's anchor `±2`px instead of inheriting the template's compact size. Page count and density remain the confirmed Strategist decision: do not repaginate, split, or drop content. If the page still needs a value outside the band, return upstream under [`executor-base.md`](./executor-base.md) §2.1. Mirror instead preserves source typography under §1.1.
**Geometry and bounded type co-adapt**: widen or heighten the card, open spacing, recompute child `y` / `dy`, and choose within the mapped role's anchor `±2`px instead of inheriting the template's compact size. Page count and density remain the confirmed Strategist decision: do not repaginate, split, or drop content. If structural text or a reusable slot still needs a value outside the band, return upstream under [`executor-base.md`](./executor-base.md) §2.1; only qualifying Slide-local display text uses its sparse exception. Mirror instead preserves source typography under §1.1.
### 1.1 Mirror reuse — literal page replacement
@@ -58,7 +58,7 @@ When `spec_lock.md` records the AI-derived `template_reuse_scope: mirror`, Execu
6. **Visible text editing** — mirror SVGs may keep literal source text rather than `{{...}}` authoring markers. Edit values in place while retaining imported semantic `data-pptx-placeholder` identity and exact text topology.
7. **Output filename** — follow the standard project SVG naming convention (`<NN>_<page_name>.svg` where `<NN>` matches the project page index, not the mirror source index). The mirror filename is the *reference*, not the *output*.
**Detecting mirror mode**: read `page_context.template.reuse_scope` from the current page delta. `replication_mode: mirror` in the installed template only determines whether that derived scope is legal; it must never force mirror behavior when the lock records `layout` or `style`.
**Detecting mirror mode**: read `template_reuse_scope` from the retained lock. `replication_mode: mirror` in the installed template only determines whether that scope is legal; it must never force mirror behavior when the lock records `layout` or `style`.
**Mirror + chart pages**: chart structures inside a mirror SVG are already drawn (axis, series, labels). Treat them as visual references — replace the data labels and series text content to match the project's chart spec, but do not redraw the chart from a `templates/charts/<name>.svg` baseline. A mirror template's `page_charts` entries are normally absent for this reason.
@@ -126,7 +126,7 @@ The exporter writes these solid fills as real Master/Layout/Slide `p:bg`, not se
**Per-page template lookup — `page_layouts` section (`mirror` / `layout` only)**:
Before drawing each page, use `page_context.template.prototype` to identify the inherited basename. Its matching `reference_set` entry supplies the complete SVG's path and SHA; §1.0 owns whether that file must be read or can be reused from the active context:
Before drawing each page, use its retained `spec_lock.md page_layouts` row to identify the inherited basename. Resolve the complete SVG from the selected template directory; §1.0 owns whether that file must be read or can be reused from the active context. An on-demand `reference_set` fingerprint may diagnose an uncertain path/SHA but is not required for normal lookup:
- Entry present (e.g., `P04: 03a_content_image_text`) → inherit the corresponding full SVG. The basename **must match** an actual file in the chosen template directory. If it does not, stop before drawing and report the invalid mapping; neither `strict` nor `adaptive` may fall back to free design inside a structured template deck.
- No entry for this page with `template_reuse_scope: mirror|layout` → stop before drawing and report the missing Strategist mapping. Adaptive mode still requires one selected complete template SVG; flexibility applies to the post-design output Layout, not to whether an input prototype exists.
@@ -142,7 +142,7 @@ Do **not** invent a prototype entry, and do **not** assume a structured template
- Read the current page assignment as `P<NN>: <layout_key>`. Resolve the assigned Layout key in `pptx_layouts`, then resolve its Master key in `pptx_masters`. Missing, malformed, or partial mappings stop before drawing.
- Write matching root Master/Layout key and picker names. Do not write `data-pptx-layout-kind` or `data-pptx-page-role`.
- On strict template use, the row and SVG contract match the selected prototype exactly.
- On adaptive template use, retain the prototype Master and realize the Layout key/name already declared for this page. If construction proves that fixed Layout atoms or slot topology/bounds must change, stop before completing the page and return to Strategist to declare the revised definition and assignment; regenerate the current page context before resuming.
- On adaptive template use, retain the prototype Master and realize the Layout key/name already declared for this page. If construction proves that fixed Layout atoms or slot topology/bounds must change, stop before completing the page and return to Strategist to declare, read back, and validate the revised definition and assignment before resuming.
- A Layout key may repeat across non-adjacent pages only when its fixed atoms and slot contracts are identical.
**Structured template-page scaffold**:
@@ -126,10 +126,4 @@ Executor does NOT invoke `image_gen.py` / `image_search.py` / `slice_images.py`.
## 10. Task Completion Checkpoint
```markdown
## ✅ Image Acquisition Phase Complete
- [x] {N} rows processed (`ai`: {a} / `web`: {b} / `slice`: {s})
- [x] {a} `Generated`, {b} `Sourced`, {s} sliced `Generated`, {c} `Needs-Manual`
- [x] image_prompts.json / image_sources.json written
- [ ] **Next**: Auto-proceed to Executor phase
```
Verify internally that every row was processed, all triggered manifests/sidecars were written, and each result is `Generated`, `Sourced`, or `Needs-Manual`. Do not print a checklist. On success, auto-proceed to Executor and emit at most one compact status line when useful; on failure, report only the blocking rows and required recovery.
@@ -24,7 +24,7 @@ Every new SVG project declares one deterministic route. Free-design, brand-only,
**Project lock**: A Master row is `<master_key>: <PowerPoint picker name>`. A unique Layout row is `<layout_key>: <master_key> | <PowerPoint picker name> | <prototype source>`, where the source is a generated `P<NN>` or installed `template:<basename>`. A page assignment is `P<NN>: <layout_key>` under `page_pptx_layouts`. The SVG root values MUST match the assigned definition. A Layout key belongs to exactly one Master and must be globally unique. Reuse one key only when prototypes share identical ordered Layout atoms and slot ids/types/effective indices/default bounds/binding modes. An unused Layout uses a template SVG source and remains registered without a published carrier slide. Every structured route requires numeric `spec_lock.md` typography `title` / `body` rows.
**Template behavior**: Strict preserves the selected prototype's declared Master/Layout/slot contract. Adaptive retains its Master and realizes the current or new Layout key/name declared by Strategist. A construction-discovered change to fixed Layout atoms or slot topology/bounds returns upstream for plan/lock repair and regenerated page context before authoring resumes. Mirror-created prototypes preserve validated source identity, literal paint, typography, effects, atomic geometry, and referenced assets in a new workspace. `standard` / `fidelity` never make source topology authoritative; mirror does not synthesize a replacement topology or fill missing facts.
**Template behavior**: Strict preserves the selected prototype's declared Master/Layout/slot contract. Adaptive retains its Master and realizes the current or new Layout key/name declared by Strategist. A construction-discovered change to fixed Layout atoms or slot topology/bounds returns upstream for plan/lock repair, readback, and validation before authoring resumes. Mirror-created prototypes preserve validated source identity, literal paint, typography, effects, atomic geometry, and referenced assets in a new workspace. `standard` / `fidelity` never make source topology authoritative; mirror does not synthesize a replacement topology or fill missing facts.
Imported inherited-shape visibility remains an immutable analysis fact until a
structured mirror is materialized. The final mirror root carries that fact with
@@ -14,6 +14,8 @@ Before Stage 2, use proposed sources only for candidate construction. After conf
For illustration, apply this precedence: confirmed `none` → explicit user intent → the locked visual style's `Illus.` propensity (`core` / `supportive` / `sparse`) → none. Propensity controls the lean, not the source or a page quota. When illustration is active, prefer one coherent motif family across hero/section anchors and local spots, but only when the confirmed assets can form that family.
**Context-first understanding for provided assets**: Do not visually scan `images/`. First infer identity, role, and crop / focus needs from source position and surrounding prose, captions / alt / titles, filename, user notes / confirmed `image_notes`, existing resource records, and CSV geometry. Inspect only one specific image when a remaining ambiguity would change selection, factual identity, page role, crop safety, or focal placement. Never inspect for inspiration, bulk-open the folder, or infer external facts / provenance from pixels. Record the result in §VIII. Leave an optional unresolved asset unused; route an unresolved must-use asset through failure recovery.
For ≥3 AI-generated same-family spots, plan one unplaced `ai` Illustration Sheet row plus one placed `slice` row per used element; only slice rows enter `spec_lock.md images`. State the intended placement shape family in the sheet reference and use separate sheets for incompatible shapes. [`image-generator.md`](./image-generator.md) §4.3 owns grid, ratio, slicing, and execution details. Stage 3 chooses the AI execution path under `image-generator.md` §7; do not pre-empt or re-pick it here.
## 2. AI Image Strategy — propose before Stage 2; lock only for confirmed `ai`
@@ -22,7 +24,7 @@ When proposed sources include `ai`, read [`image-renderings/_index.md`](./image-
Also write one `custom_candidates.image_strategy` under the Confirm UI contract: localized `name` / `visual` / `mood`, `rendering: custom`, and non-empty localized `behavior` satisfying the catalog grammar. If it combines or borrows existing renderings, name every exact id in the visible proposal and read every corresponding `image-renderings/<id>.md` before writing the synthesis. If it is genuinely novel, read no preset file and name no catalog basis. Keep it unselected unless the user supplied it (`recommend.image_strategy: custom`); under a template it obeys inherited identity and application. Only a selected custom locks its edited behavior as `image_rendering_behavior`; when catalog material is actually used, also project the exact ids as `image_rendering_references`, otherwise omit that field. Discard an unselected candidate downstream. Ignore legacy `image_palette`.
For specialized or regulated paper-figure subjects, preserve the prompt depth required by [`image-generator.md`](./image-generator.md) §4.2 rather than shortening to a generic brief. Scan the outline for genuine image-led pages, list the proposed hero pages in Stage-2 `image_notes` so the user can retain, edit, or remove them in the same confirmation, then mark only the confirmed pages' AI rows `page_role: hero_page`; local is the default. `text_policy: embedded` is reserved for lettering that must be fused into the artwork; ordinary titles, data, labels, and prose remain editable SVG. Analyze confirmed provided assets before writing §VIII.
For specialized or regulated paper-figure subjects, preserve the prompt depth required by [`image-generator.md`](./image-generator.md) §4.2 rather than shortening to a generic brief. Scan the outline for genuine image-led pages, list the proposed hero pages in Stage-2 `image_notes` so the user can retain, edit, or remove them in the same confirmation, then mark only the confirmed pages' AI rows `page_role: hero_page`; local is the default. `text_policy: embedded` is reserved for lettering that must be fused into the artwork; ordinary titles, data, labels, and prose remain editable SVG. Resolve confirmed provided assets through the context-first boundary above before writing §VIII.
## 3. Formula Asset Policy
@@ -49,6 +51,8 @@ Follow `latex_render.py --help` for the manifest fields. The renderer writes dim
Add §VIII rows for the image resources actually planned from the confirmed source boundary and for every selected formula; a formula-only plan contains only formula rows. A permitted but unused source needs no row. Author each row's filename, dimensions/ratio, layout pattern, crop policy, purpose/type, acquisition, status, reference, and conditional AI fields as part of the complete Design Spec. `Acquire Via` is `ai`, `web`, `user`, `formula`, `placeholder`, or `slice`; status follows [`svg-image-embedding.md`](./svg-image-embedding.md). When a planned or explicitly required asset is not yet available, retain its row as `Pending` or `Needs-Manual`; never remove the row or change `Acquire Via` to make the Design Spec look complete. After §VIII passes final confirmation, project every placed row into `spec_lock.md images` as `<path> | source=<Acquire Via> | pattern=<Layout pattern> | crop=<adaptive|no-crop>` and omit unplaced Illustration Sheets. References describe visual intent: AI uses subject + intent + composition without repeating rendering or HEX; web uses a concrete subject plus a few positive quality descriptors; formula preserves the source LaTeX and placement intent.
**Prepared-user fast path**: For initial imported or user-supplied assets confirmed as `provided`, copy the exact `Filename` basename and derive `Dimensions` / `Ratio` from that row's `Width` / `Height` / `AspectRatio` in the latest `analysis/image_analysis.csv`; drop source-side directories, set `Acquire Via: user` and `Status: Existing`, and decide the remaining §VIII fields normally. Existing §VIII / lock / provenance-manifest records override this inference. Assets declared as `ai`, `web`, `slice`, `formula`, or manual fulfillment retain that provenance and advance through their own status lifecycle after entering `images/`; location never reclassifies them as `user / Existing`.
🚧 **GATE — non-formula rows**: read every entry in [`image-layout-patterns.md`](./image-layout-patterns.md). Copy one primary `#<id> <name>` plus any modifier names verbatim into each row; no empty, paraphrased, or invented ids. Strategist owns that pattern selection; Executor adapts its geometry while retaining the selected primary/modifier semantics, resource role, and explicit constraints. Audit the completed column against page intent: repeated left/right or top/bottom structures are valid when the narrative calls for them, but catalog families and modifiers must remain available without a usage quota.
Choose narrative intent before dimensions: hero/full-bleed, atmosphere/background, side-by-side, or accent/inline. Portrait and multi-image calculations belong to [`image-layout-spec.md`](./image-layout-spec.md). Write `Crop Policy: no-crop` whenever cropping could remove required pixels, labels, evidence, identity, or edge content; screenshots, charts, certificates/contracts, dense diagrams, logos, product markings, and formulas are common triggers rather than an exhaustive list. Otherwise write `Crop Policy: adaptive`: Executor may use complete display or a focal-safe crop, and the value never commands cropping. Formula rows use `Type: Latex Formula`, `Acquire Via: formula`, `Crop Policy: no-crop`, and `Rendered` or `Needs-Manual`.
@@ -125,7 +125,7 @@ The deck's **narrative + persuasion skeleton** — how the argument is organized
**Source**:
- User supplied their own outline / structure → preserve its facts and intended relationships, then apply the confirmed `content_divergence`. Treat an ordinary source outline as a Reference: regroup, reorder, or retitle when the communication contract benefits. Treat it as authoritative only when the user presents it as the final page plan or explicitly asks to preserve page order, titles, or wording; record that promoted boundary in `design_spec.md`. Still lock a mode for register, voice, and any permitted reshaping. `briefing` imposes the least if no particular "讲法" is intended.
- Beautify / re-layout profile ([`beautify-pptx.md`](../workflows/profiles/beautify-pptx.md)) → the extracted source content is authoritative and **verbatim**, one step stricter than the user-outline case above. Each source slide becomes exactly one `§IX` page in source order; transcribe every content block word-for-word — never reshape / re-primary / condense / merge / split / reword. Lock `mode: briefing`; color (e) and typography (g) are whatever the user confirmed in the beautify plan — the source identity (theme or observed) by default, or a content / brand-aware alternative the beautify plan offered and the user picked — locked as truth (the beautify plan already ran the recommendation through the confirm UI, so do not re-recommend here). Charts / tables / images are regenerated from their extracted data in the inherited style (route chart/table data to §VII, pictures to §VIII) — data values stay frozen, the rendering is the deck's own; never carried over verbatim. Layout, hierarchy, rhythm, and visual rendering are what gets redesigned.
- Beautify / re-layout profile ([`beautify-pptx.md`](../workflows/profiles/beautify-pptx.md)) → the extracted source content is authoritative and **verbatim**, one step stricter than the user-outline case above. Each source slide becomes exactly one `§IX` page in source order; transcribe every content block word-for-word — never reshape / re-primary / condense / merge / split / reword. Lock `mode: briefing`; color (e) and typography (g) are whatever the user confirmed in the beautify plan — the source identity (theme or observed) by default, or a content / brand-aware alternative the beautify plan offered and the user picked — locked as truth (the beautify plan already ran the recommendation through the confirm UI, so do not re-recommend here). Charts / tables / images are regenerated from their extracted data in the inherited style: record only selected catalog references in §VII, keep unmatched chart/table plans in their §IX page blocks, and route pictures to §VIII. Data values stay frozen and the rendering is the deck's own; visuals are never carried over verbatim. Layout, hierarchy, rhythm, and visual rendering are what gets redesigned.
- A bespoke direction the five don't give — a nameable cadence (dialectic 正反合, myth-vs-reality, countdown, Socratic), a multi-act fusion of modes, or the user's own feel (confrontational here, detached there). Either the user asks, **or you recommend it** when a fusion / bespoke direction genuinely serves the deck better than a single preset (a recommendation the user confirms, like every lock). The *kind* doesn't matter → `mode: custom` + a `mode_behavior:` paragraph that **crystallizes the intent** (act sequence or posture shifts, title voice, page rhythm, register) concretely enough for the Executor to follow per page; it reads only `spec_lock.md`, never the chat. If the direction uses existing modes, read every corresponding `modes/<id>.md` before synthesis and retain those exact ids as its catalog basis; if it is genuinely new, do not invent a basis. One deck locks **one** value — a fusion is one `custom` describing the acts, never several modes. Avoid only the *dodge*: don't default to `custom` when a preset genuinely fits, and prefer a dominant mode + page-level variation when one mode leads.
- No user structure or cadence → recommend from the confirmed `communication_intent`, `audience_outcome`, source texture, and delivery context using the index's auto-selection table. Composite intent does not automatically require `custom`: choose the dominant spine of the body pages when one exists; use a concrete `custom` act sequence only when no single spine can serve the stated priority / sequence. Present as a recommendation; the user may override.
@@ -182,13 +182,14 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
>
> **At the Strategist confirmation stage — decide the library and stroke only; resolve and sync filenames after approval.**
>
> 1. **Pick exactly one stylistic library** read the source material, then choose the library whose visual character best serves the deck:
> 1. **Pick at most one primary stylistic library from the four bundled choices** — when generic icons are needed, read the source material and choose the one whose visual character best serves the deck:
> - **`chunk-filled`** — fill, straight-line geometry (M/L/H/V/Z only); sharp right angles; heavy, solid, architectural
> - **`tabler-filled`** — fill, bezier curves and arcs (C/A); smooth, rounded, organic; medium weight, approachable
> - **`tabler-outline`** — stroke (line art); airy, refined, lightweight; best for screen-only (thin strokes may be hard to read in print)
> - **`phosphor-duotone`** — duotone; main shape + 20% opacity backplate; medium weight, layered, contemporary
> - ⚠️ **One presentation = one stylistic library** for generic icons (home, chart, users, etc.). Mixing `chunk-filled` / `tabler-filled` / `tabler-outline` / `phosphor-duotone` is FORBIDDEN. If the chosen library lacks an exact icon, find the closest alternative **within that same library**.
> - **Brand-logo exception**: `simple-icons` is NOT a stylistic library. Add it to the deck's icon inventory **only when** the deck genuinely contains real company / product / service brand marks (customer logos, tech-stack icons, social handles). Never substitute it for a missing generic icon.
> - During bundled-library selection, do not select generic icons from more than one of `chunk-filled` / `tabler-filled` / `tabler-outline` / `phosphor-duotone`. If the chosen library lacks an exact icon, find the closest alternative **within that same library**.
> - **`simple-icons` may be selected alone or alongside the primary library**: it is a brand-logo library, not one of the four stylistic choices. Add it only for real company / product / service marks (customer logos, tech-stack icons, social handles), never as a substitute for a missing generic icon.
> - This restriction governs Strategist selection from the bundled catalog, not the prepared project asset pool. User-provided, template-carried, imported, custom, and previously prepared files under `<project_path>/icons/` remain valid material regardless of namespace or visual style.
> 2. **Stroke weight lock (stroke-style libraries only)** — for stroke-based libraries (currently `tabler-outline`), pick one deck-wide value from `{1.5, 2, 3}` (default `2`). For heavier presence, switch library instead of going above `3`.
>
> **After the Strategist confirmation stage is approved — when writing `design_spec.md` §VI / `spec_lock.md`**, then materialize the icon inventory:
@@ -197,9 +198,9 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
> 4. Put known basenames in the final batch. For an uncertain one, search the chosen style library — or `simple-icons` for a real brand mark — with `rg --files "skills/ppt-master/templates/icons/<library>" -g '*<keyword>*.svg'`; do not enumerate broad keyword families.
> 5. **Copy and validate in one batch** — run `python3 skills/ppt-master/scripts/icon_sync.py <project_path> <lib/name> [<lib/name> …]`. This both validates and materializes `<project>/icons/<lib>/`; skip per-file prechecks.
> 6. Keep each successful, case-sensitive `lib/name`: bundled basenames are lowercase (`tabler-outline/award`, never `tabler-outline/Award`); custom icons retain exact case.
> 7. Record the successful inventory, library, and stroke-library `stroke_width` in `design_spec.md` §VI and `spec_lock.md icons`. Executor may use only this list.
> 7. Record the successful bundled selection, its primary stylistic library, and any stroke-library `stroke_width` in `design_spec.md` §VI and `spec_lock.md icons`. Keep selected `simple-icons/*` ids in the same inventory without treating them as a second stylistic library. The inventory records the plan; it does not revoke other prepared project-local icons.
>
> 🚧 **GATE — missing icon = re-pick now**: on non-zero exit, search only the missing concept in the chosen library, re-pick, and rerun the final batch until clean. Never carry a missing icon forward or switch stylistic libraries to fill the gap.
> 🚧 **GATE — missing icon = re-pick now**: on non-zero exit, search a missing generic concept only in the chosen stylistic library, or a missing real brand mark in `simple-icons`; re-pick and rerun the final batch until clean. Never carry a missing icon forward or switch among the four stylistic libraries to fill the gap.
>
> **Default — targeted lookup only**: do not load or rebuild a full index; search only unresolved concepts.
@@ -209,14 +210,14 @@ See [`../templates/icons/README.md`](../templates/icons/README.md) for the curre
**Family selection**:
- User or active template typography is authoritative. Otherwise present two coherent choices: one concord (safe) and one contrast (more tension). Do not pair title/body families that are merely near-duplicates.
- User or active template typography is authoritative. Otherwise ≥3 Stage-2 directions include concord (safe) and contrast (tension); never add a separate font-choice round or pair near-duplicate title/body families.
- Every Stage-2 direction carries `heading` / `body` `cjk`, `latin`, `css`, and positive `body_size`; repeat user/template-fixed stacks.
- Exported faces must resolve to fonts available in PowerPoint. Safe anchors are CJK `Microsoft YaHei` / `SimHei` / `SimSun` / `FangSong` / `KaiTi`; Latin sans `Arial` / `Calibri` / `Segoe UI`; Latin serif `Times New Roman` / `Georgia` / `Cambria`; mono `Consolas`; display `Impact` / `Arial Black`. Executor may sparsely use another export-safe family on short non-structural display/ornament; never on title/body/data/annotation roles. Recurrence requires upstream selection.
- Use PowerPoint-installed exported faces. Safe anchors: CJK `Microsoft YaHei` / `SimHei` / `SimSun` / `FangSong` / `KaiTi`; Latin sans `Arial` / `Calibri` / `Segoe UI` / `Verdana` / `Trebuchet MS`; Latin serif `Times New Roman` / `Georgia` / `Cambria` / `Garamond` / `Book Antiqua`; mono `Consolas` / `Courier New`; display `Impact` / `Arial Black`. Other export-safe families are limited to sparse short display/ornament; structural or recurring use returns upstream.
- Keep each stack to four families or fewer. A non-installed brand or web face is legal only when the Design Spec explicitly records the install / embed requirement and a safe substitute.
- Avoid splitting roles across near-equivalents such as YaHei↔PingFang, SimSun↔Songti, Arial↔Helvetica↔Segoe UI, or Times New Roman↔Times. A cross-platform counterpart may remain inside one fallback stack.
- Choose by the locked style: serif for editorial / data-journalism, display weight for brutalist / poster directions, KaiTi or FangSong for ink character, mono accents for dark-tech / blueprint, and restrained sans for swiss-minimal / soft-rounded.
- Choose by locked style and vary the axis instead of defaulting to YaHei/Arial: serif×sans, Kai/FangSong×hei, hei×song, double-serif, display×neutral, same-family weight, or sans+mono. These are recall seeds, not presets.
**Strategist-owned role extension after confirmation**: Confirm UI keeps the heading/body choice unchanged. While authoring the complete §IX roster and §IV typography plan, scan the actual content for recurring roles that materially need a different family for character or legibility—such as `annotation`, `footer`, `footnote`, `data`, `emphasis`, `quote`, or `code`. Add a lowercase snake_case role and exact stack only when it recurs; inherited roles and one-off garnish stay omitted. The extension must remain coherent with the confirmed heading/body system and locked visual style, and it does not reopen confirmation. Record one compact `Role rationale` in §IV stating the added roles and why, or that no additional family role is justified.
**Strategist-owned role extension after confirmation**: Confirm UI keeps the heading/body choice unchanged. While authoring the complete §IX roster and §IV typography plan, scan the actual content for recurring roles that materially need a different family for character or legibility—such as `annotation`, `footer`, `footnote`, `data`, `emphasis`, `quote`, or `code`. Add a lowercase snake_case role and exact stack only when it recurs; inherited roles and one-off garnish stay omitted. The extension must remain coherent with the confirmed heading/body system and locked visual style, and it does not reopen confirmation. Only when an additional family role is added, record one compact `Role rationale` in §IV naming the added role(s) and why; otherwise omit the line.
**Size anchors — px only**: Every authoring layer carries bare px numbers. PowerPoint's displayed pt is an export result (`px × 0.75`), never an input or confirmation value.
@@ -239,7 +240,7 @@ Other canvases use the body baseline in [`canvas-formats.md`](canvas-formats.md)
| Annotation | 0.70.85× |
| Footnote / page number | 0.50.65× |
Scan §IX before locking. Declare every recurring role, including `lead`, `footnote`, and chart annotations when used; a lead is always at least body size. Give each role one deck-wide anchor and snap derived anchors to clean even px (for body 24, a sound set is title 42, subtitle 32, lead 30, annotation 18, footnote 16). Executor may vary one occurrence within that role's anchor ±2px while preserving hierarchy and readability. A new semantic role or any size outside ±2px requires an explicit named slot; feature elements that need a larger departure are planned here rather than improvised downstream.
Scan §IX before locking. Declare every recurring role, including `lead`, `footnote`, and chart annotations when used; a lead is always at least body size. Give each role one deck-wide anchor and snap derived anchors to clean even px (for body 24, a sound set is title 42, subtitle 32, lead 30, annotation 18, footnote 16). Executor may vary one occurrence within that role's anchor ±2px while preserving hierarchy and readability. A short non-structural Hero/Display size planned for at most two occurrences may remain undeclared; the third planned occurrence makes it recurring and requires an explicit named slot. Structural text never uses this sparse exception.
#### Formula Planning Trigger
Formula policy and formula-asset planning are conditional. If the source contains formula-worthy expressions, or the user explicitly requests formula handling, read [`strategist-image.md`](./strategist-image.md) §3 before confirming the production policy or writing formula rows. Load it even when `image_usage` is `none`; otherwise omit formula planning from the core path.
@@ -287,12 +288,12 @@ python3 skills/ppt-master/scripts/chart_recall.py recall \
--limit 6
```
The command returns a bounded shortlist plus `no-template-match`. Read it unfiltered: `--limit` already bounds output, while `tail` / `head` / `grep` can hide higher-ranked candidates. `confidence` is diagnostic only. Semantically review the candidates; if terminology or structural ambiguity suggests a missed catalog structure, rerun with `--semantic-fallback` and compare its rules. This is optional, not a routine no-match gate. Do not open a second index.
The command returns a bounded shortlist plus `no-template-match`. Read it unfiltered; `tail` / `head` / `grep` can hide ranked candidates. `confidence` is lexical only. At `high` / `medium`, keep no-match after candidate review. At `low` / `none`, use a fitting candidate directly; otherwise rerun once with `--semantic-fallback` before no-match. Do not open a second index.
**Selection**:
1. Choose the most specific valid structure from the bounded candidates or an explicitly requested semantic fallback; keep one primary visualization per page and adapt its treatment rather than mimicking it.
2. When no recalled reference fits, retain `no-template-match` by Strategist judgment: data content falls back to a table, permitted conceptual content to an AI image, and structural content to a custom layout. Record the chosen fallback only in the affected page's §IX `Visualization` / `Layout`; do not serialize the negative result into §VII.
1. Choose the most relevant candidate as a reference for that page. It does not lock the final visualization type or geometry and never applies to another page without its own row.
2. After the fallback gate above, retain `no-template-match` when no reference fits: data content falls back to a table, permitted conceptual content to an AI image, and structural content to a custom layout. Record the fallback only in the page's §IX `Visualization` / `Layout`; never serialize it into §VII.
3. Validate all selected keys before writing the lock:
```bash
@@ -301,17 +302,14 @@ python3 skills/ppt-master/scripts/chart_recall.py validate <key> [<key> ...]
A failed validation must be corrected with a recalled key. `no-template-match` is not a key and never appears in `page_charts`.
**Section VII audit**: §VII is a positive reference inventory. Include it only when at least one catalog candidate is selected. Every row copies the selected candidate's returned `summary` verbatim into `Summary-quote` and records its real path plus page-specific usage. List real returned runners-up only for pages with a selected reference. Never write an empty §VII, a `no-template-match` / `n/a` row, or prose saying no reference exists; a no-match page is described only in its §IX block.
**Section VII selection list**: when a reference is selected, write `Page | Template | Usage`; Usage is one short page-local purpose, not geometry. Omit §VII when none is selected, and never add path, summary, runners-up, `no-template-match`, or `n/a`. §IX remains authoritative.
**Native-ready boundary**: For every independent data chart or pure text-grid table, add `Native-ready: yes|no` to its §IX page block. Choose `yes` only when the confirmed requirement or artifact afterlife benefits from an editable native data object; otherwise keep the designed SVG with `no`. Conceptual rows and incidental sparklines, KPI trends, or insets omit the field; Executor never promotes them.
```
| Page | Template | Path | Summary-quote (verbatim) | Usage |
|---|---|---|---|---|
| P03 | line_chart | templates/charts/line_chart.svg | "<returned summary>" | <intent> |
Runners-up considered:
- <returned_key> | rejected for P03: <page-specific reason>
```markdown
| Page | Template | Usage |
| --- | --- | --- |
| P03 | line_chart | Compare the source metrics over time |
```
**Flag native-preset candidates**: In the affected page's §IX `Layout` / `Visualization`, note when the content calls for a literal stock PowerPoint chevron, block arrow, standard flowchart node, callout, banner, or star. Executor still decides the exact preset under its native-shape branch; this note never creates a §VII row by itself.
@@ -384,7 +382,7 @@ Content-outline and speaker-notes strategy follow the deck's locked **mode** —
This is what makes the axis meaningful: a `presentation` deck and a `text` deck built from the **same source and communication contract** must differ in page grammar, page count recommendation, per-page text volume, visual burden, layout density, rhythm, and notes—not only in font size. Page count stays the user's call; reading mode informs the recommendation when the user has not fixed one. Record it as **Reading Mode** in `design_spec.md §I` (compatibility key `delivery_purpose`, lock key `consumption_mode`). Separately, `communication_intent` / `audience_outcome` determine what the outline must accomplish, while `delivery_context` and `artifact_afterlife` help select the reading mode and still remain independent constraints after selection. The `page_rhythm` leans are a bias, not a quota. Preservation paths keep source wording and structure verbatim: honor reading mode only in styling and notes, never by rephrasing or re-paginating.
> Note: §IX is the complete page brief projected into each Executor page-context — what you write there is what survives context compression.
> Note: §IX is the complete page brief; Executor retains it with the lock until context invalidation, then reloads both once.
### 6.2 Planning Artifact Content
@@ -412,12 +410,12 @@ Generate Step 4 owns this reference-first sequence. `design_spec.md` is the Stra
**GATE 2 — lock context fidelity.** After the Design Spec passes Gate 1, author its machine-relevant execution anchors and routing values into `spec_lock.md`. The lock may normalize syntax and add named recurring implementation roles justified by the Design Spec/page plan, but it must not change confirmed identity or introduce a competing direction. It is intentionally not a field-for-field copy and not a whitelist of every legal SVG value. If authoring exposes a contradiction or missing confirmed decision, return to Gate 1 and repair the Design Spec from the retained final-confirmation state; on a fresh recovery turn only, read the persisted final result once to restore that state.
**Execution lock content**: `spec_lock.md` compactly carries communication, stable color/type anchors, icons, images, page rhythm, chart choices, and route-specific PowerPoint structure. Name every recurring typography role and any planned feature role that needs to leave another role's ±2px band; never re-derive a confirmed anchor. New locks keep `font_family` as the body/default compatibility stack and also write explicit `title_family` + `body_family`; every additional recurring Design Spec role projects to `<role>_family`. Collapsing distinct Design Spec stacks into `font_family`, or dropping an extra role, fails Gate 2. Keep core fonts/palette roles stable; page authoring varies treatment and may add sparse local garnish. Project every placed §VIII image's source, pattern, and crop policy; omit unplaced sheets and planning provenance. Free-design, brand-only, and `template_reuse_scope: style` use `pptx_structure.mode: flat`; the template module owns structured mappings. Executor rebuilds page projection before every page ([executor-base.md](executor-base.md) §2.1). Repair the Design Spec only from retained final confirmation, then re-author affected lock rows.
**Execution lock content**: `spec_lock.md` compactly carries communication, stable color/type anchors, icons, images, page rhythm, chart choices, and route-specific PowerPoint structure. Name every recurring typography role; a planned short non-structural Hero/Display size may stay omitted only while the same value appears at most twice, and its third occurrence requires a named role. Never re-derive a confirmed anchor. New locks keep `font_family` as the body/default compatibility stack and also write explicit `title_family` + `body_family`; every additional recurring Design Spec role projects to `<role>_family`. Collapsing distinct Design Spec stacks into `font_family`, or dropping an extra role, fails Gate 2. Keep core fonts/palette roles stable; page authoring varies treatment and may add sparse local garnish. Project every placed §VIII image's source, pattern, and crop policy; omit unplaced sheets and planning provenance. Free-design, brand-only, and `template_reuse_scope: style` use `pptx_structure.mode: flat`; the template module owns structured mappings. Executor context policy lives in [executor-base.md](executor-base.md) §2.1. Repair the Design Spec only from retained final confirmation, then re-author affected lock rows.
**Contextual extension**: derived paint or sparse local font/color garnish may stay in one SVG while non-structural and non-recurring. New base/semantic colors, structural/recurring fonts, resources, or patterns require upstream repair; Executor never reverse-projects a choice as fact. Promote garnish upstream before reuse, regenerate page-context, and never add values to silence a comparison.
**Contextual extension**: derived paint or sparse local font/color garnish may stay in one SVG while non-structural and non-recurring. New base/semantic colors, structural/recurring fonts, resources, or patterns require upstream repair; Executor never reverse-projects a choice as fact. Promote garnish upstream before reuse, read back and validate the affected planning fragments, and never add values to silence a comparison.
- **Communication trace is mandatory**: Keep the full confirmed communication contract in `design_spec.md §I`, then project only `audience`, `objective`, `core_message`, and canonical `consumption_mode` into `spec_lock.md communication`. Write `objective` as one concise execution sentence that preserves both the confirmed `communication_intent` and the success condition in `audience_outcome`; do not copy `delivery_context`, `artifact_afterlife`, dates, provenance, or conflict-resolution commentary into the lock. Before finalizing §IX, check that every named purpose has at least one outline obligation and **every Slide block**, including cover / divider / closing pages, has an `Audience move` that advances the global outcome. A page that advances no purpose or outcome should be merged, rewritten, or cut. `project_manager.py validate` and `svg_quality_checker.py` enforce the compact lock fields and per-page move presence, not their subjective quality.
- **Custom behavior is concise and executable**: For confirmed `custom` mode or visual style, project one resolved `mode_behavior` / `visual_style_behavior` sentence or short paragraph. When the direction actually combines or borrows catalog entries, also project the exact, comma-separated `mode_references` / `visual_style_references`; omit the field for a genuinely novel direction and never fabricate a nearby reference. Preserve the confirmed direction, reference locked role names such as `colors.primary` when needed, and omit selection history, contradictions, precedence explanations, or other Design Spec provenance. Page-context carries these fields directly to Executor.
- **Custom behavior is concise and executable**: For confirmed `custom` mode or visual style, project one resolved `mode_behavior` / `visual_style_behavior` sentence or short paragraph. When the direction actually combines or borrows catalog entries, also project the exact, comma-separated `mode_references` / `visual_style_references`; omit the field for a genuinely novel direction and never fabricate a nearby reference. Preserve the confirmed direction, reference locked role names such as `colors.primary` when needed, and omit selection history, contradictions, precedence explanations, or other Design Spec provenance. Executor reads these fields from the retained lock and loads every referenced catalog entry once per valid context.
- **page_rhythm is mandatory**: Based on the page list in §IX Content Outline, assign each page one of `anchor` / `dense` / `breathing`. This is what breaks the uniform "every page is a card grid" feel. New locks may not omit the section; consumer omission behavior is owned by [`executor-base.md`](executor-base.md) §2.1.
- **Fact IDs and scenario labels are mandatory when applicable**: Read any `sources/*.facts.json`. For each §IX page, list the stable IDs actually used; never cite an ID whose claim is absent from the page. Mark invented KPIs/targets/internal ratios as `Data class: scenario` and state which values are scenario data. Executor carries external sources into notes/footnotes and renders a visible scenario label for scenario figures.
- **Rhythm follows narrative, not quota**: `breathing` pages mark natural pauses — chapter transitions, standalone emphasis (hero quote / big number), SCQA bridges. Dense decks may legitimately be all `dense`. **Do NOT invent filler pages** ("Thank you", empty dividers) to pad rhythm — every `breathing` page must say something independent. Consumption mode biases the overall lean (`presentation` toward more `anchor` / `breathing`, `text` toward `dense`; see §6.1) — a bias, never a quota.
@@ -427,7 +425,7 @@ Generate Step 4 owns this reference-first sequence. `design_spec.md` is the Stra
- **pptx_structure is mandatory**: Free-design, brand-only, and `template_reuse_scope: style` routes write `mode: flat`; a style-reference route may also record `template_reuse_scope: style` but omits every structure mapping and `template_adherence`. `template_reuse_scope: mirror|layout` writes `mode: structured` plus `template_adherence: strict|adaptive`. Do not write legacy `baseline`, `template`, `preserve`, `layout_strategy`, or Layout-kind rows into a new project.
- **Flat-route boundary**: With `mode: flat`, omit `pptx_masters`, `pptx_layouts`, `page_pptx_layouts`, and `page_layouts`. Do not plan native Master/Layout families or reusable placeholder slots. Every generated SVG object remains Slide-local: omit root Master/Layout identity, `data-pptx-layer`, and `data-pptx-placeholder*` metadata. Export materializes one clean project-owned Master plus one Blank Layout from the current color/typography lock, removes stock content placeholders/Layout inventory, and retains only the standard date/footer/slide-number capability hooks.
- **Structured template route**: When [`strategist-template.md`](./strategist-template.md) is active and reuse is `mirror|layout`, follow its complete Master/Layout/slot/prototype mapping rules.
- **page_charts (write only for pages with a selected catalog reference)**: For each page in `design_spec.md §VII` whose path points to `templates/charts/<name>.svg`, add `P<NN>: <chart_name>`. No-match pages never appear here because §VII omits them and Executor would otherwise look for a non-existent reference. If no catalog reference is selected, omit the section even when §IX contains custom data visualizations.
- **page_charts**: project each §VII row's `Page` and `Template` as `P<NN>: <template_key>`; Usage stays in the Design Spec. This is a page-local reference, not a type/geometry lock. Omit no-match pages and the empty section.
---
@@ -80,13 +80,15 @@ python3 scripts/project_manager.py page-context <project_path> P07 --record-usag
python3 scripts/project_manager.py page-context-report <project_path>
```
`page-context` prints a read-only compact current-page projection. Its global
lock projection repeats per page as a continuity anchor set, not a color/font allowlist; large Design Specs,
`page-context` is an on-demand read-only current-page projection for diagnostics,
routing checks, or context measurement; normal generation retains the complete
Design Spec and lock once per valid execution context. Each invocation includes
the global lock projection as a continuity anchor set, not a color/font allowlist; large Design Specs,
prototype, and `templates/charts/` references are emitted only as scoped
path/SHA fingerprints and are read once per execution context. `--bundle` is a
deprecated compatibility no-op. `--record-usage` writes one derived snapshot
under `analysis/page-context/`; exact `o200k_base` token counts are optional and
degrade to `tokens: null` when `tiktoken` is absent.
degrade to `tokens: null` when `tiktoken` is absent. Telemetry may be partial.
Chart candidate recall:
@@ -199,20 +199,33 @@ def recall_candidates(
else:
confidence = "none"
fallback_required_before_no_match = (
confidence in {"low", "none"} and not force_semantic_fallback
)
if fallback_required_before_no_match:
no_match_instruction = (
f"Lexical confidence is {confidence}. Select a bounded candidate when one "
"fits; otherwise rerun the same recall with --semantic-fallback before "
"keeping no-template-match. Keep the final negative result out of Design "
"Spec Section VII and describe the chosen fallback in the page's Section "
"IX block."
)
else:
no_match_instruction = (
"Use when none of the reviewed candidates fits the page structure. Keep "
"this result out of Design Spec Section VII and describe the chosen fallback "
"in the page's Section IX block."
)
result: dict[str, object] = {
"page": page,
"semantic_tags": tags,
"confidence": confidence,
"candidates": candidates,
"no_template_match": {
"allowed": True,
"allowed": not fallback_required_before_no_match,
"key": "no-template-match",
"instruction": (
"Use when none of the bounded candidates fits the page structure. If the "
"shortlist may have missed a relevant catalog rule, rerun with "
"--semantic-fallback first. Keep this result out of Design Spec Section "
"VII and describe the chosen fallback in the page's Section IX block."
),
"instruction": no_match_instruction,
},
}
if force_semantic_fallback:
@@ -15,9 +15,9 @@ python3 skills/ppt-master/scripts/chart_recall.py recall \
--limit 6
```
`--limit` accepts 3-8 and defaults to 6. The JSON is already bounded and must be read unfiltered: `tail`, `head`, `grep`, or another truncator can discard higher-ranked candidates. `confidence` reports lexical strength only; `low` / `none` never decides whether a template should be used.
`--limit` accepts 3-8 and defaults to 6. The JSON is already bounded and must be read unfiltered: `tail`, `head`, `grep`, or another truncator can discard higher-ranked candidates. `confidence` reports lexical strength only; it never decides whether a candidate fits. At `low` / `none`, a fitting bounded candidate needs no expansion, but `no_template_match.allowed` remains false until one explicit semantic fallback review.
Semantically review the bounded candidates. If none fits and the page clearly needs a custom composition, retain `no-template-match`. If terminology mismatch or structural ambiguity suggests that bounded recall may have missed a catalog match, rerun the same command with `--semantic-fallback`, then compare the returned rules semantically. The flag is an uncertainty fallback, not a routine no-match gate. Do not open or maintain a second keyword/category index. `no-template-match` is an internal recall result, not a Design Spec §VII row.
Semantically review the bounded candidates. At `high` / `medium`, retain `no-template-match` when none fits. At `low` / `none`, select a fitting bounded candidate directly; otherwise rerun the same command once with `--semantic-fallback`, compare the returned rules semantically, and only then retain `no-template-match`. The full-catalog review is therefore a narrow low-confidence no-match gate, not a routine recall step. Do not open or maintain a second keyword/category index. `no-template-match` is an internal recall result, not a Design Spec §VII row.
| Field | Contract |
|---|---|
@@ -26,9 +26,9 @@ Semantically review the bounded candidates. If none fits and the page clearly ne
| `confidence` | Lexical recall strength; never a selection decision |
| `candidates` | Ranked keys, SVG paths, verbatim catalog summaries, scores, and matched tags |
| `semantic_fallback` | Full live catalog, present only with `--semantic-fallback`; requires semantic comparison |
| `no_template_match` | Explicit fallback when the Strategist judges that no recalled reference fits |
| `no_template_match` | Explicit fallback; `allowed` stays false for `low` / `none` until `--semantic-fallback` is used |
The scorer treats the key and the summary's Pick clause as positive evidence and the Skip clause as negative evidence. A term found only in Skip cannot make a candidate eligible, and Skip matches explicitly reduce a candidate's score. Unicode input is NFKC-normalized before matching. The Strategist still applies semantic judgment: inspect the returned candidates, reject candidates whose Skip clause matches, and prefer the most specific valid structure. An empty shortlist permits `no-template-match`; use `--semantic-fallback` only when the Strategist suspects a relevant catalog structure was missed.
The scorer treats the key and the summary's Pick clause as positive evidence and the Skip clause as negative evidence. A term found only in Skip cannot make a candidate eligible, and Skip matches explicitly reduce a candidate's score. Unicode input is NFKC-normalized before matching. The Strategist still applies semantic judgment: inspect the returned candidates, reject candidates whose Skip clause matches, and prefer the most specific valid structure. An empty or low-confidence shortlist requires one `--semantic-fallback` review only when the Strategist is about to keep `no-template-match`.
## Validate selected keys
@@ -43,8 +43,8 @@ The command is read-only. It exits `0` when every key exists and `1` when any ke
## Selection boundary
- Preserve the two-lens review: numeric/data pages and structural-information pages.
- Record the selected candidate's returned `summary` verbatim as the Section VII `summary-quote`.
- Keep §VII as a positive inventory: every row has a real key/path, and the whole section is omitted when no candidate is selected.
- Keep §VII as a positive selection list: record `Page | Template | Usage` for each selected key, and omit the whole section when no candidate is selected.
- Make `Usage` one concise page-local purpose, not geometry or execution instructions; derive `templates/charts/<key>.svg` from the key and keep detailed adaptation in §IX.
- Never serialize `no-template-match`, an empty table, or a no-reference explanation into §VII.
- Record real returned runners-up and page-specific rejection reasons only for pages with a selected reference.
- Open only the selected `<key>.svg` before authoring that visualization; do not load unrelated catalog SVGs.
- Do not serialize returned summaries, paths, or runners-up into new §VII tables. Legacy wider tables remain readable.
- Open the selected `<key>.svg` only as a reference for its mapped page; it does not lock type or geometry. Do not load unrelated catalog SVGs.
@@ -205,7 +205,7 @@ After Stage 2 is confirmed, overwrite it with Stage-3 production recommendations
- `custom_candidates` is recommendation-only. Mode / style carry localized `name` + `behavior`; conditional image strategy also carries `rendering: "custom"`, `visual`, and `mood`. When a proposal combines or borrows existing catalog entries, the visible behavior names every exact id and Strategist reads every corresponding file before authoring it; a genuinely novel proposal names none. The server rejects missing required candidates; the UI shows full copy, edits it only after selection, rejects a selected blank, and omits unselected candidates from `result.json`. Template-backed proposals obey inherited identity, prototype capacity, and `template_application`.
- `audience`, `communication_intent`, and `audience_outcome` are load-bearing Stage-1 reasoning inputs, so seed concrete recommendations when the evidence supports them; they are not required user inputs. Every Stage-1 prose field may be blank after confirmation. The complete six-field contract stays in `result.json` and `design_spec.md`; `spec_lock.md communication` receives only the compact `audience` / `objective` / `core_message` execution projection plus the applicable reading mode. `communication_intent` may preserve several purposes plus priority / sequence; never add a `primary_job` enum.
- Do not write `recommend.template_reuse_scope` or `recommend.template_adherence`. Strategist records those internal exporter values later in `spec_lock.md` after inspecting the actual template and current content.
- For an active template workspace, write one editable prose field as top-level `template_application.value`. It summarizes actual page/prototype use and preservation/reorganization decisions. Omit it for free design. The UI returns the current string through Stage 2, Stage 3, and final confirmation; Strategist then persists the final effective plan as `- **Template Application**: ...` in `design_spec.md §I`, and `page-context` projects it to Executor. Never replace it with internal reuse/adherence ids or a fixed option menu.
- For an active template workspace, write one editable prose field as top-level `template_application.value`. It summarizes actual page/prototype use and preservation/reorganization decisions. Omit it for free design. The UI returns the current string through Stage 2, Stage 3, and final confirmation; Strategist then persists the final effective plan as `- **Template Application**: ...` in `design_spec.md §I`, which Executor reads from the retained Design Spec. Never replace it with internal reuse/adherence ids or a fixed option menu.
- `recommend.image_usage` should be an array of source ids when more than one source applies, e.g. `["ai", "provided"]`. A single string is still accepted for backward compatibility. Do not write bare `"custom"` and do not encode a mixed-source plan as prose here; write the prose to top-level `image_notes.value`.
- `image_notes` is the initial strategy note shown under the image source chips. Use it for page-role guidance and constraints: which source applies where, what to avoid, which user assets are authoritative, how realistic / abstract the imagery should be, and what can remain as placeholders. It is intent guidance, not a separate finite option.
- When confirmed Stage-2 `image_usage` includes `ai`, Stage 3 sets `recommend.image_ai_path` to one of `auto` / `api` / `host-native` / `manual`. Stage 2 never asks for the acquisition mechanism while the user is still deciding the image role.
@@ -164,7 +164,7 @@ Analyze images in a project directory before writing the design spec or composin
python3 scripts/analyze_images.py <project_path>/images
```
Use this instead of opening image files directly when following the project workflow.
Use this as the default inventory and geometry source; it does not perform semantic image understanding. Generate planning follows the Strategist's context-first boundary: source context, captions / alt text / titles, filenames, user notes, and existing resource records come first. Only a specific asset whose meaning or safe placement remains materially ambiguous may be inspected, and the workflow never bulk-opens the image folder.
## `image_search.py`
@@ -70,26 +70,29 @@ Notes:
Multi-deck per project: several PPTX imports each get their own `<stem>.*`
artifacts and a `decks[]` entry; re-importing the same stem replaces its entry.
### Per-page execution view
### On-demand page execution view
`page-context` projects `design_spec.md` and `spec_lock.md` into one compact
current-page view on stdout. The default command is read-only; `--pretty`
changes JSON formatting only. Before projection it revalidates the machine lock
and selected template-root identities; design-brief values are not treated as
a second lock. Slide headings at H3H6 remain readable by the projector.
a second lock. Slide headings at H3H6 remain readable by the projector. Normal
generation retains the complete planning artifacts once per valid execution
context and does not invoke this command before every page; use it only for an
explicit diagnostic, routing check, or context-usage measurement.
The output deliberately repeats the bounded `global` anchor set on every
page as a cross-page anchor set, not a color/font allowlist. `lock_source` binds that projection to the current
Each invocation deliberately includes the bounded `global` anchor set as a
cross-page continuity view, not a color/font allowlist. `lock_source` binds that projection to the current
`spec_lock.md` SHA. `page_context` contains the current §IX brief, rhythm,
resources, and conditional template/chart assignment. `reference_set` contains
`kind`, scoped path, SHA, and `once-per-execution-context` policy for the
project/template Design Specs and selected prototype/chart SVGs. The project
Design Spec additionally carries
`same_context_edit_policy: targeted-readback-and-rebind`: when the current main
agent makes a bounded repair that preserves roster/order/identity/communication,
it reads back only the exact changed fragments,
validates, reruns `page-context`, and binds the verified delta to the new SHA.
Fresh, external, unknown, or mismatched changes still require a complete read.
agent makes a bounded repair in a valid uncompacted context that preserves
roster/order/identity/communication, it reads back only the exact changed
fragments and validates them before continuing. Fresh, compacted, external,
unknown, or mismatched changes require one complete Design Spec and lock read.
The deprecated `--bundle` flag remains accepted as a compatibility no-op. It
never appends a Design Spec, prototype SVG, chart SVG, manifest, or text-slot
@@ -115,9 +118,9 @@ exact compact stdout, and records the reference fingerprints. `tiktoken` is
loaded lazily with `o200k_base`; when unavailable, the command still succeeds
and records bytes, characters, hashes, and `tokens: null`.
`page-context-report` summarizes only fresh snapshots and identifies stale or
token-unavailable pages plus unique referenced files. The telemetry does not
measure the once-loaded reference payloads, source-material reads, or other
session-level prompt references.
token-unavailable pages plus unique referenced files. Telemetry may be partial;
it does not measure once-loaded references, source reads, or other session
context.
Common formats:
- `ppt169`
@@ -296,6 +296,12 @@ python3 scripts/svg_to_pptx.py <project_path> --no-merge # strict line-fidelit
python3 scripts/svg_to_pptx.py <project_path> --recorded-narration audio
```
The normal command reads `pptx_structure.mode` from `spec_lock.md`. For legacy
projects whose lock exists but predates that field, export emits one compatibility
warning and uses `flat`; no SVG regeneration is required. A missing `spec_lock.md`,
an explicit legacy/unknown mode, or a requested `structured` export without an
explicit current structured contract remains blocking.
For generated-project narration, follow the
[`generate-audio`](../../workflows/stages/generate-audio.md) stage. It owns voice
selection, audio generation, and the narrated re-export workflow.
@@ -6,8 +6,9 @@ Copy chosen library icons into `<project>/icons/<lib>/` when selected. Missing
names exit non-zero before export. Known basenames need no separate existence
check; search the chosen library only for unresolved concepts.
Project-local custom icons count as satisfied. `simple-icons` may accompany one
stylistic library only for real brand marks.
Project-local custom icons count as satisfied. In one Strategist selection
batch, `simple-icons` may accompany one of the four stylistic libraries for
real brand marks.
Usage:
python3 scripts/icon_sync.py <project_path> <lib/name> [<lib/name> ...]
@@ -104,7 +105,8 @@ def main(argv: Optional[list[str]] = None) -> int:
file=sys.stderr,
)
print(
"Choose one stylistic library per deck; simple-icons may coexist for real brand marks.",
"Choose one of the four stylistic libraries per selection batch; "
"simple-icons may coexist for real brand marks.",
file=sys.stderr,
)
return 1
@@ -278,11 +278,12 @@ def _template_execution_manifest_files(
"text_slots_schema": TEMPLATE_TEXT_SLOTS_SCHEMA,
"execution_policy": (
"This manifest and its text_slots_path records are derived tool "
"metadata, not page-authoring inputs. Before each page, run "
"project_manager.py page-context <project> P<NN> --record-usage. "
"Read the selected complete prototype only when its path and SHA "
"are absent from the active execution context or changed, then "
"reuse it. Choose semantic replacements and edit only existing "
"metadata, not page-authoring inputs. Retain the complete project "
"Design Spec and lock once per valid uncompacted execution context; "
"use page-context only for on-demand diagnostics or telemetry. "
"Read the selected complete prototype once per valid context and "
"again only after a known change or context invalidation. Choose "
"semantic replacements and edit only existing "
"visible text values; structured export validates text/tspan "
"topology and attributes against the prototype."
),
@@ -29,6 +29,7 @@ from typing import Callable, Iterable
from project_specs import (
default_spec_lock_forbidden,
parse_markdown_artifact,
parse_spec_lock_artifact,
validate_project_artifacts,
)
from svg_to_pptx.pptx_package.template_structure import (
@@ -460,7 +461,7 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
f"{preview}{suffix}"
)
try:
lock_sections_raw = parse_markdown_artifact(
lock_sections_raw = parse_spec_lock_artifact(
lock_path,
report_duplicate_fields=True,
)
@@ -551,8 +552,8 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
reference_set.append(chart_reference)
mode_fields = _section_fields(lock_sections, "mode")
visual_style_fields = _section_fields(lock_sections, "visual_style")
# Repeat this bounded projection per page intentionally: stable lock roles
# are continuity anchors, while large reference payloads use reference_set.
# Each on-demand projection includes bounded lock anchors; large reference
# payloads stay outside it and are represented by reference_set.
global_context = {
"communication": _section_fields(lock_sections, "communication"),
"canvas": _section_fields(lock_sections, "canvas"),
@@ -591,7 +592,7 @@ def build_page_context(project: str | Path, raw_page: str) -> PageContextResult:
"lock_source": {
"path": "spec_lock.md",
"sha256": _file_sha256(lock_path),
"load_policy": "per-page-context-anchors",
"load_policy": "on-demand-anchor-projection",
},
"global": global_context,
"page_context": current_page,
@@ -19,6 +19,7 @@ Dependencies:
from __future__ import annotations
import json
import math
import re
from pathlib import Path
from typing import Mapping
@@ -69,6 +70,10 @@ _MARKDOWN_DATA_LINE_RE = re.compile(
r"^[ \t]*-[ \t]+(?:\*\*)?([^:\n*]+?)(?:\*\*)?[ \t]*:[ \t]*(.*)$",
re.MULTILINE,
)
_IMAGE_PATH_SUFFIXES = frozenset(
{".bmp", ".gif", ".jpeg", ".jpg", ".png", ".svg", ".tif", ".tiff", ".webp"}
)
_LEGACY_SPEC_LOCK_FORBIDDEN = frozenset({"Mixing icon libraries"})
_SCAFFOLD_TOKEN_RE = re.compile(r"\{\{[A-Z_]+\}\}")
_SCHEMA_MARKER_RE = re.compile(
r"^<!--[ \t]+ppt-master-schema:[ \t]*([a-z0-9-]+/v[1-9][0-9]*)[ \t]+-->$",
@@ -165,6 +170,90 @@ def parse_markdown_artifact(
return sections
def _looks_like_image_path(raw: str) -> bool:
"""Return whether one lock token looks like a project image path."""
token = raw.strip().strip("`'\"").replace("\\", "/")
return bool(token) and Path(token).suffix.casefold() in _IMAGE_PATH_SUFFIXES
def parse_spec_lock_artifact(
lock_path: Path,
*,
report_duplicate_fields: bool = False,
compatibility_warnings: list[str] | None = None,
) -> list[dict[str, object]]:
"""Parse one execution lock and normalize supported legacy image rows.
New locks use ``- <key>: <path> | source=... | pattern=... | crop=...``.
Some versioned projects instead placed the image path before the colon.
Preserve those projects by projecting the key path back into the value so
every consumer sees the same path-first image value.
"""
sections = parse_markdown_artifact(
lock_path,
report_duplicate_fields=report_duplicate_fields,
)
normalized_sections: list[dict[str, object]] = []
compatibility_keys: list[str] = []
for section in sections:
if str(section.get("heading", "")).strip().casefold() != "images":
normalized_sections.append(section)
continue
raw_fields = section.get("fields")
if not isinstance(raw_fields, dict):
normalized_sections.append(section)
continue
fields: dict[str, str] = {}
for raw_key, raw_value in raw_fields.items():
key = str(raw_key)
value = str(raw_value).strip()
value_path = value.split("|", 1)[0].strip()
if _looks_like_image_path(key) and not _looks_like_image_path(value_path):
value = f"{key} | {value}" if value else key
compatibility_keys.append(key)
fields[key] = value
normalized_section = dict(section)
normalized_section["fields"] = fields
normalized_sections.append(normalized_section)
if compatibility_warnings is not None and compatibility_keys:
sample = ", ".join(compatibility_keys[:3])
suffix = "" if len(compatibility_keys) <= 3 else ", ..."
compatibility_warnings.append(
f"{lock_path.name} images: normalized {len(compatibility_keys)} legacy "
"path-as-key row(s); new locks should use '- <key>: <path> | "
"source=... | pattern=... | crop=...' "
f"(found: {sample}{suffix})"
)
return normalized_sections
def parse_spec_lock(
lock_path: Path,
*,
report_duplicate_fields: bool = False,
compatibility_warnings: list[str] | None = None,
) -> dict[str, dict[str, str]]:
"""Return one execution lock as ``{section: {key: value}}`."""
sections = parse_spec_lock_artifact(
lock_path,
report_duplicate_fields=report_duplicate_fields,
compatibility_warnings=compatibility_warnings,
)
parsed: dict[str, dict[str, str]] = {}
for section in sections:
raw_fields = section.get("fields")
if not isinstance(raw_fields, dict):
continue
parsed[str(section.get("heading", "")).strip()] = {
str(key): str(value) for key, value in raw_fields.items()
}
return parsed
def default_spec_lock_forbidden() -> frozenset[str]:
"""Return the versioned scaffold's universal forbidden-item defaults."""
sections = parse_markdown_artifact(SCAFFOLD_DIR / "spec_lock.md")
@@ -178,11 +267,12 @@ def default_spec_lock_forbidden() -> frozenset[str]:
)
if section is None:
raise ValueError("spec-lock scaffold has no forbidden section")
return frozenset(
current = frozenset(
re.sub(r"^-[ \t]+", "", line.strip())
for line in str(section.get("body", "")).splitlines()
if line.strip()
)
return current | _LEGACY_SPEC_LOCK_FORBIDDEN
def _load_markdown_schema(schema_path: Path) -> dict[str, object]:
@@ -337,6 +427,38 @@ def _validate_section(
f"'{field_name}' does not match '{pattern}'"
)
field_value_rules = definition.get("field_value_rules", [])
if isinstance(field_value_rules, list):
for rule in field_value_rules:
if not isinstance(rule, dict):
continue
key_pattern = rule.get("key_pattern")
value_pattern = rule.get("value_pattern")
if not isinstance(key_pattern, str) or not isinstance(
value_pattern, str
):
continue
requirement = str(rule.get("requirement", "match its value grammar"))
for field_name, raw_value in fields.items():
if re.fullmatch(key_pattern, str(field_name)) is None:
continue
value = str(raw_value).strip()
if bool(rule.get("normalize", True)):
value = _normalize_schema_value(value)
value_matches = re.fullmatch(value_pattern, value) is not None
if value_matches and rule.get("numeric") == "positive_finite":
try:
number = float(value)
except ValueError:
value_matches = False
else:
value_matches = math.isfinite(number) and number > 0
if not value_matches:
errors.append(
f"{markdown_name} schema: section '{section_id}' field "
f"'{field_name}' must {requirement}; found '{value}'"
)
minimum = definition.get("min_entries")
if isinstance(minimum, int) and len(fields) < minimum:
errors.append(
@@ -1022,6 +1144,14 @@ def validate_project_artifacts(
artifact_errors = validate_markdown_schema(artifact_path, schema_path)
errors.extend(artifact_errors)
if artifact_kind == "lock" and not artifact_errors:
try:
parse_spec_lock_artifact(
artifact_path,
compatibility_warnings=warnings,
)
except (OSError, UnicodeError, ValueError) as exc:
errors.append(f"spec_lock.md compatibility parse failed: {exc}")
continue
versioned_lock_valid = True
if versioned_lock_valid:
try:
@@ -15,9 +15,9 @@
"max_tokens": 365000
},
"file_budgets": {
"AGENTS.md": 2300,
"skills/ppt-master/SKILL.md": 800,
"skills/ppt-master/references/executor-base.md": 7060,
"AGENTS.md": 2350,
"skills/ppt-master/SKILL.md": 850,
"skills/ppt-master/references/executor-base.md": 7250,
"skills/ppt-master/references/executor-structured.md": 5300,
"skills/ppt-master/references/executor-chart.md": 3100,
"skills/ppt-master/references/executor-image.md": 900,
@@ -28,12 +28,12 @@
"skills/ppt-master/references/svg-effects.md": 11600,
"skills/ppt-master/references/native-data-interface.md": 6200,
"skills/ppt-master/references/pptx-structure-interface.md": 4300,
"skills/ppt-master/references/strategist.md": 13350,
"skills/ppt-master/references/strategist-image.md": 2100,
"skills/ppt-master/references/strategist.md": 13450,
"skills/ppt-master/references/strategist-image.md": 2250,
"skills/ppt-master/references/strategist-template.md": 2100,
"skills/ppt-master/templates/design_spec_reference.md": 2650,
"skills/ppt-master/templates/spec_lock_reference.md": 2100,
"skills/ppt-master/workflows/generate-pptx.md": 12450,
"skills/ppt-master/templates/design_spec_reference.md": 2850,
"skills/ppt-master/templates/spec_lock_reference.md": 2350,
"skills/ppt-master/workflows/generate-pptx.md": 12700,
"skills/ppt-master/workflows/stages/apply-template-workspace.md": 1800
},
"load_sets": {
@@ -45,7 +45,23 @@
"skills/ppt-master/SKILL.md",
"skills/ppt-master/workflows/routing.md"
],
"max_tokens": 5200
"max_tokens": 5425
},
"support.sponsor-recommendation.en": {
"description": "English sponsor context for explicit user requests for model, AI image model, API/provider, or hosted-service recommendations.",
"scope": "incremental",
"files": [
"skills/ppt-master/SPONSORS.md"
],
"max_tokens": 1250
},
"support.sponsor-recommendation.zh": {
"description": "Chinese sponsor context for explicit user requests for model, AI image model, API/provider, or hosted-service recommendations.",
"scope": "incremental",
"files": [
"skills/ppt-master/SPONSORS_CN.md"
],
"max_tokens": 1250
},
"route.create-template.brand": {
"description": "Create Brand path; it authors no SVG and does not activate Template_Designer or SVG modules.",
@@ -77,7 +93,7 @@
"skills/ppt-master/templates/README.md",
"skills/ppt-master/templates/decks/README.md"
],
"max_tokens": 57250
"max_tokens": 57475
},
"route.create-template.layout": {
"description": "Create Layout path through Template_Designer, SVG core, and the structured PPTX interface.",
@@ -95,7 +111,7 @@
"skills/ppt-master/templates/README.md",
"skills/ppt-master/templates/layouts/README.md"
],
"max_tokens": 57400
"max_tokens": 57675
},
"route.enhance-native-pptx": {
"description": "Finished-PPTX native enhancement route.",
@@ -106,7 +122,7 @@
"files": [
"skills/ppt-master/workflows/native-enhance-pptx.md"
],
"max_tokens": 8200
"max_tokens": 8375
},
"route.fill-native-pptx": {
"description": "Raw-PPTX native fill route.",
@@ -117,7 +133,7 @@
"files": [
"skills/ppt-master/workflows/template-fill-pptx.md"
],
"max_tokens": 10800
"max_tokens": 11075
},
"route.generate.planning": {
"description": "Generate-PPTX planning through Strategist, including reference-first whole-document authoring and representative multi-source custom mode/style synthesis; schemas and optional scaffolds are tool-consumed.",
@@ -306,7 +322,7 @@
"stage.generate.executor.notes"
],
"files": [],
"max_tokens": 81250
"max_tokens": 82475
},
"route.generate.flat-ai-two-types": {
"description": "Generate-PPTX context with AI images, representative multi-source custom rendering, and two local types.",
@@ -319,7 +335,7 @@
"stage.generate.image.ai-two-types"
],
"files": [],
"max_tokens": 121450
"max_tokens": 123000
},
"route.generate.flat-in-hand-image": {
"description": "Generate-PPTX context with provided, placeholder, or formula images and no acquisition role.",
@@ -331,7 +347,7 @@
"stage.generate.executor.notes"
],
"files": [],
"max_tokens": 94750
"max_tokens": 96300
},
"route.generate.flat-web-image": {
"description": "Generate-PPTX context with web image acquisition.",
@@ -344,7 +360,7 @@
"stage.generate.image.web"
],
"files": [],
"max_tokens": 102000
"max_tokens": 103575
},
"route.enhance-native-pptx.audio": {
"description": "Enhance Native PPTX with the shared narration-audio stage.",
@@ -354,7 +370,7 @@
"stage.shared.generate-audio"
],
"files": [],
"max_tokens": 11000
"max_tokens": 11125
},
"route.generate.beautify-flat-no-image": {
"description": "Generate-PPTX 1:1 beautify profile on the flat no-image path.",
@@ -364,7 +380,7 @@
"profile.generate.beautify-pptx"
],
"files": [],
"max_tokens": 88110
"max_tokens": 89325
},
"route.generate.flat-no-image-chart": {
"description": "Default flat Generate-PPTX path with chart authoring, native-data replacement, and verification.",
@@ -377,7 +393,7 @@
"stage.generate.verify-charts"
],
"files": [],
"max_tokens": 105250
"max_tokens": 106550
},
"route.generate.brand-flat-no-image": {
"description": "Brand-preset flat Generate-PPTX path without image acquisition.",
@@ -389,7 +405,7 @@
"stage.generate.template.brand"
],
"files": [],
"max_tokens": 88300
"max_tokens": 89500
},
"route.generate.deck-structured-no-image": {
"description": "Deck-preset structured Generate-PPTX path without image acquisition.",
@@ -401,7 +417,7 @@
"stage.generate.template.deck"
],
"files": [],
"max_tokens": 98100
"max_tokens": 99425
},
"route.generate.layout-structured-no-image": {
"description": "Layout-preset structured Generate-PPTX path without image acquisition.",
@@ -413,7 +429,7 @@
"stage.generate.template.layout"
],
"files": [],
"max_tokens": 98550
"max_tokens": 99875
},
"route.generate.topic-only-flat-no-image": {
"description": "Topic research followed by the default flat no-image Generate-PPTX path.",
@@ -423,7 +439,7 @@
"route.generate.flat-no-image"
],
"files": [],
"max_tokens": 82650
"max_tokens": 83875
},
"stage.generate.executor.flat": {
"description": "Incremental flat Executor core with representative multi-source custom mode/style execution.",
@@ -533,7 +549,7 @@
"stage.generate.executor.image"
],
"files": [],
"max_tokens": 58350
"max_tokens": 59100
},
"stage.generate.topic-research": {
"description": "Gap-targeted factual intake stage.",
@@ -565,7 +581,7 @@
"files": [
"skills/ppt-master/workflows/stages/verify-charts.md"
],
"max_tokens": 6200
"max_tokens": 6250
},
"stage.generate.chart-authoring": {
"description": "Conditional visualization-library authoring guidance.",
@@ -661,7 +677,7 @@
"stage.generate.strategist.template"
],
"files": [],
"max_tokens": 59200
"max_tokens": 60250
},
"route.generate.planning-template-ai": {
"description": "Explicit template workspace plus confirmed AI-image planning.",
@@ -671,7 +687,7 @@
"route.generate.planning-ai"
],
"files": [],
"max_tokens": 71400
"max_tokens": 72700
},
"stage.generate.executor.chart": {
"description": "Conditional chart/table page execution rules.",
@@ -687,7 +703,7 @@
"files": [
"skills/ppt-master/references/strategist-image.md"
],
"max_tokens": 1950
"max_tokens": 2250
},
"stage.generate.strategist.image-layout": {
"description": "Non-formula image planning plus the image-layout pattern catalog required for Section VIII rows.",
@@ -698,7 +714,7 @@
"files": [
"skills/ppt-master/references/image-layout-patterns.md"
],
"max_tokens": 8000
"max_tokens": 8250
},
"stage.generate.strategist.template": {
"description": "Conditional Strategist module for an explicitly installed template workspace.",
@@ -725,7 +741,7 @@
"skills/ppt-master/references/image-layout-spec.md",
"skills/ppt-master/references/svg-image-embedding.md"
],
"max_tokens": 11600
"max_tokens": 11625
},
"stage.generate.executor.web-image": {
"description": "Conditional sourced-image attribution layered on image execution.",
@@ -736,7 +752,7 @@
"files": [
"skills/ppt-master/references/executor-web-image.md"
],
"max_tokens": 12000
"max_tokens": 12050
},
"stage.generate.executor.notes": {
"description": "Post-SVG speaker-notes generation rules.",
@@ -52,7 +52,7 @@ from svg_to_pptx.canvas_contract import (
)
try:
from update_spec import parse_lock as _parse_spec_lock
from project_specs import parse_spec_lock as _parse_spec_lock
except ImportError:
_parse_spec_lock = None # spec_lock anchor comparison will be skipped
@@ -889,6 +889,7 @@ PPT_SAFE_FONTS = {
# is prompt-owned; Checker only verifies that a used value is close to at least
# one declared size anchor.
FONT_SIZE_ANCHOR_TOLERANCE_PX = 2.0
SPARSE_UNDECLARED_FONT_SIZE_MAX_OCCURRENCES = 2
# Oversampling alone does not imply distortion and is often harmless for small
# logos. Warn about downscaling only when the source also has material on-disk
@@ -1160,6 +1161,8 @@ class SVGQualityChecker:
'fonts': defaultdict(set),
'sizes': defaultdict(set),
}
self._undeclared_size_occurrences: Counter[str] = Counter()
self._undeclared_size_counts_ready = False
self._lock_seen = False # True once we locate at least one spec_lock.md
self._source_manifest_cache: Dict[Path, Dict] = {}
# Template-mode aggregation (populated by check_directory when
@@ -1370,10 +1373,10 @@ class SVGQualityChecker:
self._check_semantic_markers(root, svg_path, result)
# 9. Compare values with spec_lock anchors. Additional colors
# and fonts are informational. Generated-page type sizes
# outside every role band are errors; other spec-backed SVG
# locations retain advisory review. Templates do not ship a
# spec_lock.md, so skip in template mode to avoid noise.
# and fonts are informational. Generated-page type sizes may
# stay sparse twice; the third occurrence is an error. Other
# spec-backed SVG locations retain advisory review. Templates
# do not ship a spec_lock.md, so skip in template mode.
if not self.template_mode:
self._check_spec_lock_alignment(
content,
@@ -3703,6 +3706,12 @@ class SVGQualityChecker:
return
icons_dir, fallback_dir = _icon_search_dirs_for_svg(svg_path)
require_project_local = self._requires_project_local_icons(svg_path)
project_icons_dir = (
_project_root_for_svg_path(svg_path) / 'icons'
if _project_root_for_svg_path is not None
else None
)
seen = set()
for elem in placeholders:
icon_name = (elem.get('data-icon') or '').strip()
@@ -3713,6 +3722,20 @@ class SVGQualityChecker:
continue
seen.add(icon_name)
if require_project_local and project_icons_dir is not None:
local_path, _ = _resolve_icon_path(
icon_name,
project_icons_dir,
None,
)
if not local_path.exists():
result['errors'].append(
f"Icon is not prepared in the project: {icon_name} "
f"(expected under {project_icons_dir}); return to "
"Strategist preparation instead of using the global fallback"
)
continue
icon_path, _ = _resolve_icon_path(icon_name, icons_dir, fallback_dir)
if not icon_path.exists():
fallback_msg = f", then {fallback_dir}" if fallback_dir else ""
@@ -3742,6 +3765,31 @@ class SVGQualityChecker:
result['info'].get('native_icon_payload_refs', 0) + hydrated
)
@staticmethod
def _requires_project_local_icons(svg_path: Path) -> bool:
"""Return whether a generated page belongs to a versioned project."""
if svg_path.parent.name != 'svg_output' or _project_root_for_svg_path is None:
return False
lock_path = _project_root_for_svg_path(svg_path) / 'spec_lock.md'
try:
first_line = next(
(
line.strip()
for line in lock_path.read_text(encoding='utf-8-sig').splitlines()
if line.strip()
),
'',
)
except OSError:
return False
return bool(
re.fullmatch(
r'<!--[ \t]+ppt-master-schema:[ \t]*spec-lock/v[1-9][0-9]*[ \t]+-->',
first_line,
re.IGNORECASE,
)
)
def _check_unsupported_visual_elements(
self,
root: ET.Element,
@@ -4734,7 +4782,8 @@ class SVGQualityChecker:
self,
) -> Tuple[set[str], set[str], set[str]]:
"""Return color/font/size values owned by the selected mirror page."""
if self._active_prototype_root() is None:
prototype_root = self._active_prototype_root()
if prototype_root is None:
return set(), set(), set()
try:
content = self._active_prototype_path.read_text(encoding='utf-8')
@@ -4760,13 +4809,180 @@ class SVGQualityChecker:
for value in self._font_family_values(content)
if self._normalize_font_stack(value)
}
sizes = {
self._normalize_size(value)
for value in self._svg_property_values(content, 'font-size')
if self._normalize_size(value)
}
sizes = set(self._effective_text_size_counts(prototype_root))
return colors, fonts, sizes
def _declared_typography_size_anchors(
self,
lock: Dict,
) -> Tuple[Dict, set[str], List[float], List[str]]:
"""Return valid declared size anchors and malformed lock rows."""
typography = lock.get('typography', {})
positive_numeric_re = re.compile(
r'^(?=.*[1-9])(?:[0-9]+(?:\.[0-9]+)?|\.[0-9]+)$'
)
locked_sizes: set[str] = set()
anchor_sizes: List[float] = []
invalid_sizes: List[str] = []
for key, raw_value in typography.items():
if key == 'font_family' or key.endswith('_family'):
continue
value = raw_value.strip()
if positive_numeric_re.fullmatch(value) is None:
invalid_sizes.append(f"{key}: {raw_value}")
continue
try:
anchor = float(value)
except (TypeError, ValueError):
invalid_sizes.append(f"{key}: {raw_value}")
continue
if not math.isfinite(anchor) or anchor <= 0:
invalid_sizes.append(f"{key}: {raw_value}")
continue
locked_sizes.add(self._canonical_font_size_key(anchor))
anchor_sizes.append(anchor)
return typography, locked_sizes, anchor_sizes, invalid_sizes
def _count_undeclared_size_occurrences(
self,
root: ET.Element,
*,
locked_sizes: set[str],
anchor_sizes: List[float],
prototype_sizes: set[str],
) -> Counter[str]:
"""Count text objects using valid sizes outside all declared bands."""
counts: Counter[str] = Counter()
if not locked_sizes:
return counts
for value, occurrence_count in self._effective_text_size_counts(root).items():
if value in prototype_sizes and value not in locked_sizes:
continue
if value in locked_sizes:
continue
try:
used_px = float(value)
except (TypeError, ValueError):
continue
if not math.isfinite(used_px) or used_px < 0:
continue
if any(
abs(used_px - anchor_px) <= FONT_SIZE_ANCHOR_TOLERANCE_PX
for anchor_px in anchor_sizes
):
continue
counts[value] += occurrence_count
return counts
def _effective_text_size_counts(self, root: ET.Element) -> Counter[str]:
"""Count each effective size once per non-empty SVG text object."""
counts: Counter[str] = Counter()
if _resolve_project_font_sizes is None:
return counts
working_root = root
if (
_expand_local_use_references is not None
and _UseExpansionError is not None
):
expanded_root = copy.deepcopy(root)
try:
_expand_local_use_references(expanded_root)
except _UseExpansionError:
pass
else:
working_root = expanded_root
try:
effective_sizes = _resolve_project_font_sizes(working_root)
except ValueError:
return counts
def collect_text_object_sizes(element: ET.Element) -> set[str]:
values: set[str] = set()
def visit(node: ET.Element) -> None:
if (node.text or '').strip():
values.add(
self._canonical_font_size_key(effective_sizes[id(node)])
)
for child in node:
visit(child)
if (child.tail or '').strip():
values.add(
self._canonical_font_size_key(
effective_sizes[id(node)]
)
)
visit(element)
return values
definition_containers = {
'clippath',
'defs',
'marker',
'mask',
'pattern',
'symbol',
}
def visit_visible(element: ET.Element) -> None:
local_name = _local_name(element).casefold()
if local_name in definition_containers:
return
if local_name == 'text':
counts.update(collect_text_object_sizes(element))
return
for child in element:
visit_visible(child)
visit_visible(working_root)
return counts
@staticmethod
def _canonical_font_size_key(value: float) -> str:
"""Canonicalize equivalent numeric spellings for deck-wide counting."""
return format(value, '.12g')
def _prepare_undeclared_size_occurrences(
self,
svg_files: List[Path],
) -> None:
"""Pre-count sparse undeclared sizes before per-file diagnostics."""
previous_prototype = self._active_prototype_path
try:
for svg_path in svg_files:
lock = self._get_spec_lock(svg_path)
if lock is None:
continue
_typography, locked_sizes, anchor_sizes, _invalid = (
self._declared_typography_size_anchors(lock)
)
self._active_prototype_path = self._prototype_by_output.get(
svg_path.resolve()
)
_colors, _fonts, prototype_sizes = (
self._prototype_drift_allowances()
)
try:
content = svg_path.read_text(encoding='utf-8')
except OSError:
continue
try:
root = ET.fromstring(content)
except ET.ParseError:
continue
self._undeclared_size_occurrences.update(
self._count_undeclared_size_occurrences(
root,
locked_sizes=locked_sizes,
anchor_sizes=anchor_sizes,
prototype_sizes=prototype_sizes,
)
)
finally:
self._active_prototype_path = previous_prototype
self._undeclared_size_counts_ready = True
def _check_spec_lock_alignment(
self,
content: str,
@@ -4780,12 +4996,13 @@ class SVGQualityChecker:
Covers colors (fill / stroke / stop-color / flood-color / pattern
metadata), font-family, and font-size.
Additional colors and font families are valid contextual authoring and
are recorded as information. Font sizes outside every declared role
anchor's ±2px band are errors in generated ``svg_output`` pages and
warnings in other spec-backed SVG locations. Exact mirror-prototype
values remain inherited information. Exact values are accumulated in
self._anchor_value_summary for the end-of-run aggregation. When
spec_lock.md is missing, silently skip this local comparison; the
are recorded as information. A valid undeclared display size may occur
at most twice across generated pages; its third occurrence makes it a
recurring role and blocks ``svg_output`` until the role is declared.
Structural text still maps to declared role bands. Exact mirror-
prototype values remain inherited information. Exact values are
accumulated in self._anchor_value_summary for end-of-run aggregation.
When spec_lock.md is missing, silently skip this local comparison; the
Generate route's required-artifact gate owns whether execution may begin.
"""
lock = self._get_spec_lock(svg_path)
@@ -4834,20 +5051,15 @@ class SVGQualityChecker:
locked_colors = set(allowed_colors)
allowed_colors.update(prototype_colors)
typo = lock.get('typography', {})
numeric_size_re = re.compile(r'^(?:\d+(?:\.\d+)?|\.\d+)$')
invalid_lock_sizes = []
for k, v in typo.items():
if k == 'font_family' or k.endswith('_family'):
continue
if not numeric_size_re.fullmatch(v.strip()):
invalid_lock_sizes.append(f"{k}: {v}")
typo, locked_sizes, anchor_sizes, invalid_lock_sizes = (
self._declared_typography_size_anchors(lock)
)
if invalid_lock_sizes:
shown = ', '.join(invalid_lock_sizes[:5])
more = len(invalid_lock_sizes) - 5
suffix = f" (+{more} more)" if more > 0 else ""
result['errors'].append(
f"spec_lock typography sizes must be unitless numeric px values; "
f"spec_lock typography sizes must be positive finite unitless px values; "
f"found {shown}{suffix}."
)
@@ -4874,20 +5086,6 @@ class SVGQualityChecker:
# Sizes: declared slots are anchors. Checker cannot infer which role a
# text node carries, so it uses the union of their ±2px bands as a cheap
# numeric safety net; prompt rules own semantic role mapping.
allowed_sizes = set()
anchor_sizes = []
for k, v in typo.items():
if k == 'font_family' or k.endswith('_family'):
continue
normalized_size = self._normalize_size(v)
allowed_sizes.add(normalized_size)
try:
anchor_sizes.append(float(normalized_size))
except (ValueError, TypeError):
pass
locked_sizes = set(allowed_sizes)
allowed_sizes.update(prototype_sizes)
# Scan SVG for used values
color_drifts = set()
inherited_colors = set()
@@ -4925,26 +5123,17 @@ class SVGQualityChecker:
):
inherited_fonts.add(val)
size_drifts = set()
size_drift_counts = self._count_undeclared_size_occurrences(
root,
locked_sizes=locked_sizes,
anchor_sizes=anchor_sizes,
prototype_sizes=prototype_sizes,
)
size_drifts = set(size_drift_counts)
inherited_sizes = set()
for raw_value in self._svg_property_values(content, 'font-size'):
val = self._normalize_size(raw_value)
for val in self._effective_text_size_counts(root):
if val in prototype_sizes and val not in locked_sizes:
inherited_sizes.add(val)
continue
if not allowed_sizes or val in allowed_sizes:
continue
try:
used_px = float(val)
except (TypeError, ValueError):
size_drifts.add(val)
continue
if any(
abs(used_px - anchor_px) <= FONT_SIZE_ANCHOR_TOLERANCE_PX
for anchor_px in anchor_sizes
):
continue
size_drifts.add(val)
# Record in run-wide aggregation. Colors/fonts beyond the anchor set are
# contextual values, not release issues. Generated-page sizes enforce
@@ -4965,22 +5154,45 @@ class SVGQualityChecker:
if contextual_values:
result['info']['contextual_values'] = contextual_values
if size_drifts:
sparse_sizes = {}
recurring_sizes = {}
for value, local_count in size_drift_counts.items():
total_count = (
self._undeclared_size_occurrences.get(value, local_count)
if self._undeclared_size_counts_ready
else local_count
)
target = (
sparse_sizes
if total_count <= SPARSE_UNDECLARED_FONT_SIZE_MAX_OCCURRENCES
else recurring_sizes
)
target[value] = total_count
if sparse_sizes:
result['info']['sparse_typography_sizes'] = {
value: count for value, count in sorted(sparse_sizes.items())
}
if recurring_sizes:
shown = ', '.join(
f"{value} ({count} occurrences)"
for value, count in sorted(recurring_sizes.items())
)
size_issue = (
f"{len(size_drifts)} font-size value(s) fall outside ±2px of "
"every declared spec_lock role anchor (see anchor comparison "
"summary)"
f"undeclared font-size {shown} exceeds the sparse-display limit "
f"of {SPARSE_UNDECLARED_FONT_SIZE_MAX_OCCURRENCES} occurrences"
)
if svg_path.parent.name == 'svg_output':
result['errors'].append(
"spec_lock typography-size violation: "
f"{size_issue}. Reflow within a declared role band or repair "
"the Design Spec and spec_lock with a justified named role; "
"do not add a role only to silence the checker."
"spec_lock typography-size recurrence: "
f"{size_issue}. Structural text must return to its declared "
"role band; a genuinely recurring display treatment needs a "
"justified named role in the Design Spec and spec_lock."
)
else:
result['warnings'].append(
f"spec_lock typography-size review: {size_issue}"
f"spec_lock typography-size recurrence review: {size_issue}"
)
inherited_parts = []
if inherited_colors:
@@ -5178,6 +5390,8 @@ class SVGQualityChecker:
"""
dir_path = Path(directory)
self._has_incomplete_page_roster = False
self._undeclared_size_occurrences = Counter()
self._undeclared_size_counts_ready = False
if not dir_path.exists():
print(f"[ERROR] Directory does not exist: {directory}")
@@ -5249,6 +5463,8 @@ class SVGQualityChecker:
return []
self._configure_prototype_context(dir_path, svg_files)
if not self.template_mode:
self._prepare_undeclared_size_occurrences(svg_files)
directory_expected_viewbox: str | None = None
directory_expected_label = "the first SVG canvas"
@@ -6720,8 +6936,8 @@ class SVGQualityChecker:
self._anchor_value_summary[category]
for category in ('colors', 'fonts')
)
has_size_issues = bool(self._anchor_value_summary['sizes'])
if not has_contextual and not has_size_issues:
has_undeclared_sizes = bool(self._anchor_value_summary['sizes'])
if not has_contextual and not has_undeclared_sizes:
print(
"\n[OK] spec_lock anchor comparison: no additional contextual "
"colors/fonts or out-of-band font sizes"
@@ -6752,19 +6968,31 @@ class SVGQualityChecker:
"recurring named semantic role."
)
if has_size_issues:
if has_undeclared_sizes:
print(
"\nTypography sizes outside every declared role anchor ±2px "
"(blocking in generated svg_output; advisory elsewhere):"
"(up to 2 occurrences are sparse; the 3rd is recurring):"
)
entries = sorted(
self._anchor_value_summary['sizes'].items(),
key=lambda item: (-len(item[1]), item[0]),
)
for val, files in entries:
count = len(files)
suffix = "file" if count == 1 else "files"
print(f" {val} ({count} {suffix})")
occurrences = self._undeclared_size_occurrences.get(
val,
len(files),
)
file_count = len(files)
file_suffix = "file" if file_count == 1 else "files"
policy = (
"sparse"
if occurrences <= SPARSE_UNDECLARED_FONT_SIZE_MAX_OCCURRENCES
else "recurring — declare a role"
)
print(
f" {val} ({occurrences} occurrences in {file_count} "
f"{file_suffix}; {policy})"
)
def _percentage(self, count: int) -> int:
"""Calculate percentage"""
@@ -6874,8 +7102,8 @@ class SVGQualityChecker:
introduced.append(item)
# Keep the legacy `drift` JSON field for report compatibility. Its
# colors/fonts entries are informational anchor comparisons; size
# entries produce errors for generated pages and warnings elsewhere.
# colors/fonts entries are informational anchor comparisons; sparse
# size entries are informational until their third occurrence.
drift = {
category: {
value: sorted(files)
@@ -80,7 +80,7 @@ _PPTX_STRUCTURE_SECTION_RE = re.compile(
r"(?ms)^##[ \t]+pptx_structure[ \t]*\r?\n(.*?)(?=^##[ \t]+|\Z)"
)
_PPTX_STRUCTURE_MODE_RE = re.compile(
r"(?m)^-[ \t]+mode[ \t]*:[ \t]*([^\s#]+)[ \t]*(?:#.*)?$"
r"(?m)^-[ \t]+mode[ \t]*:[ \t]*([^#\r\n]*?)[ \t]*(?:#.*)?$"
)
_LEGACY_PPTX_STRUCTURE_MODES = frozenset({
'baseline',
@@ -466,7 +466,7 @@ def _print_postflight_receipt(receipt: _PostflightReceipt) -> None:
def _declared_pptx_structure_mode(project_path: Path) -> str | None:
"""Return the explicitly locked SVG export mode, without legacy fallback."""
"""Return the explicitly locked SVG export mode, if the lock declares one."""
lock_path = project_path / 'spec_lock.md'
try:
content = lock_path.read_text(encoding='utf-8')
@@ -493,22 +493,40 @@ def _declared_canvas_viewbox(project_path: Path) -> str | None:
return value.strip() if isinstance(value, str) and value.strip() else None
def _print_structure_contract_error(mode: str | None) -> None:
"""Explain how to replace a legacy or absent SVG structure contract."""
label = repr(mode) if mode else 'missing (legacy implicit baseline)'
def _print_structure_contract_error(
mode: str | None,
*,
requested_mode: str | None = None,
) -> None:
"""Explain an unsupported mode or a structured-export lock mismatch."""
label = repr(mode) if mode is not None else 'missing'
if requested_mode == 'structured':
print(
"Error: release SVG export requires an explicit spec_lock.md "
"pptx_structure.mode: flat (style reference / free design / brand-only) "
"or structured (mirror/layout reuse); found " + label + ".",
"Error: --pptx-structure structured requires an explicit "
"spec_lock.md pptx_structure.mode: structured contract; found "
+ label + ".",
file=sys.stderr,
)
print(
" Style-reference, free-design, and brand-only projects must write a "
"new mode: flat lock and regenerate project-canonical flat SVG pages. "
" A legacy lock without pptx_structure.mode defaults only to flat. "
"Mirror/layout reuse must first create a current template workspace "
"through skills/ppt-master/workflows/create-template.md, then generate "
"new structured SVG pages. Existing PPTX/SVG files are not upgraded "
"in place.",
"new structured SVG pages.",
file=sys.stderr,
)
return
print(
"Error: unsupported spec_lock.md pptx_structure.mode " + label + ". "
"Current release modes are flat (style reference / free design / "
"brand-only) and structured (mirror/layout reuse).",
file=sys.stderr,
)
print(
" A legacy lock with no pptx_structure.mode defaults to flat. "
"Explicit legacy or unknown values are not inferred. Mirror/layout reuse "
"must first create a current template workspace "
"through skills/ppt-master/workflows/create-template.md, then generate "
"new structured SVG pages.",
file=sys.stderr,
)
@@ -735,8 +753,8 @@ Recorded narration:
default=None,
help=(
'PPTX structure strategy for native export. Omitting this flag reads '
'spec_lock.md: flat is the style-reference/free-design/brand-only '
'release mode and '
'spec_lock.md; a legacy lock without pptx_structure.mode defaults to '
'flat. Flat is the style-reference/free-design/brand-only release mode and '
'builds one clean project-owned Master plus Blank Layout while keeping '
'all SVG objects slide-local; structured is the mirror/layout reuse '
'mode and requires complete explicit metadata. baseline, template, '
@@ -837,17 +855,38 @@ Recorded narration:
structure_lock = None
native_structure_contract = None
pptx_structure = args.pptx_structure
lock_path = project_path / 'spec_lock.md'
if not lock_path.is_file():
print(
"Error: spec_lock.md is required for release SVG export",
file=sys.stderr,
)
return 1
declared_structure_mode = _declared_pptx_structure_mode(project_path)
if pptx_structure in _LEGACY_PPTX_STRUCTURE_MODES:
_print_structure_contract_error(pptx_structure)
return 1
if pptx_structure is None:
if declared_structure_mode not in _RELEASE_PPTX_STRUCTURE_MODES:
if (
declared_structure_mode is not None
and declared_structure_mode not in _RELEASE_PPTX_STRUCTURE_MODES
):
_print_structure_contract_error(declared_structure_mode)
return 1
if pptx_structure is None:
if declared_structure_mode is None:
pptx_structure = 'flat'
print(
"Warning: spec_lock.md has no pptx_structure.mode; using flat "
"compatibility mode.",
file=sys.stderr,
)
else:
pptx_structure = declared_structure_mode
elif pptx_structure == 'structured' and declared_structure_mode != 'structured':
_print_structure_contract_error(declared_structure_mode)
_print_structure_contract_error(
declared_structure_mode,
requested_mode='structured',
)
return 1
if (
@@ -28,6 +28,7 @@ import sys
from pathlib import Path
from console_encoding import configure_utf8_stdio
from project_specs import parse_spec_lock as parse_lock
configure_utf8_stdio()
@@ -35,29 +36,6 @@ HEX_RE = re.compile(r"^#(?:[0-9A-Fa-f]{3,4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$")
FONT_FAMILY_RE = re.compile(r"""(font-family\s*=\s*)(["'])(.*?)\2""")
def parse_lock(lock_path: Path) -> dict[str, dict[str, str]]:
"""Return {section_name: {key: value}} parsed from spec_lock.md.
The format is:
## section
- key: value
"""
sections: dict[str, dict[str, str]] = {}
current: str | None = None
for raw in lock_path.read_text(encoding="utf-8").splitlines():
line = raw.rstrip()
if line.startswith("## "):
current = line[3:].strip()
sections.setdefault(current, {})
continue
if current is None:
continue
m = re.match(r"^-\s+([A-Za-z0-9_]+)\s*:\s*(.+?)\s*$", line)
if m:
sections[current][m.group(1)] = m.group(2)
return sections
def rewrite_lock(lock_path: Path, section: str, key: str, new_value: str) -> None:
"""Rewrite the single `- key: old_value` line under `## section`."""
lines = lock_path.read_text(encoding="utf-8").splitlines(keepends=True)
@@ -6,11 +6,11 @@ This directory contains the standardized SVG visualization templates used by PPT
[`charts_index.json`](./charts_index.json) is the single source of truth for the library: total count + one selection-rule `summary` per template (format: `"Pick for X. Skip if Y (use other_key)."`). [`chart_recall.py`](../../scripts/chart_recall.py) can recall a bounded candidate set from this live registry without maintaining a second category or keyword index.
For one planned page, provide 3-8 English semantic content-shape tags, then inspect the positive-scoring summaries plus the explicit `no-template-match` option. The requested limit is a cap, not a padding target, and lexical confidence never expands the output automatically. When terminology mismatch or structural ambiguity suggests that bounded recall missed a relevant template, rerun with `--semantic-fallback` for an explicit full-catalog review. The flag is optional, not a gate before `no-template-match`. `no-template-match` stays inside planning: it creates no §VII row or `page_charts` entry, and the chosen fallback is described in the page's §IX block. See [`chart-recall.md`](../../scripts/docs/chart-recall.md). The Generate workflow remains the authority for when Strategist uses this helper; maintainers may still open the registry directly when editing or auditing the catalog.
For one planned page, provide 3-8 English semantic content-shape tags, then inspect the positive-scoring summaries plus the explicit `no-template-match` option. The requested limit is a cap, not a padding target, and lexical confidence never expands the output automatically. At `high` / `medium`, the Strategist may keep `no-template-match` when no bounded candidate fits. At `low` / `none`, a fitting candidate needs no expansion, but the Strategist must rerun the same query once with `--semantic-fallback` before keeping `no-template-match`. A selected result creates one `Page | Template | Usage` row in §VII and one matching `page_charts` entry; its path is derived from the key, while Usage is concise page-local intent and detailed adaptation remains in §IX. The negative result stays inside planning: it creates no §VII row or `page_charts` entry, and the chosen fallback is described in the page's §IX block. See [`chart-recall.md`](../../scripts/docs/chart-recall.md). The Generate workflow remains the authority for when Strategist uses this helper; maintainers may still open the registry directly when editing or auditing the catalog.
## Authoring contract
[`CHART_STYLE_GUIDE.md`](./CHART_STYLE_GUIDE.md) owns readable standalone SVG, semantic and structural fidelity, root-group bounds, data encoding, and neutral previews. The active Design Spec and `spec_lock.md` own final palette, typography, containers, effects, and chrome. The reference preserves visualization semantics; its preview styling, example frames, grouping implementation, item count, and capacity are adaptable to the actual authored content.
[`CHART_STYLE_GUIDE.md`](./CHART_STYLE_GUIDE.md) owns readable standalone SVG, semantic and structural fidelity, root-group bounds, data encoding, and neutral previews. The active Design Spec §IX and source data own the generated page's semantics; `spec_lock.md` owns stable project anchors. A selected SVG is a page-local structural reference, while its type, styling, frames, grouping, item count, capacity, and geometry remain adaptable.
## Visualization output model
@@ -97,7 +97,6 @@ Use these exact subsections and field shapes:
- **Title stack**: <complete ordered stack>
- **Body stack**: <complete ordered stack>
- **Role rationale**: <why the confirmed Title/Body system fits the content; name justified recurring family overrides, or state that none is needed>
### Font Size Hierarchy
@@ -126,13 +125,14 @@ Use these exact subsections and field shapes:
## VI. Icon Usage Specification
- **Library**: <confirmed library, custom, or none>
- **Primary bundled library**: <one of chunk-filled / tabler-filled / tabler-outline / phosphor-duotone, or none>
- **Brand-logo library**: <simple-icons when selected for real brand marks; omit otherwise>
| Purpose | Icon Path | Page |
| --- | --- | --- |
```
Preserve the confirmed Title/Body system, then add every Strategist-established recurring family override justified by the completed page plan. Append the same semantic role to the Font Plan table and add `- **<Role> stack**: <complete ordered stack>`. Typical optional roles include `Annotation`, `Footer`, `Footnote`, `Data`, `Emphasis`, `Quote`, and `Code`; add only roles that recur and intentionally differ. `Role rationale` records the decision but does not itself become a lock field. Do not collapse distinct Title/Body stacks or discard a declared optional role. Treat every Font Size Hierarchy value as a role anchor: Executor may adjust one occurrence within anchor `±2px`, but a new semantic role or any planned feature outside that band needs its own row. Add every recurring palette role and typography-size anchor established by the plan; do not enumerate one-off paint or font-family garnish. For confirmed custom directions, add the applicable `Mode References`, `Mode Behavior`, `Visual Style References`, and `Visual Style Behavior` lines under Theme Style. Include `Stroke Width` under §VI only for a stroke library. Leave the §VI table empty when no icons are used.
Preserve the confirmed Title/Body system, then add every Strategist-established recurring family override justified by the completed page plan. Append the same semantic role to the Font Plan table and add `- **<Role> stack**: <complete ordered stack>`. Typical optional roles include `Annotation`, `Footer`, `Footnote`, `Data`, `Emphasis`, `Quote`, and `Code`; add only roles that recur and intentionally differ. Add one compact `Role rationale` only when at least one such override is declared; otherwise omit it. The rationale does not become a lock field. Do not collapse distinct Title/Body stacks or discard a declared optional role. Treat every Font Size Hierarchy value as a role anchor: Executor may adjust one occurrence within anchor `±2px`; a short non-structural Hero/Display size may stay unlisted only while the same value is planned at most twice, and its third occurrence needs a named row. Add every recurring palette role and typography-size anchor established by the plan; do not enumerate one-off paint or font-family garnish. For confirmed custom directions, add the applicable `Mode References`, `Mode Behavior`, `Visual Style References`, and `Visual Style Behavior` lines under Theme Style. Include `Stroke Width` under §VI only for a stroke library. `simple-icons` may accompany the one primary bundled library and is recorded only when real brand marks were selected. The icon table records planned usage, but user-provided, template-carried, imported, custom, and other prepared SVGs under the project `icons/` directory remain usable without being forced into that stylistic selection. Leave the §VI table empty when no icons are used.
When §VIII contains any `Acquire Via: ai` row, add this subsection under §III and preserve the complete confirmed AI direction:
@@ -153,8 +153,8 @@ Use the §VII table only when at least one real catalog reference is selected. A
```markdown
## VII. Visualization Reference List
| Page | Template | Path | Summary-quote (verbatim) | Usage |
| --- | --- | --- | --- | --- |
| Page | Template | Usage |
| --- | --- | --- |
## VIII. Image Resource List
@@ -162,7 +162,7 @@ Use the §VII table only when at least one real catalog reference is selected. A
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
```
§VII is a positive reference inventory. Every row uses a returned catalog key, its real `templates/charts/<key>.svg` path, and its verbatim summary. Never emit an empty §VII, a `no-template-match` / `n/a` placeholder row, or prose explaining that no reference exists. When recall finds no fit, omit that page from §VII and describe the custom fallback in the page's §IX `Visualization` / `Layout`. List real runners-up only for pages with a selected §VII reference, as `- <returned_key> | rejected for P<NN>: <page-specific reason>`.
§VII is an optional page-local reference list. Each row records the page, catalog key, and a short semantic Usage—not geometry. The key derives `templates/charts/<key>.svg`; §IX remains authoritative over final type and realization. Omit an empty §VII and never add path, summary, runners-up, `no-template-match`, or `n/a`. Put unmatched fallbacks in §IX. Legacy wider rows remain readable; new specs use these three columns.
For every independent data chart or pure text-grid table, add `- **Native-ready**: yes|no` to its §IX Slide block. Choose `yes` only when the confirmed requirement or artifact afterlife benefits from an editable native data object; otherwise use `no`. Conceptual visualizations and incidental sparklines, KPI trends, or insets omit this field and remain ordinary SVG.
@@ -194,7 +194,9 @@ Write one ordered Slide block per page. Slide count and order must equal §I `Pa
- **Presentation purpose**: <inform, persuade, inspire, instruct, report, or resolved combination>
```
Add `Visualization` and `Images` to a Slide block when it consumes §VII/§VIII rows or uses a page-local visualization. State whether `Visualization` is data-driven when source values determine geometry; this page-level declaration remains authoritative even when no catalog reference fits. Add `Native-ready: yes|no` only for independent data charts or pure text-grid tables. Add `Fact IDs` for sourced claims and `Data class: scenario` for invented demo values. Add `Cover impact` to P01 except on preservation paths; add `Closing impact` only when the final page genuinely resolves the deck. Roster ids/count/order and final content are authoritative. Layout, cover/closing composition, and image/chart patterns are References whose selected semantics remain fixed while Executor realizes their geometry, hierarchy, treatment, and sparse local garnish.
Add `Visualization` and `Images` to a Slide block when it consumes §VII/§VIII rows or uses a page-local visualization. State whether `Visualization` is data-driven when source values determine geometry; this page-level declaration remains authoritative even when no catalog reference fits. Add `Native-ready: yes|no` only for independent data charts or pure text-grid tables. Add `Fact IDs` for sourced claims and `Data class: scenario` for invented demo values. Add `Cover impact` to P01 except on preservation paths; add `Closing impact` only when the final page genuinely resolves the deck. Roster ids/count/order and final content are authoritative. Image patterns preserve their selected semantic composition; chart rows only offer page-local references. Executor owns geometry, hierarchy, treatment, and sparse local garnish.
For free-design pages, describe `Layout` through relationships, hierarchy, regions, and column spans; do not prescribe element-level `x`, `y`, `width`, or `height` or duplicate the global geometry in §II/§V. Exact coordinates belong to Executor SVG authoring. Preserve literal geometry only when the user explicitly requires it or a mirror/template preservation contract owns it.
---
@@ -1,6 +1,6 @@
# SVG Icon Library
This directory provides **11,600+ high-quality SVG icons** across five libraries that can be directly embedded into SVG files generated by PPT Master. Four are stylistic libraries (pick **one** per deck); one is a brand-logo library (`simple-icons`) used as an inset alongside the chosen stylistic library.
This directory provides **11,600+ high-quality SVG icons** across five libraries that can be directly embedded into SVG files generated by PPT Master. When the Strategist selects bundled generic icons, it chooses at most one primary library from the four stylistic libraries; the brand-logo library (`simple-icons`) may be selected alone or alongside it.
## Libraries
@@ -22,7 +22,7 @@ This directory is the **global library**. At selection time the Strategist copie
python3 skills/ppt-master/scripts/icon_sync.py <project_path> tabler-outline/home tabler-outline/bulb simple-icons/github
```
Missing names and batches that mix stylistic libraries exit non-zero; `simple-icons` may coexist for real brand marks. `finalize_svg.py embed-icons` embeds **project-first** from `<project>/icons/`, with per-icon global fallback.
Missing names and a single selection batch that mixes the four stylistic libraries exit non-zero; `simple-icons` may coexist for real brand marks. Once files are under `<project>/icons/`, they form the prepared project asset pool and may be combined freely with user-provided, custom, or imported icons. `finalize_svg.py embed-icons` embeds **project-first**; its per-icon global fallback exists for legacy compatibility, not new asset discovery.
**Custom icons**: drop your own `.svg` into `<project>/icons/<lib>/` (any `<lib>`, e.g. `custom/`) and reference it as `data-icon="<lib>/<name>"` — it embeds like any library icon.
@@ -49,7 +49,7 @@ Use placeholder syntax **during SVG generation**:
<!-- phosphor-duotone (soft depth — single color renders the backplate at 20% opacity) -->
<use data-icon="phosphor-duotone/house" x="100" y="200" width="48" height="48" fill="#0076A8"/>
<!-- simple-icons (brand logo — used alongside the deck's primary library, not as a substitute) -->
<!-- simple-icons (brand logo — used alone or alongside the deck's primary stylistic library) -->
<use data-icon="simple-icons/github" x="100" y="200" width="48" height="48" fill="#181717"/>
```
@@ -97,9 +97,9 @@ Do not load a full index or enumerate broad keyword families. Re-pick from the n
> 1. **Geometry**: straight lines (`chunk-filled`) vs. curves (`tabler-filled` / `phosphor-duotone`) vs. open strokes (`tabler-outline`)
> 2. **Visual weight**: heavy solid (`chunk-filled`) → medium solid (`tabler-filled`) → medium layered (`phosphor-duotone`) → light stroke (`tabler-outline`)
**One presentation = one stylistic library.** Pick `chunk-filled` / `tabler-filled` / `tabler-outline` / `phosphor-duotone` at the start and use it exclusively throughout for generic icons (home, chart, users, etc.). If the chosen library doesn't have an exact icon, find the closest available alternative within that same library — never cross stylistic libraries to fill a gap.
**One primary bundled stylistic library per Strategist selection.** Pick one of `chunk-filled` / `tabler-filled` / `tabler-outline` / `phosphor-duotone` for generic icons (home, chart, users, etc.). If it lacks an exact icon, find the closest available alternative within that library instead of selecting from another bundled stylistic library. This is a catalog-selection rule, not a prohibition on combining assets that already exist in the project's `icons/` directory.
**Brand-logo exception (`simple-icons`).** `simple-icons` is **not a stylistic library** and does not participate in the "one library" rule. Its job is brand recognition — Slack's purple, GitHub's cat, AWS's color — which is intentionally heterogeneous. Use it **alongside** the chosen stylistic library, but **only** for actual company / product / service brand marks. Do **not** reach for it as a substitute when the chosen stylistic library lacks a generic icon.
**Brand-logo exception (`simple-icons`).** `simple-icons` is **not a stylistic library** and does not participate in the "one library" rule. Its job is brand recognition — Slack's purple, GitHub's cat, AWS's color — which is intentionally heterogeneous. Use it **alone or alongside** the chosen stylistic library, but **only** for actual company / product / service brand marks. Do **not** reach for it as a substitute when the chosen stylistic library lacks a generic icon.
| Use `simple-icons` for | Do NOT use `simple-icons` for |
|------------------------|-------------------------------|
@@ -107,4 +107,4 @@ Do not load a full index or enumerate broad keyword families. Re-pick from the n
| Tech stack icons on architecture / integration diagrams | Replacing a missing icon in `chunk-filled` / `tabler-*` / `phosphor-duotone` |
| Social media handles in a footer | Decorative / illustrative purposes |
⚠️ Do **not** mix icons from different **stylistic** libraries (`chunk-filled` / `tabler-filled` / `tabler-outline` / `phosphor-duotone`). `simple-icons` is the sole exception and may co-exist as a brand-logo inset — see the brand-logo exception above.
⚠️ During bundled selection, choose generic icons from only one of the four **stylistic** libraries. `simple-icons` may be selected at the same time for real brand marks. Project-local assets are already prepared material and are not subject to a runtime mixing ban.
@@ -41,6 +41,5 @@
- mode: flat
## forbidden
- Mixing icon libraries
- `mask`, `<style>`, `class`, external CSS, `<foreignObject>`, `textPath`, `@font-face`, `<animate*>`, `<set>`, `<script>` / event attributes, `<iframe>`
- HTML named entities in text; write typography as raw Unicode and escape XML reserved characters
@@ -140,21 +140,21 @@
"pattern": "^typography$",
"required": true,
"required_fields": ["font_family", "body", "title"],
"field_patterns": {
"font_family": "^\\S(?:.*\\S)?$",
"title_family": "^\\S(?:.*\\S)?$",
"body_family": "^\\S(?:.*\\S)?$",
"subtitle_family": "^\\S(?:.*\\S)?$",
"annotation_family": "^\\S(?:.*\\S)?$",
"footer_family": "^\\S(?:.*\\S)?$",
"footnote_family": "^\\S(?:.*\\S)?$",
"data_family": "^\\S(?:.*\\S)?$",
"emphasis_family": "^\\S(?:.*\\S)?$",
"quote_family": "^\\S(?:.*\\S)?$",
"code_family": "^\\S(?:.*\\S)?$",
"body": "^[0-9]+(?:\\.[0-9]+)?$",
"title": "^[0-9]+(?:\\.[0-9]+)?$"
"entry_key_pattern": "^[a-z][a-z0-9_]*$",
"field_value_rules": [
{
"key_pattern": "^(?:font_family|[a-z][a-z0-9_]*_family)$",
"value_pattern": "^\\S(?:.*\\S)?$",
"requirement": "be a non-empty font-family stack"
},
{
"key_pattern": "^(?!font_family$)(?![a-z][a-z0-9_]*_family$)[a-z][a-z0-9_]*$",
"value_pattern": "^(?=.*[1-9])(?:[0-9]+(?:\\.[0-9]+)?|\\.[0-9]+)$",
"normalize": false,
"numeric": "positive_finite",
"requirement": "be a positive finite unitless px number"
}
]
},
{
"id": "icons",
@@ -24,7 +24,7 @@ After Generate Step 4 Gate 1, read the completed Design Spec and current page/re
| `visual_style` | `visual_style` | Preset or `custom` |
| `colors` | Stable semantic color roles | Core identity and recurring roles only; contextual SVG paints need no row; `image_rendering` appears only for AI images |
| `typography` | `font_family`, `body`, `title` | Core family/size anchors; new locks also write explicit `title_family` and `body_family`; size anchors are unitless px numbers |
| `icons` | `library`, `inventory` | `stroke_width` is conditional |
| `icons` | `library`, `inventory` | `library` is the Strategist's primary bundled style choice or `none`; `simple-icons/*` may be selected alone or accompany it; `inventory` records the planned bundled selection rather than all usable project-local icons; `stroke_width` is conditional |
| `page_rhythm` | One `P<NN>` row per page | Values: `anchor`, `dense`, `breathing` |
| `pptx_structure` | `mode` | Values: `flat`, `structured` |
| `forbidden` | Literal list items | General standards stay in their owning reference |
@@ -35,7 +35,6 @@ The required universal block is:
```markdown
## forbidden
- Mixing icon libraries
- `mask`, `<style>`, `class`, external CSS, `<foreignObject>`, `textPath`, `@font-face`, `<animate*>`, `<set>`, `<script>` / event attributes, `<iframe>`
- HTML named entities in text; write typography as raw Unicode and escape XML reserved characters
```
@@ -71,7 +70,7 @@ Structured section value shapes:
- P01: 03_content
```
`page_charts` values must exist as keys in `charts/charts_index.json`; a `no-template-match` result stays out of both Design Spec §VII and `page_charts`, while its custom fallback remains in the page's §IX block.
Project each §VII `Page | Template | Usage` row's first two fields into `page_charts`; Usage stays in the Design Spec. This is a page-local reference, not a type/geometry lock. Keys must exist in `charts/charts_index.json`; no-match stays in §IX.
Typography projection is role-for-role, not a lossy summary:
@@ -82,14 +81,15 @@ Typography projection is role-for-role, not a lossy summary:
| Any additional recurring font role `<role>` | `<role>_family` |
| Every Font Size Hierarchy role `<role>` | lowercase `<role>` with its numeric anchor |
New locks always write `title_family` and `body_family`, even when their values happen to match. Every additional recurring family row and every size-anchor row in the Design Spec must appear under the same lowercase snake_case role; omit only family roles that inherit without an explicit override. Existing locks without family-role fields remain readable through `font_family` fallback. Executor may choose the anchor or a value within that role's `±2px` band; the lock does not enumerate intermediate values.
New locks always write `title_family` and `body_family`, even when their values happen to match. Every additional recurring family row and every size-anchor row in the Design Spec must appear under the same lowercase snake_case role; omit only family roles that inherit without an explicit override. Existing locks without family-role fields remain readable through `font_family` fallback. Executor may choose the anchor or a value within that role's `±2px` band; the lock does not enumerate intermediate values. A short non-structural Hero/Display size may remain absent only while the same undeclared value appears at most twice across the deck; its third occurrence requires a named role.
---
## 4. Field Grammar Index
- `font_family`, `title_family`, `body_family`, and every optional `<role>_family` use one non-empty PPT-safe exported family stack. `font_family` is the body/default compatibility stack, not permission to erase role differences.
- Every non-family `typography` value is a positive unitless px anchor. Intermediate values need no lock row when they stay within the mapped role's anchor `±2px`; a new semantic role or an outside-band size requires Design Spec repair and a new anchor.
- Every non-family `typography` value is a positive finite unitless px anchor. Intermediate values need no lock row when they stay within the mapped role's anchor `±2px`. At most two occurrences of one undeclared short non-structural Hero/Display size may remain sparse; a third occurrence or any structural use requires Design Spec repair and a named anchor.
- `icons.library` records the primary stylistic library selected from `chunk-filled`, `tabler-filled`, `tabler-outline`, or `phosphor-duotone`, or `none` when no generic bundled icons are selected. Selected `simple-icons/*` brand marks may appear alone or alongside it in `inventory` without becoming a stylistic library. The inventory records planned bundled choices, while every SVG already under `<project_path>/icons/` remains valid prepared execution material.
- `objective` grammar: one concise sentence preserving the deck goal and audience success condition.
- `image_rendering` grammar: one catalog id, or `custom` with `image_rendering_behavior`.
- `images`: `- <key>: <path> | source=<via> | pattern=<layout> | crop=<adaptive|no-crop>`; e.g. `- p04: images/a.png | source=user | pattern=#2 Left image | crop=no-crop`. Omit unplaced sheets.
@@ -128,5 +128,5 @@ Field meaning and selection logic stay in the owning Strategist modules. Executo
- Confirmed core palette roles and every declared typography family/size role remain stable cross-page anchors.
- Page-local tints, gradient stops, shadow/glow paints, transparency composites, and one-off export-safe display families may be authored from context without adding a lock row.
- Executor may adjust one occurrence within its declared size role's anchor `±2px` while preserving hierarchy and readability; intermediate values are realization choices, not new lock rows.
- When a contextual value becomes a recurring semantic role, or typography needs a value outside every applicable anchor band, add the descriptive color/family/size role and regenerate page-context before later pages use it.
- When a contextual value becomes a recurring semantic role, or one undeclared display size reaches its third occurrence, add the descriptive role, read back and validate affected planning fragments, then reuse it. Structural typography outside its applicable anchor band returns upstream immediately.
- Do not expand the lock merely to make an informational checker comparison empty. A lock edit should express reuse or identity, not enumerate incidental literals.
@@ -12,8 +12,8 @@ description: Generate PPTX route authority for source intake, planning, SVG auth
- The current main agent hand-writes every SVG page; never delegate page generation or run a Python, Node, or shell generator over `svg_output/`.
- Initial SVG cadence: P01 → first-page gate → uninterrupted remaining pages → final gate. Grouped batches and mid-run checker calls are forbidden.
- Before each page, load compact page-context; its repeated global values are continuity anchors, while unchanged large references stay in context.
- `preset_shape_svg.py` may provide one stdout fragment only after the main agent chooses its semantic role, frame, and paint; it cannot choose layout or write a page.
- Gate checklists are internal verification, not user-facing output. On success, continue automatically and emit at most one compact status line when useful; on failure, report only the blocking items and required recovery.
### SVG Page-Design Boundary
@@ -212,16 +212,16 @@ If the user opted out of the page but did not delegate confirmation, skip launch
**GATE — consume the final state once into the Design Spec, then author the lock from context.** Treat every explicitly present final value as user-owned input and consume it at the semantic type defined by [`strategist.md`](../references/strategist.md) §1 and its field owner. Do not omit or substitute a value, and do not silently strengthen or weaken its type; accepting a recommendation does not turn a Reference or Permission into a Literal requirement. First author and audit the complete `design_spec.md` through [`strategist.md`](../references/strategist.md) §6.2 from the retained final object, including production mechanics, the complete recurring typography-role system, the confirmed image-source boundary, and explicit `image_notes` obligations. Do not reopen `result.json` afterward. Only after that audit passes, author `spec_lock.md` from the completed Design Spec and current project/page/template context: preserve confirmed identity, project every declared recurring typography family and size-anchor role without collapsing it into one default stack, choose reusable execution anchors and routing values, project each placed image's source/pattern/crop policy without reselection, and do not enumerate page-local paint or font-family garnish. Apply `strategist-template.md` §3 for an active template, and never write a separate image palette. If a confirmed requirement cannot be honored, follow [`failure-recovery.md`](governance/failure-recovery.md) instead of silently changing it.
**Mandatory — split-mode note** (not a separate confirmation): after listing the Strategist confirmation stage details, you MUST append exactly one short line (rendered in the user's language, prefixed with 💡) about generation mode. Pick the variant by qualitative read of upstream-load signals recommended page count, source-material bulk, whether `topic-research` ran with substantial web-fetch accumulation:
**Conditional — split-mode note** (not a separate confirmation): after listing the Strategist confirmation stage details, append one short line (rendered in the user's language, prefixed with 💡) only when the confirmed mode is `split` or upstream-load signals make a fresh execution context materially useful. Judge those signals from recommended page count, source-material bulk, and substantial `topic-research` web-fetch accumulation:
| Signal read | Line content |
|---|---|
| Heavy (long page count / bulky sources / heavy web-fetch accumulation) | State estimated page count and large source size; recommend switching to [split mode](stages/resume-execute.md) after Step 5 — stop this chat, open a fresh window and input `继续生成 projects/<project_name>` to enter the execution session (SVG generation + export); no response or "continue" = default continuous mode. |
| Normal (default) | State scale is moderate, default continuous mode generates in one go; if mid-way window switch is desired, input `继续生成 projects/<project_name>` after Step 5 to switch to [split mode](stages/resume-execute.md). |
| Explicit `split` selection | Confirm that planning will stop after Step 5 and give the `继续生成 projects/<project_name>` handoff command. |
This line is required output every run — the user must always see the mode choice exists. Whether to act on it is the user's call. When the Confirm UI is used, this choice also appears as the in-page generation-mode toggle and is captured in `result.json` (`generation_mode`); the chat-summary fallback still prints this line.
For the normal/default `continuous` path, print no split-mode reminder and proceed automatically. Confirm UI still exposes the generation-mode toggle and records it in `result.json`; a chat fallback captures the same choice in its confirmation summary without adding a separate reminder.
**Mandatory — spec-refinement note** (not a separate confirmation): after the split-mode line, you MUST append one short opt-in line (rendered in the user's language, prefixed with 💡) telling the user they may **refine the spec first** — Strategist will produce the full design spec, then stop for review/revision of any part of it before any generation, via the [refine-spec](stages/refine-spec.md) stage. Default is OFF: no request → the spec is written in one go and the pipeline auto-proceeds as usual. Only when the user explicitly asks in chat (e.g. "refine the spec first") or confirms `refine_spec: true` through Confirm UI does the [refine-spec](stages/refine-spec.md) stage take over after the Strategist confirmation stage. This line, like the split-mode line, is required output every run — the user must see the choice exists; whether to act on it is theirs. When the Confirm UI is used, this choice also appears as the in-page refine-spec toggle and is captured in `result.json` (`refine_spec`); the chat-summary fallback still prints this line.
**Mandatory — spec-refinement note** (not a separate confirmation): after the confirmation details and any conditional split-mode line, you MUST append one short opt-in line (rendered in the user's language, prefixed with 💡) telling the user they may **refine the spec first** — Strategist will produce the full design spec, then stop for review/revision of any part of it before any generation, via the [refine-spec](stages/refine-spec.md) stage. Default is OFF: no request → the spec is written in one go and the pipeline auto-proceeds as usual. Only when the user explicitly asks in chat (e.g. "refine the spec first") or confirms `refine_spec: true` through Confirm UI does the [refine-spec](stages/refine-spec.md) stage take over after the Strategist confirmation stage. This opt-in line remains required output every run; whether to act on it is the user's call. When the Confirm UI is used, this choice also appears as the in-page refine-spec toggle and is captured in `result.json` (`refine_spec`); the chat-summary fallback still prints this line.
**Formula policy**: Stage 3 confirms `mixed`, `render-all`, or `text-only`. When the confirmed policy requires rendering formula-worthy content, load [`strategist-image.md`](../references/strategist-image.md) even if `image_usage` is `none`, and follow its formula-resource contract before filling the planning artifacts. `text-only` creates no formula image rows.
@@ -230,13 +230,13 @@ If the user provided images or formula PNGs were rendered, run analysis **before
python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images
```
> 🔁 **Image facts are regenerated on demand, never a durable store.** `images/` is a live working folder — pictures are extracted from the source at import, the user may drop or replace files at any time, and Step 5 writes web/AI images into it. The single source of truth is therefore the **current contents of `images/`**, and `analysis/image_analysis.csv` is a *regenerated view* of it, not a fact to keep in sync. Re-run `analyze_images.py <project_path>/images` immediately **before any step that reads image facts** so the view reflects the live folder: before the image-source recommendation (see [strategist.md](../references/strategist.md) §h), here before authoring §VIII, after Step 5 acquisition (so web/AI files join the view), and again any time the user says they added or replaced images. This is the staleness strategy — re-derive on use, no cache to invalidate.
> 🔁 **Image facts are regenerated on change, never maintained as a second store.** `images/` is the live working folder and single source of truth; `analysis/image_analysis.csv` is its regenerated view. Run `analyze_images.py` before the first inventory read, then reuse that CSV while `images/` is unchanged. Re-run after import/acquisition or any user addition, removal, or replacement; if the folder becomes empty, treat the inventory as empty and ignore a stale CSV.
> ⚠️ **Image handling**: NEVER directly read / open / view image files (`.jpg`, `.png`, etc.). All image info comes from `analyze_images.py` output (`analysis/image_analysis.csv`) or the Design Spec's Image Resource List.
> ⚠️ **Image understanding**: Do not bulk-open images. Strategist uses source context, captions / alt / titles, filenames, user notes, existing resource records, and `image_analysis.csv` first; only a specifically ambiguous asset may be inspected under [`strategist-image.md`](../references/strategist-image.md). Record the result in §VIII. Executor never reopens source images for semantic discovery or reselection.
**Output**:
- `<project_path>/design_spec.md` — complete human-readable design narrative and durable confirmed production state
- `<project_path>/spec_lock.md` — machine-readable communication + stable execution anchors/routing contract; Executor consumes its current-page projection from `page-context`
- `<project_path>/spec_lock.md` — machine-readable communication + stable execution anchors/routing contract; Executor retains it in the active execution context and may inspect an on-demand current-page projection for diagnostics
For a new project, use the reference-first whole-document sequence:
@@ -247,20 +247,7 @@ For a new project, use the reference-first whole-document sequence:
A retained final state → Design Spec mismatch or Design Spec/context → lock mismatch is blocking even when the standalone Markdown schemas pass. `validate` reads the planning artifacts only; it does not reopen `confirm_ui/result.json` or prove semantic fidelity. Repair the Design Spec from the retained final state; only a fresh recovery turn with no retained state reads persisted final evidence once. Then re-author the affected lock rows from the corrected Design Spec and current context. A resume or refine path edits existing completed files in the same order; it does not replace them with scaffolds.
**✅ Checkpoint — Phase deliverables complete, auto-proceed to next step**:
```markdown
## ✅ Strategist Phase Complete
- [x] Read the auto-extracted facts already in `analysis/` (e.g. `source_profile.json`) before the Strategist confirmation stage
- [x] Strategist confirmation stage completed (user confirmed via Confirm UI `result.json` or chat fallback)
- [x] Final confirmation read once and retained through Design Spec authoring/audit; `result.json` not reopened in the normal path
- [x] Split-mode note appended below the confirmation fields (heavy or normal variant)
- [x] Spec-refinement opt-in line appended (default OFF; only the user's explicit request enters the refine-spec stage)
- [x] Design Specification & Content Outline generated
- [x] Gate 1 passed: Design Spec preserves every confirmed value at its owner-defined semantic type
- [x] Execution lock (`spec_lock.md`) authored from the completed Design Spec + current context as stable anchors/routing, not an exhaustive value whitelist
- [x] Communication trace validated: Design Spec retains the contract; the lock has compact `audience` / `objective` / `core_message` fields (plus PPT `consumption_mode`); every §IX Slide has an `Audience move`
- [ ] **Next**: Auto-proceed to [Image_Generator / Executor] phase
```
**✅ Internal checkpoint — Phase deliverables complete**: verify that analysis facts were read before confirmation; final confirmation was consumed once; applicable split/refinement handling is resolved; the complete Design Spec passed Gate 1; the lock was authored from it; and communication plus every §IX `Audience move` were validated. Do not print this checklist. On success, auto-proceed to Image_Generator / Executor under the compact status rule above.
---
@@ -268,7 +255,7 @@ A retained final state → Design Spec mismatch or Design Spec/context → lock
🚧 **GATE**: Step 4 complete; `<project_path>/design_spec.md` and `<project_path>/spec_lock.md` both exist. If either required artifact is missing, stop before any acquisition or generation and follow [`failure-recovery.md`](governance/failure-recovery.md) §3. Formula rows already have `Acquire Via: formula` and status `Rendered` or `Needs-Manual`.
> **Trigger**: At least one row in the resource list has `Acquire Via: ai`, `web`, and/or `slice`. If every row is `user`, `formula`, or `placeholder`, skip to Step 6. A permitted but unused image source creates no row and does not trigger acquisition. If §VIII omits a source, asset, or page role that `image_notes` explicitly requires, the Design Spec is incomplete; return to Step 4 Gate 1, repair it from the retained final state, and re-author the affected lock anchors from context. Do not reopen `result.json` during this check.
> **Trigger**: At least one row in the resource list has `Acquire Via: ai`, `web`, and/or `slice`. A prepared-user-only plan contains `user / Existing` rows and skips this entire step; `formula` and `placeholder` rows also do not trigger acquisition. A permitted but unused image source creates no row and does not trigger acquisition. If §VIII omits a source, asset, or page role that `image_notes` explicitly requires, the Design Spec is incomplete; return to Step 4 Gate 1, repair it from the retained final state, and re-author the affected lock anchors from context. Do not reopen `result.json` during this check.
**Failure recovery**: stop/continue behavior for AI/web/slice/image-readiness failures is defined in [`workflows/governance/failure-recovery.md`](governance/failure-recovery.md). This Step keeps the acquisition procedure.
@@ -305,16 +292,7 @@ Workflow:
3. Verify every row reaches a terminal status: `Generated` (ai success / sliced element), `Sourced` (web success), or `Needs-Manual`. `Failed` is not a terminal status: it means the current run did not generate that item, but the item remains retryable. On `auto`, follow the owning fallback chain. On an explicitly confirmed `api` or `host-native` path, retry only that path; if it still fails, mark the row `Needs-Manual` without switching to another automated provider.
4. Re-derive image facts now that web / AI / sliced files are in the folder — `python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images` — so `analysis/image_analysis.csv` reflects every acquired image **including the sliced elements** (real measured sizes) before the Executor lays them out. Image facts are regenerated on use, never a stale store (see Step 4's image-facts note).
**✅ Checkpoint — Confirm acquisition attempted for every row**:
```markdown
## ✅ Image Acquisition Phase Complete
- [x] image_prompts.json created (when any ai rows processed)
- [x] image_prompts.md sidecar rendered (when any ai rows processed)
- [x] image_sources.json created (when any web rows processed)
- [x] Spot-illustration sheets sliced (when any `slice` rows exist); every element file present in `images/` and listed in `spec_lock.md images`
- [x] Each row: status is `Generated` / `Sourced` / `Needs-Manual` (no `Pending` or `Failed` remaining)
- [x] analyze_images.py re-run so image_analysis.csv covers the acquired web / AI / sliced images
```
**✅ Internal checkpoint — acquisition complete**: verify conditional AI/web sidecars, all required slice outputs, terminal status for every resource row, and a refreshed `image_analysis.csv`. Do not print this checklist. On success, auto-proceed under the compact status rule above.
**Default — auto-proceed to Step 6.** Only when `design_spec.md §I` records `generation_mode: split`, output the planning-session handoff below and stop this conversation:
@@ -337,6 +315,8 @@ Workflow:
**Page content**: §IX is preferred wording and semantic authority. Use it when it works; adapt it when presentation benefits while preserving intent, facts, and explicit literal requirements. Read sources only to verify requested evidence; return incomplete blocks to Step 4 instead of enriching them during execution.
**Planning context**: follow [`executor-base.md`](../references/executor-base.md) §2.1. Reuse the complete Design Spec and lock in an unchanged, uncompacted context. Fresh/resumed/restarted, compacted/summary-only, or externally/unknown changed execution reads both once and reloads triggered inputs. For a local question, consult the retained lock first, then only the owning Design Spec fragment; do not poll files merely to prove validity.
**Artifact ownership**: `svg_output/` is the author source, `svg_final/` is derived, and image facts come from the regenerated `analysis/image_analysis.csv`; see [`references/artifact-ownership.md`](../references/artifact-ownership.md).
Read the execution references for this deck's locked `mode` + `visual_style` (from `spec_lock.md`):
@@ -376,26 +356,20 @@ python3 ${SKILL_DIR}/scripts/svg_editor/server.py <project_path> --live --daemon
- **Do NOT read or apply submitted annotations during generation.** Users may annotate at any time, but Executor proceeds without touching them. The window to apply annotations opens only after Step 7 completes — see [`workflows/stages/live-preview.md`](stages/live-preview.md).
- The editor also supports **staged direct edits** (text content + SVG element attributes previewed immediately, then written to `svg_output/` only when the user clicks **Apply changes**; `Ctrl+Z` / Undo drops staged edits) alongside annotation; re-export stays chat-driven. Full scope and editor details: see [`workflows/stages/live-preview.md`](stages/live-preview.md) Notes.
**Conditional reference reads**: Follow `executor-structured.md` for template Design Spec/prototypes and `executor-chart.md` for `templates/charts/<key>.svg`. Both reuse unchanged `reference_set` path + SHA fingerprints; flat routes skip template reads. Summaries and sidecars never replace full SVGs.
**Conditional reference reads**: Follow `executor-structured.md` for template Design Spec/prototypes and `executor-chart.md` for chart SVGs. Read each selected full reference once per valid context; reread only after a known change or context invalidation. Flat routes skip template reads. Summaries and sidecars never replace full SVGs.
> Image facts: trust the `analysis/image_analysis.csv` regenerated at the end of Step 5. If `images/` changed since (the user swapped or added files), re-run `python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images` before laying images out — facts are re-derived on use, never a stale store (Step 4 image-facts note).
> Image facts: trust the latest `analysis/image_analysis.csv` from the Step 4 inventory read or the Step 5 post-acquisition refresh. If `images/` changed since, re-run `python3 ${SKILL_DIR}/scripts/analyze_images.py <project_path>/images` before layout; if the folder is empty, use no image inventory and ignore a stale CSV.
**Per-page context load (Mandatory)**: before **each** SVG page, run:
```bash
python3 skills/ppt-master/scripts/project_manager.py page-context <project_path> P<NN> --record-usage
```
Use `global` as the compact cross-page anchor set, `page_context` as the page delta, and `reference_set` under the Executor load policy. Anchors are defaults and reusable semantic roles, not a color/font allowlist. The retained Design Spec owns optional `Template Application`. This replaces neither gate artifacts nor source facts. See [`executor-base.md`](../references/executor-base.md) §2.1.
**Page-context**: use the read-only projector only for the diagnostic/telemetry triggers in Executor §2.1, never as a routine pre-page load.
> ⚠️ **Main-agent only**: SVG generation MUST stay in the current main agent — page design depends on full upstream context. Do NOT delegate to sub-agents.
> ⚠️ **Generation rhythm**: P01 → first-page gate → uninterrupted remaining pages → final gate, in one context without batches or mid-run checker calls.
> ⚠️ **Generation rhythm**: P01 → first-page gate → uninterrupted remaining pages → final gate. After context invalidation, reload under §2.1 before continuing; do not insert batches or mid-run checker calls.
**Visual Construction Phase**: generate SVG pages sequentially, one at a time, in one continuous pass → `<project_path>/svg_output/`
Each completed SVG MUST be a standalone, complete representation of that slide's visible design. Template SVGs and locked planning artifacts may guide construction, but export must not reach back to them to add visible objects omitted from `svg_output/`. Speaker notes, animation, narration, transitions, and direct native-PPTX workflows remain separately owned artifacts/capabilities. When a page actually needs a literal stock shape, load and apply [`native-shape-authoring.md`](../references/native-shape-authoring.md) before drawing it. Diagram relationships remain Shape-first; do not infer a preset from contour similarity.
`template_reuse_scope: mirror|layout` pages MUST start from the complete `page_layouts` SVG, keep inherited visible objects, and preserve root Master/Layout identity plus stable atoms/slots. Strict preserves that reusable contract; under `layout`, the once-loaded Design Spec's `Template Application` may still authorize carrier text/tspan reflow inside unchanged slot bounds. Adaptive uses the current or new Layout key/name already declared by Strategist. If construction proves that fixed atoms or slot topology/bounds must change, stop and return upstream for Strategist to repair the owning plan and lock, regenerate the page context, then resume; Executor never mutates `spec_lock.md`. `mirror` changes only visible text values while preserving text/tspan topology and attributes. `style` follows the flat paragraph below without structure metadata.
`template_reuse_scope: mirror|layout` pages MUST start from the complete `page_layouts` SVG, keep inherited visible objects, and preserve root Master/Layout identity plus stable atoms/slots. Strict preserves that reusable contract; under `layout`, the once-loaded Design Spec's `Template Application` may still authorize carrier text/tspan reflow inside unchanged slot bounds. Adaptive uses the current or new Layout key/name already declared by Strategist. If construction proves that fixed atoms or slot topology/bounds must change, stop and return upstream for Strategist to repair the owning plan and lock, validate and read back the affected fragments, then resume; Executor never mutates `spec_lock.md`. `mirror` changes only visible text values while preserving text/tspan topology and attributes. `style` follows the flat paragraph below without structure metadata.
`template_reuse_scope: style`, free-design, and brand-only pages use `pptx_structure.mode: flat`. Draw the complete page directly: keep backgrounds, repeated chrome, headings, text, images, and decoration as ordinary Slide-local SVG content. Do not plan `pptx_masters` / `pptx_layouts` / `page_pptx_layouts`, do not add root Master/Layout identity, and do not add `data-pptx-layer` or `data-pptx-placeholder` metadata. Group logical content normally with top-level `<g id>` elements. Export materializes one clean project-owned Master plus one Blank Layout, applies the locked theme colors/fonts/title-body defaults, removes stock content placeholders and unused built-in Layouts, and retains only the standard date/footer/slide-number capability hooks. It does not promote or deduplicate page content.
@@ -407,6 +381,22 @@ python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage first
```
Run the command unfiltered—do not pipe it through `tail`, `head`, `grep`, or another output truncator. Review the complete P01 issue set from that one run before editing. Select any advisory warnings worth addressing, fix all blocking errors and selected warnings in one consolidated edit pass, then perform one verification rerun. Do not rerun merely to reveal the next issue. If verification still fails, treat its complete output as the next batch and repeat the same review → consolidated edit → single verification cycle; never check between individual fixes. If the terminal output itself is truncated, read only the relevant issue arrays from `validation/svg_quality_first_page_report.json`; do not launch another checker run for discovery. After the gate passes, draw P02 through the final page without checker calls.
**Mandatory — read P01 as a method sample, then emit the classification before editing**: the gate validates how the remaining pages will be authored, not only this page.
| Signal | Reading |
|---|---|
| Two or more issues share a category and direction | Method-level bias — resolve it to the authoritative rule before P02; a correction fitted to the observed offset only patches this sample. For text extents that rule is `svg_to_pptx.drawingml.elements.estimate_single_line_text_frame_width(runs)`, with `skills/ppt-master/scripts` on `sys.path` and every run key present — `text`, `font_size`, `font_family`, `font_weight`, `letter_spacing` — since omissions under-measure |
| One isolated issue tied to this page's structure | Page-local — fix and continue |
| A recurring element appears for the first time (page furniture, caption format, section numbering, accent discipline) | It will be copied to every later page — confirm its semantics now |
Emit one line before the consolidated edit:
```
gate-signal: method=<rule resolved, or none> | page-local=<count> | not-exercised=<list>
```
`not-exercised` names what P01 could not test — a cover typically omits multi-line text, columns, charts, image captions, and data objects. Carry every resolved rule forward as arithmetic; P02 through the final page run without further tool calls.
**Quality Check Gate (Mandatory)** — only after every planned SVG exists, BEFORE annotation handling and speaker notes:
```bash
python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage final --json
@@ -422,17 +412,7 @@ python3 ${SKILL_DIR}/scripts/svg_quality_checker.py <project_path> --stage final
**Logic Construction Phase**: after the SVG quality gate passes, load [`executor-notes.md`](../references/executor-notes.md) and generate speaker notes → `<project_path>/notes/total.md`
**✅ Checkpoint — Confirm all SVGs and notes are fully generated and quality-checked. Run the applicable conditional gates below, then proceed to Step 7**:
```markdown
## ✅ Executor Phase Complete
- [x] Live preview started before the first SVG and kept available at the reported URL
- [x] P01 gate passed; remaining pages authored without checker calls
- [x] Each failing checker run was reviewed as one complete issue set and followed by one consolidated edit pass; checker output was not filtered
- [x] `svg_output/` matches the ordered §IX roster exactly, one SVG per planned page
- [x] Every wrapped prose paragraph uses one `<text>` frame with `<tspan>` line breaks
- [x] svg_quality_checker.py passed (0 errors)
- [x] Speaker notes generated at notes/total.md
```
**✅ Internal checkpoint — execution complete**: verify live preview timing, the P01 method gate, uninterrupted remaining-page generation, consolidated repair of any complete failure set, exact §IX roster coverage, one-frame prose wrapping, a final checker result of 0 errors, and `notes/total.md`. Do not print this checklist. Run the applicable conditional gates below, then proceed to Step 7 under the compact status rule above.
> **Chart pages?** If this deck contains data charts, run the [`verify-charts`](stages/verify-charts.md) quality-gate stage before Step 7 to calibrate coordinates. Skip if no chart pages.
@@ -20,7 +20,8 @@ Global stop/continue rules for all four top-level routes, plus concrete failure
| Missing final confirmation | Yes | None | User must confirm or change the values | Step 4 final confirmation |
| Final confirmed value is missing, changed, substituted, or weakened in `design_spec.md` | Yes | Repair from the retained final-confirmation object; only a fresh recovery turn with no retained state reads persisted final evidence once | Only when the confirmed value genuinely cannot be honored | Step 4 Gate 1 — confirmation fidelity |
| `spec_lock.md` changes confirmed identity or omits a required execution anchor/routing decision | Yes | Re-author the affected lock rows from the completed Design Spec and current context; do not enumerate page-local literals | No unless the Design Spec itself is incomplete | Step 4 Gate 2 — lock context fidelity |
| Execution exposes a missing Strategist-owned semantic role or plan detail | Yes for the affected page | In the same main-agent context, return to Strategist; repair the owning Design Spec fragments, then affected lock rows; targeted-readback, validate, rerun `page-context` for affected pages, and rebind the new SHA per [`executor-base.md`](../../references/executor-base.md) §2.1. Fresh or unknown context reads the complete Design Spec once | Only if the repair changes confirmed identity or user intent | Step 4 Gate 1/2 → Step 6 current page |
| Execution exposes a missing Strategist-owned role/plan detail | Yes for the affected page | Repair affected Design Spec/lock fragments under [`executor-base.md`](../../references/executor-base.md) §2.1 | Only if confirmed intent changes | Step 4 Gate 1/2 → Step 6 current page |
| Execution context is fresh, resumed, restarted, compacted/summary-only, external, or unknown | Yes until rebuilt | Read complete Design Spec, then lock, once; reload triggered inputs and latest completed SVG when mid-deck | No | Step 6 current page |
| Step 3 rejects a legacy or incomplete template contract | Yes | Stop template consumption; create a new current workspace through Create Template from the original PPTX/reference, then return with its exact workspace root | Only when required source evidence or template choices are unavailable | Create Template → Generate PPTX Step 3 |
| Formula rendering provider failure | No until the Step 7 readiness gate | Exhaust the provider chain; if unresolved, mark only the affected formula rows `Needs-Manual` and continue | Supply the exact target PNG or change formula policy | Step 4 / Step 7 image readiness gate |
| AI image generation failure | No | `auto`: follow A → B → Offline Manual. Explicit `api` / `host-native`: retry only that path, then mark the row `Needs-Manual` without switching automated providers | Only when missing files are required before export | Step 5 / Step 7 image readiness gate |
@@ -87,7 +87,7 @@ python3 ${SKILL_DIR}/scripts/pptx_intake.py <project_path>/sources/<source.pptx>
| `<stem>.slide_library.json` field | Use |
|---|---|
| `slides[].charts[]` (`chart_type` / `categories` / `series[].values`) | regenerate as a native SVG chart; use the §VII `templates/charts/` path only when recall selects a real reference, otherwise plan the custom chart in §IX |
| `slides[].charts[]` (`chart_type` / `categories` / `series[].values`) | regenerate as a native SVG chart; use the §VII catalog key only when recall selects a real reference, otherwise plan the custom chart in §IX |
| `slides[].tables[]` (`row_count` / `column_count` / cell text) | regenerate as a native SVG table |
**Hard rule — regenerate visuals, do not carry them over**: charts / tables / images are rebuilt from their data in the inherited style, never spliced in byte-for-byte. This keeps the deck style-consistent and natively editable. **Data values are frozen** (categories / series / cell text / numbers unchanged); only their rendering is the deck's own. Pictures (`ppt_to_md`-extracted files) are reused but re-laid-out — position / crop / size follow the new layout, not the source slot. A user who wants an original element verbatim copies it across themselves.
@@ -208,7 +208,7 @@ python3 ${SKILL_DIR}/scripts/confirm_ui/server.py <project_path> --daemon --wait
After the final wait returns, read `<project_path>/confirm_ui/result.json` exactly once and retain the complete confirmed object through Design Spec authoring. Chat is the canonical fallback when the page cannot open (remote / headless) — present the same fields in chat and retain the reply identically. Always run `--shutdown` on exit (page-confirm or chat-fallback) so port 5050 is free for Step 6 live preview.
On confirmation, enter [`generate-pptx`](../generate-pptx.md) Step 4 as Strategist with the plan pre-resolved. The two beautify invariants always hold: the content-faithful clause ([`strategist.md`](../../references/strategist.md) §d Layer 1) and page count = source slide count (strict 1:1). Write the retained confirmed object completely into `design_spec.md``mode` (recommended `briefing`), canvas, `visual_style`, color (e) + typography (g) incl. `body_size` (the reviewed values; skip both recommendation flows) — honoring whatever the user kept or overrode. Do not reopen `result.json` afterward. §VII contains only selected `templates/charts/` references; unmatched chart/table plans stay in their §IX page blocks. §VIII contains source pictures for re-layout.
On confirmation, enter [`generate-pptx`](../generate-pptx.md) Step 4 as Strategist with the plan pre-resolved. The two beautify invariants always hold: the content-faithful clause ([`strategist.md`](../../references/strategist.md) §d Layer 1) and page count = source slide count (strict 1:1). Write the retained confirmed object completely into `design_spec.md``mode` (recommended `briefing`), canvas, `visual_style`, color (e) + typography (g) incl. `body_size` (the reviewed values; skip both recommendation flows) — honoring whatever the user kept or overrode. Do not reopen `result.json` afterward. §VII contains only `Page | Template | Usage` rows for selected catalog references; unmatched chart/table plans stay in their §IX page blocks. §VIII contains source pictures for re-layout.
**Hard rule — §IX is verbatim and 1:1**: each source slide becomes exactly one page, in source order, its text transcribed word-for-word from `sources/<stem>.md`. Do not merge, split, drop, or rewrite. Complete and audit `design_spec.md` first, then author `spec_lock.md` from that Design Spec plus the source/page/template context per `strategist.md` §6 before handing off to the Executor.
@@ -46,6 +46,7 @@ route selection. After selection, the route authority owns execution.
| Existing PPTX must preserve wording, page count, and page order 1:1 | Activate the [`beautify-pptx`](./profiles/beautify-pptx.md) profile inside the main pipeline |
| Explicit current brand/layout/deck workspace root | Enter [`generate-pptx`](./generate-pptx.md) Step 3 and conditionally load [`apply-template-workspace`](./stages/apply-template-workspace.md); consume the workspace root, never only its inner `templates/` directory |
| Split-mode project resumes in a fresh chat | Run [`resume-execute`](./stages/resume-execute.md) inside the active Generate route |
| Existing generated project needs a deck-wide `colors.*` or universal `typography.font_family` substitution | Stay in Generate; load [`update_spec.py`](../scripts/docs/update_spec.md), honor its supported-key boundary, then rerun the final quality gate and Step 7 export |
| User explicitly requests spec refinement | Run [`refine-spec`](./stages/refine-spec.md) after Strategist confirmation |
| Data charts exist | Run [`verify-charts`](./stages/verify-charts.md) before export |
| User explicitly requests visual review | Run [`visual-review`](./stages/visual-review.md) before post-processing |
@@ -119,4 +120,12 @@ Object animation for generated SVG projects uses the animation stage. Native PPT
| Bare template/brand name or style label | Do not resolve it to a local path; treat it as a style brief |
| “What templates exist?” | List indexed workspace paths as Q&A; do not advance a route |
For that Q&A only, read the matching discovery indexes:
| Kind | Discovery index |
|---|---|
| Brand | [`brands_index.json`](../templates/brands/brands_index.json) |
| Layout | [`layouts_index.json`](../templates/layouts/layouts_index.json) |
| Deck | [`decks_index.json`](../templates/decks/decks_index.json) |
**Forbidden — fuzzy resolution**: Never resolve a bare name to a local template directory on the user's behalf. The explicit workspace root is the only Step 3 template trigger, except the exact validated workspace handed off by Create Template in the current conversation.
@@ -28,8 +28,8 @@ Verify the project's planning-session artifacts before doing anything else:
| File / Directory | Required when | Reason |
|---|---|---|
| `<project_path>/spec_lock.md` | Always | Strategist's execution contract; `page-context` projects its current-page values |
| `<project_path>/design_spec.md` | Always | Section IX page outline; `page-context` projects the current page block |
| `<project_path>/spec_lock.md` | Always | Strategist's execution anchors and routing contract; read it completely once in this fresh execution context |
| `<project_path>/design_spec.md` | Always | Complete approved design narrative and Section IX page outline; read it completely once in this fresh execution context |
| `<project_path>/images/` plus files whose row status requires existence | `spec_lock images` references any image | `Existing` / `Generated` / `Sourced` / `Rendered` files must exist; an absent `Needs-Manual` file remains allowed until the Step 7 readiness gate |
| `<project_path>/templates/` | `spec_lock page_layouts` references any | Layout / mirror prototypes required by execution |
| `skills/ppt-master/templates/charts/` | `spec_lock page_charts` references any | Shared chart SVGs selected by key |
@@ -50,10 +50,12 @@ Read skills/ppt-master/workflows/generate-pptx.md
Then jump to `### Step 6: Executor Phase` and run the documented pipeline:
- Read the complete project Design Spec, then the complete `spec_lock.md`, once to establish the fresh execution context
- If resuming mid-deck, read the latest completed SVG and current image metadata when images are used
- Read the Step 6 flat core (`executor-base`, `shared-standards-core`, and the locked preset mode / visual-style files); for custom directions, reload every optional `*_references` file from `spec_lock.md` before applying the behavior, then only the branches selected by the condition table
- Design Parameter Confirmation
- Read the project Design Spec and, when structured, the template Design Spec once; retain both in the fresh execution context. A later repair authored by this same main agent follows [`executor-base.md`](../../references/executor-base.md) §2.1 targeted readback/rebind instead of loading the project Design Spec a second time
- Per-page `python3 skills/ppt-master/scripts/project_manager.py page-context <project_path> P<NN> --record-usage` load + sequential page generation; the stable anchor projection repeats intentionally without becoming a color/font allowlist, while each prototype/chart SVG is loaded only before its first use or after its SHA changes
- When structured, read the template Design Spec and each selected prototype once; retain unchanged references in the fresh context. A later bounded repair follows [`executor-base.md`](../../references/executor-base.md) §2.1 only while that context remains valid and uncompacted
- Generate pages sequentially from the retained planning artifacts. Use `page-context` only for the on-demand diagnostic/telemetry triggers in Executor §2.1, never as a routine pre-page load
- Quality Check Gate
- Speaker notes generation
- Step 7: Post-processing & Export (`total_md_split``finalize_svg``svg_to_pptx`)
@@ -65,7 +65,7 @@ For each page in the Step 1 list:
- Preferred: `<!-- chart-plot-area: ... -->` marker placed by Executor (see [executor-chart.md §2.1](../../references/executor-chart.md)). Read coordinates directly.
- If missing: derive the plot area from the SVG's axis lines (rectangular charts) or center/radius elements (radial charts). Then **add the marker back to the SVG** so future runs are not paying this cost again.
3. Read the data series from the SVG's `<text>` label/value elements.
4. **Read axis tick labels for every axis-based chart.** Locate the `<text>` elements along the value axis — X-axis labels for horizontal bars, Y-axis labels for vertical bars, and Y-axis labels for line-like charts. Extract the first and last tick values to determine the axis range (e.g. `0%` to `120%` → range `0,120`). Pass this range as `--value-range`, `--y-range`, or `--x-range` as appropriate. Radar uses `--max-value` instead of a range: read the outermost ring's tick value and pass it as `--max-value`. If the SVG has no explicit tick labels (data labels only, no grid), omit the range and let the calculator auto-normalize — but flag the receipt as `scale=auto (no ticks)`.
4. **Read axis tick labels for every axis-based chart.** Locate the `<text>` elements along the value axis — X-axis labels for horizontal bars, Y-axis labels for vertical bars, and Y-axis labels for line-like charts. Extract the first and last tick values to determine the axis range (e.g. `0%` to `120%` → range `0,120`). Pass this range as `--value-range`, `--y-range`, or `--x-range` as appropriate. Use the attached `--*-range=min,max` form, which also keeps a negative minimum from being parsed as another option. Radar uses `--max-value` instead of a range: read the outermost ring's tick value and pass it as `--max-value`. If the SVG has no explicit tick labels (data labels only, no grid), omit the range and let the calculator auto-normalize — but flag the receipt as `scale=auto (no ticks)`.
**Local vs absolute coordinates.** Many chart templates wrap chart content in `<g transform="translate(cx, cy)">` or similar, so child `<circle>`/`<polygon>`/`<rect>` coords are relative to that origin (e.g. radar polygon at `0,-198`, donut paths starting from `0,0` inside a translated `<g>`, dumbbell circles at `cy="0"` inside a per-row translated `<g>`). The calculator outputs **absolute** SVG coordinates. Before comparing, either add the wrapping translate's offset to the SVG coords or subtract it from the calculator's output — pick one direction and apply it consistently.
5. Run the matching calculator command:
@@ -75,11 +75,11 @@ For each page in the Step 1 list:
# IMPORTANT: always pass --value-range from axis tick labels (step 4)
python3 skills/ppt-master/scripts/svg_position_calculator.py calc bar \
--data "Label1:Value1,Label2:Value2" --area "x_min,y_min,x_max,y_max" \
--bar-width 120 --value-range "0,axis_max"
--bar-width 120 --value-range=0,axis_max
# line_chart / area_chart / scatter_chart — area uses line output as the top boundary, then closes to y_max
python3 skills/ppt-master/scripts/svg_position_calculator.py calc line \
--data "x1:y1,x2:y2,..." --area "x_min,y_min,x_max,y_max" --y-range "0,max"
--data "x1:y1,x2:y2,..." --area "x_min,y_min,x_max,y_max" --y-range=0,max
# pie_chart — default start angle is -90 (12 o'clock); pass --start-angle only if the SVG starts elsewhere
python3 skills/ppt-master/scripts/svg_position_calculator.py calc pie \
@@ -121,11 +121,11 @@ python3 skills/ppt-master/scripts/svg_quality_checker.py <project_path>
# Run 1 — bottom segment (origin = baseline)
python3 skills/ppt-master/scripts/svg_position_calculator.py calc bar \
--data "Q1:30,Q2:..." --area "x_min,100,x_max,500" \
--bar-width 80 --value-range "0,axis_max"
--bar-width 80 --value-range=0,axis_max
# Run 2 — top segment (origin shifted up by bottom segment's height in pixels)
python3 skills/ppt-master/scripts/svg_position_calculator.py calc bar \
--data "Q1:20,Q2:..." --area "x_min,100,x_max,<500 - bottom_height_px>" \
--bar-width 80 --value-range "0,axis_max"
--bar-width 80 --value-range=0,axis_max
```
**Stacked area** — for N stacked series, run `calc line` N times on **cumulative** y-values (series 1 raw; series 2 = series1+series2; …). Each call yields the top boundary of one band. Each band's SVG path closes to the **previous** band's top boundary (not to `y_max`).
@@ -140,7 +140,7 @@ Use these recipes for `decomposable-calc` and `partial-calc` pages. Each recipe
**Dumbbell chart** — for before/after or two-state values across categories. The two endpoints are **points**, not bar ends — `calc bar --horizontal` always anchors at `x_min`, which only matches the right endpoint. Use `calc line` × 2 instead, treating category index as the y axis:
1. Number categories `0.5, 1.5, …, N-0.5` so each row's y lands on its band center; set `--y-range "0,N"`. The same convention applies to vertical dumbbells with the axes swapped.
1. Number categories `0.5, 1.5, …, N-0.5` so each row's y lands on its band center; set `--y-range=0,N`. The same convention applies to vertical dumbbells with the axes swapped.
2. Set `--x-range` to the shared value-axis range read from ticks.
3. Run `calc line` once per endpoint series with identical `--area`, `--x-range`, `--y-range`. Each output `(SVG_X, SVG_Y)` is the matching endpoint circle's `(cx, cy)`.
4. Compare both endpoint circles and the connector line (`x1=cx_left, x2=cx_right, y1=y2=cy`) against the two calculated point sets.
@@ -150,17 +150,17 @@ Use these recipes for `decomposable-calc` and `partial-calc` pages. Each recipe
# Encode category index as the y value: row 1 → 0.5, row 2 → 1.5, row 3 → 2.5.
python3 skills/ppt-master/scripts/svg_position_calculator.py calc line \
--data "42:0.5,55:1.5,37:2.5" --area "100,100,700,460" \
--x-range "0,100" --y-range "0,3"
--x-range=0,100 --y-range=0,3
python3 skills/ppt-master/scripts/svg_position_calculator.py calc line \
--data "68:0.5,71:1.5,49:2.5" --area "100,100,700,460" \
--x-range "0,100" --y-range "0,3"
--x-range=0,100 --y-range=0,3
```
**Pareto chart** — split into descending bars plus cumulative line:
1. Run `calc bar` on the descending category values with the bar axis range from ticks.
2. Precompute cumulative percentages in category order.
3. Run `calc line` on `0.5:cum1,1.5:cum2,...,N-0.5:cumN` with `--x-range "0,N"`, the right-side percentage axis as `--y-range` (usually `0,100`), and the same `--area` as the bars. The `n - 0.5` offset puts each cumulative point on the matching bar's center; using `1,2,…,N` shifts the polyline left by half a bar width.
3. Run `calc line` on `0.5:cum1,1.5:cum2,...,N-0.5:cumN` with `--x-range=0,N`, the right-side percentage axis as `--y-range` (usually `--y-range=0,100`), and the same `--area` as the bars. The `n - 0.5` offset puts each cumulative point on the matching bar's center; using `1,2,…,N` shifts the polyline left by half a bar width.
4. Compare bar rects, cumulative line path, and cumulative markers separately.
**Dual-axis line chart** — split by axis:
@@ -2,8 +2,8 @@
"sourceId": "shadcn",
"repo": "https://github.com/shadcn-ui/ui.git",
"ref": "main",
"commit": "20442886c5cfb440441c35030462fbdf64838655",
"commit": "3f47b9113a173bac4a72cefda4d4c3f2d89b8ab6",
"adapter": "claude-skill",
"sourcePath": "skills/shadcn",
"syncedAt": "2026-07-20T16:00:00Z"
"syncedAt": "2026-07-22T16:00:00Z"
}