docs(plan): Plan 4 实施计划——阶段4 发布/主题/构建/预览(9任务,浏览器时刻)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 22:34:57 +08:00
parent 066ba289bb
commit 171137bc8e
@@ -0,0 +1,118 @@
# SiteForge 纵向切片 · Plan 4:发布、主题与静态预览(阶段 4 / 快线第 3 步)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** hesupipe 真实数据第一次在浏览器亮起来:`content:publish`(显式发布)→ 冻结 Render Snapshot → pilot-minimal 主题渲染六类页面 → Astro 静态构建(媒体本地化)→ 最小门禁 → 本地 Preview Server 按真实路由响应。验收 = 快线"第 10 天演示标准"(六类页面可浏览、真实内容、图片本地化、SEO 基线、200/404、sitemap、移动端无明显破版)。
**Architecture:** 依据快线 §8-§9、V2 spec §8(发布)/§9(主题与 Render Snapshot)/§12(Route Manifest 与 Preview)/§13(门禁子集);消费 Plan 3 移交清单 9 项(**必读**——含五条既定事实)。表已冻结(build_jobs/build_input_snapshots/build_artifacts/validation_runs 均为 Plan 1 遗产);契约已冻结(ThemeManifest/RENDER_SNAPSHOT_FILES 六文件)。**主题五禁**:不查库/不调 API/不读源快照/无 Seed/构建时断网。
**Tech Stack:** Astro 5(新依赖,进 themes/pilot-minimal 包)+ 既有栈。预览服务器零新依赖(Node http + route-manifest 匹配)。
**边界(不做)**:双构建 Hash 一致性(可复现性归 Plan 5/7)、SEO 三方 diff(Plan 5)、部署器/Release/发布锁(Plan 7)、第二主题、可视化编辑、构建沙箱容器化(本地进程纪律代替)。
---
## 前置事实(执行者必读)
1. **Plan 3 移交清单 9 项**(Plan 3 文末)+ 五条既定事实:① `/products/` 被 basic_page 占据——产品列表演示用 `/product-category/*`(10 个真实分类页)+ `/products/` 内容页走通用模板;② 19 条 `?p=` 无路由不进构建;③ is_primary first-import-wins;④ 五列锚点查询三份拷贝(本计划 publish 复用 content-writer 的或抽共享 helper——**抽共享 helper**,第 4 个消费方触发 Plan 3 审查既定条件);⑤ JSON/HTML 双写窄窗口——build 报告落盘改 HTML 先写或接受(顺手修)。
2. **数据现状**(临时库已 DROP——本计划验收时重建):192 entry(129 news/48 product/15 page)全为 current 未发布;2772 routes(html 190/redirect 1746/asset 836);836 media ready(public_path=旧 uploads 路径);**媒体副本已在 `storage/incoming/hesupipe-media/`(2625 文件 164.5MiB)+ content-addressed 快照**。bodyHtml 中图片是**旧站绝对 URL**(https://hesupipe.com/...)——渲染快照生成时映射为根相对 public_path(day-10"图片本地化"的兑现位)。
3. **发布语义**(V2 §8):publish = entry_locales.published_revision_id ← current_revision_id(首次批量发布 192 条);dry-run 展示差异清单 + confirm token;审计落 audit_logs。导入永不发布(已由 Plan 3 保证,本计划不碰导入代码)。
4. **Render Snapshot 六文件**(contracts 冻结):site/content/taxonomies/routes/assets/render-manifest.json——从 published 修订 + route_registry + media ready 集合物化,canonical JSON + 总哈希(build_input_snapshots 表记录)。
5. 既有纪律全套:TDD/临时库/四段 typecheck/身份断言/cms 只读/artifacts 与 storage 不入库。
6. 版本纪律:新依赖 Astro 锁 minor;许可证 MIT 记 ADR-018。
## 仓库结构增量
```text
themes/pilot-minimal/ # Astro 项目(独立 package.json:astro 依赖)
theme.manifest.ts # contracts.ThemeManifest 实现
src/layouts/BaseLayout.astro
src/components/ # SeoHead/SiteHeader/SiteFooter/Breadcrumbs/RichText/ResponsiveMedia/ProductCard/NewsCard/Pagination/NotFound
src/pages/[...slug].astro # 六类页面按 render data 分派(getStaticPaths 读输入目录)
src/lib/load-render-data.ts # 只读输入目录(SF_BUILD_INPUT 环境变量)
apps/api/src/modules/publish/ # publish.service(零装饰器)+ module
apps/api/src/modules/build/ # build.service 扩展(已有 spawn 构建骨架?无——Plan 1 只建了 build 触发占位?核实:无 build 模块,本计划新建)
apps/preview-server/ # 预览服务器(Node http,零依赖)
scripts/sf.ts # + content:publish / build:create / preview:start
```
---
### Task 1: content:publish 命令(显式发布)
**Files:** `apps/api/src/modules/publish/{publish.service.ts,publish.module.ts}`;sf.ts + `content:publish`;Test `tests/integration/publish.test.ts`
- [ ] 失败测试(临时库,mini 全链 register→import 后):① dry-run:列出 current 非空且 published≠current 的 192 条(mini 3 条)清单(typeKey/slug/修订对)+ 汇总计数,零写入;② confirm(token 从 dry-run 输出取):published_revision_id=current、audit_logs 行(action='content.publish' 批量摘要含条数)、重复发布幂等(已等则 skip);③ site_config_revisions 首版:首次 publish 时若站点无 config 修订 → 从 site 行生成 v1(name/origin/locale 空品牌占位——data 结构按后续主题消费定义:{name, origin, locale, brand:{}});④ 未导入条目不受影响
- [ ] 实现:`publishSiteContent(db,{siteKey, dryRun, confirmToken})`——查询起点 `entry_locales.current_revision_id IS NOT NULL AND (published_revision_id IS NULL OR published_revision_id != current_revision_id)`(事务批量 UPDATE);dry-run token=随机;五列锚点共享 helper 顺手抽 `apps/api/src/modules/import/anchors.ts`(三写入器改 import——零行为变更由既有测试守护)
- [ ] 绿 → commit `feat(publish): 显式发布命令(dry-run/confirm/审计/站点配置首版)`
### Task 2: Render Snapshot 物化 + build job
**Files:** `apps/api/src/modules/build/{build.service.ts,render-snapshot.ts,build.module.ts}`;sf.ts + `build:create`;Test `tests/integration/build-snapshot.test.ts`
- [ ] 失败测试(临时库,publish 后):① build_jobs 行(queued→snapshotting→building→…本任务到 snapshotting 完成即 succeeded 分支留 Task 6 接——**简化**:本任务状态机到 `snapshotting→validating(空)→succeeded`,dist 尚空,Task 6 换真实构建)+ build_input_snapshots 行(三哈希/site_config_revision_id/theme_key='pilot-minimal');② 六文件物化:site.json(config v1)/content.json(192 published 修订——**bodyHtml URL 重写**:旧绝对 URL→根相对 public_path,映射源=media ready 集合;未命中的绝对 URL 保留原样并计 issue)/taxonomies.json(927 term+taxonomy)/routes.json(html+asset 行,redirect 并入)/assets.json(836 ready:public_path→storage 实体路径+sha256)/render-manifest.json(版本块+总数+源哈希);③ canonical JSON 确定性(键排序);④ 未 publish 站点 → 明确报错 NOT_PUBLISHED
- [ ] 实现:一致性读取(单事务 repeatable read 或快照时点标记——简化:顺序读取+记录行数基线,差异检查);媒体实体路径解析用 register 快照 metadata 的 storageRoot+storage_path(`storage/snapshots/media_bundle/…` 已存在的 content-addressed 副本)+ incoming 副本兜底
- [ ] 绿 → commit `feat(build): 冻结 Render Snapshot 物化与 build job`
### Task 3: pilot-minimal 主题骨架 + 布局组件
**Files:** `themes/pilot-minimal/`(package.json/tsconfig/theme.manifest.ts/src/{layouts,components,lib});根 workspace 已含 themes/*;Test `themes/pilot-minimal/test/manifest.test.ts`
- [ ] 失败测试:theme.manifest 满足 contracts 的 isThemeManifest(key='pilot-minimal'/version/renderContractVersion='1'/supportedContentTypes 三类/requiredSiteConfig/六模板条目:home/page/product-list/product-detail/news-list/news-detail+404);渲染数据 loader 对六文件夹具的加载形状
- [ ] 实现:Astro 5 项目(dependencies 仅 astro);BaseLayout(html 骨架/SeoHead:title/desc/canonical/og 从 page data)/SiteHeader(site.name+简导航:首页/Products/News)/SiteFooter(©+name)/基础样式(design tokens 极简 CSS 变量——快线 §8.5 视觉优先级:宽度/字体/颜色/间距先对,不动像素级还原);组件:RichText(set:html 净化输出——输入已被 Plan 2 清洗+Plan 4 重写,信任链完整但组件仍加一层 sanitize-html 复检?**不加**——输入已清洗,重复清洗违反 YAGNI,JSDoc 记录信任链)/ResponsiveMedia(alt/尺寸/懒加载)/Breadcrumbs/ProductCard/NewsCard/Pagination/NotFound
- [ ] 绿 → commit `feat(theme): pilot-minimal 骨架与布局组件`
### Task 4: 六类页面路由与渲染
**Files:** `themes/pilot-minimal/src/pages/[...slug].astro` + 模板分派;Test `themes/pilot-minimal/test/render-cases.test.ts`(Astro 构建级测试:六类夹具数据 → 构建产物断言)
- [ ] 失败测试(六夹具页 = 真实 hesupipe 抽样数据的脱敏化形状——home(/)、generic page(/products/ 内容页)、product-category 列表、product 详情、news 列表 /news/、news 详情 .html):构建产物存在且含关键内容(页面 title/slug 渲染进 HTML、产品参数/新闻正文存在、图片标签 src=根相对);404.html 存在
- [ ] 实现:getStaticPaths 读 SF_BUILD_INPUT 的 routes.json(html 行)+ 按内容类型/路径特征分派模板:`/`→home;`/product-category/*` 或 `/category/*`→列表(taxonomy.json 查 term+content.json 过滤关联内容);内容页按 typeKey(product→详情含参数表+相册、news_article→正文+日期、basic_page→富文本);/news/ 特判 news-list。分页:taxonomy 列表超 20 条时 …/page/N/(快线允许简化——本切片 471 post_tag 无页面、product_cat 最大分类条数实测后定:**若单分类>50 再做分页,否则 V1 不做**,JSDoc 记录)
- [ ] 绿 → commit `feat(theme): 六类页面渲染与路由分派`
### Task 5: 构建管线(astro 构建 + 媒体落盘 + sitemap/robots)
**Files:** `build.service.ts` 扩展(spawn astro build→媒体复制→sitemap→产物登记);Test `tests/integration/build-pipeline.test.ts`
- [ ] 失败测试(小夹具全链):① spawn `pnpm --filter pilot-minimal build`(SF_BUILD_INPUT 指输入目录)→ dist/ 页面按 routes.json 的 html 行路径落盘(route_path→文件映射:尾斜杠→index.html;.html→原样);② 媒体复制:assets.json 逐条复制到 dist 的 public_path(跳过 quarantined——build 输入本就只有 ready);复制校验 sha256 抽检;③ sitemap.xml(origin+indexable html 行)+ robots.txt(Allow / + Sitemap 行 + 不含 noindex 页);④ build_artifacts 行(dist/manifest 各 kind+哈希);⑤ 构建环境五禁断言:SF_BUILD_INPUT 之外零数据源(loader 单一入口 grep 守护 + 构建期数据库连接断开测试——DATABASE_URL 置空跑构建仍成功)
- [ ] 实现:build.service 编排(快照→astro→媒体→sitemap→登记);产物落 `artifacts/jobs/<buildJobId>/{input,dist,manifests}`
- [ ] 绿 → commit `feat(build): astro 构建管线与媒体落盘`
### Task 6: 最小门禁 + 完整 build 状态机
**Files:** `apps/api/src/modules/build/gates.ts`;sf.ts + `build:validate`;Test `tests/integration/build-gates.test.ts`
- [ ] 失败测试(夹具产物注入错误逐项触发):① 内部死链:dist 内 a[href]/img[src] 内部目标全命中(页面或 assets)——注入坏链 → fail;② sitemap 一致:sitemap 条目=实际 indexable html 集;③ SEO 基线:每 html 有 title/meta description/canonical(指向 origin+自身路径)/恰一 H1;④ 大小写冲突:dist 路径 lowercase 无重复;⑤ 产物完整性:route-manifest 的 target_file 全存在。全过 → validation_runs 行 PASS
- [ ] 实现:门禁引擎(纯函数读 dist+manifests → issues 按 P0/P2 分级——P0 阻断 P2 报告);build 状态机补 validating→succeeded/failed
- [ ] 绿 → commit `feat(build): 最小门禁与状态机闭环`
### Task 7: Preview Server
**Files:** `apps/preview-server/{server.mjs,package.json}`;sf.ts + `preview:start`;Test `tests/integration/preview-server.test.ts`
- [ ] 失败测试(真实 http 请求小夹具构建产物):① html 行 200+Content-Type;② redirect 行 301+Location(?attachment_id= 查询形态匹配);③ asset 行 200+正确 MIME(png/jpg/webp/pdf);④ 未命中 404+自定义页;⑤ 全响应 `X-Robots-Tag: noindex`;⑥ 绑定 127.0.0.1;⑦ 路径穿越拒绝(../ 编码形式)
- [ ] 实现:Node http 服务器读 dist+route-manifest.json(exact_key 匹配+query_key 提取);MIME 表复用 magic-bytes 思路(按扩展名+魔数双确认可选——扩展名够用)
- [ ] 绿 → commit `feat(preview): 路由驱动的本地预览服务器`
### Task 8: 真实 hesupipe 端到端(验收——浏览器时刻)
**Files:** `docs/evidence/phase4-preview.md`;可能 `scripts/e2e-real.mjs`(编排复验)
- [ ] 临时库重建全链:register(副本复用)→ import → **publish(192 条)** → build → validate → preview:start
- [ ] 验收断言(快线 §8.7 第 10 天标准逐条):① 六类页面 HTTP 200 且内容来自真实数据(首页/一个内容页/产品分类列表/产品详情含参数/新闻列表/新闻详情);② 图片本地化抽检 10 张(dist 内存在+页面引用根相对);③ 每页 title/description/canonical 抽检 5 页;④ 404 与 301 行为(?attachment_id= 抽检);⑤ sitemap.xml 条目数=indexable html 数;⑥ 移动端 375px 截图无明显破版(Playwright MCP 截图 3 页存证据);⑦ 全量 URL 冒烟:route-manifest html 行全 200(190 页)+redirect 抽检 50
- [ ] 耗时记录(快照物化/astro 构建/媒体复制/门禁分解——对照 Plan 3 基线 51.8s);差异如实分析。证据文档 + commit `test(evidence): 阶段4端到端预览验收`;临时库 DROP;storage 副本保留
### Task 9: 收尾
- [ ] ADR-018(Astro 5 选型与许可证/渲染信任链——bodyHtml 双重清洗决策/分页简化决策/预览零依赖);README(preview 截图/命令链 publish→build→preview);typecheck 链含 themes/pilot-minimal 与 preview-server(如 TS);lint;三绿全量
- [ ] 移交 Plan 5 清单:SEO 三方 diff(source_rendered 抓取需求)/完整门禁(redirect 环/OG/hreflang)/可复现双构建/部署前置(836 nginx conf 输出);续传 Task 0.2 备份
- [ ] commit + push 由用户执行
## 验收(对照快线 §8.7 + V2 §15 阶段 4 子集)
- [ ] 显式发布可用且导入产物发布前不可见(published 指针语义)
- [ ] Render Snapshot 冻结六文件 + 构建五禁(无库无网无 seed)
- [ ] 六类页面真实数据渲染 + 图片本地化
- [ ] Preview 200/301/404/noindex/回环
- [ ] 最小门禁五项全绿
- [ ] 真实 hesupipe 端到端验收 + 移动端截图证据