docs: ADR-018/README/Plan 5 移交清单

Plan 4 Task 9 收尾(阶段 4 收官):

- ADR-018(主题与构建决策八条):Astro 5.18 选型与 MIT/依赖只进主题包/
  vitest 双版本(根 2.1.9 + 主题 3.2.7——astro vite6 兼容);渲染信任链
  bodyHtml 不二次清洗(三段接力);分页 V1 不做——路由登记义务随迁移移交;
  P0 发布门禁偏离(issue 无 DB 源,Plan 5 SEO diff 重审);编码路径四层
  字节精确 + 资产键 URL 线形归一(236f3dc);预览零依赖 + host 白名单
  收紧(回环纪律,比 spec §12.2 更严);CSP style-src 'unsafe-inline'
  (Astro scoped 样式内联);报告 HTML 先写两域对齐
- README:快速开始补 publish→build→preview 全链命令链示例(含
  artifacts/jobs/<jobId>/ 产物路径);目录结构补 themes/ 与
  apps/preview-server/;当前状态阶段 4 完成(真实 hesupipe 190 页可
  浏览器预览/408 测试/23 验收断言/6 截图);ADR-001~018 导航;下一步
  指向 Plan 4 文末移交清单
- Plan 4 文档:Task 9 勾选 + 验收节 ✅ 全过;文末新增"移交 Plan 5 清单"
  7 项(SEO 三方 diff 新输入源/完整门禁六件含可复现双构建与 footer 年份
  固定化/sitemap-robots 预览可达/og 元数据/836 nginx conf 输出/Task 0.2
  备份续传/度量基线 ~64s 首跑·~15s 复跑)

三绿全量:lint 过 / typecheck 五段过 / test 408(根 382 + 主题 26)全绿。
不 push——推送由用户执行。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-22 15:59:48 +08:00
parent 5c2ee7dc5d
commit 87d6b9a777
3 changed files with 88 additions and 21 deletions
+21 -9
View File
@@ -14,16 +14,25 @@ pnpm db:migrate # 开发库全量迁移(apps/api/drizzle
pnpm db:test # 一键验收:起库 + 跑全部测试(测试用临时库,开发库零写入) pnpm db:test # 一键验收:起库 + 跑全部测试(测试用临时库,开发库零写入)
``` ```
`pnpm sf` CLI 速览(tsx 直吃 TS,无需构建)——阶段 2 探测/登记 + 阶段 3 导入: `pnpm sf` CLI 速览(tsx 直吃 TS,无需构建)——阶段 2 探测/登记 → 阶段 3 导入 → 阶段 4 发布/构建/预览:
```bash ```bash
pnpm sf wxr:probe --wxr path/to/export.xml # 只读诊断:流式解析→映射→清洗→计数报告(不入库) pnpm sf wxr:probe --wxr path/to/export.xml # 只读诊断:流式解析→映射→清洗→计数报告(不入库)
pnpm sf source:register --site <key> --wxr <path> [--sitemap <path>] [--media <dir>] [--media-map <json>] # 快照登记(content-addressed、幂等) pnpm sf source:register --site <key> --wxr <path> [--sitemap <path>] [--media <dir>] [--media-map <json>] # 快照登记(content-addressed、幂等)
pnpm sf import --site <key> [--snapshot <id>] [--products-path /products/] [--news-path /news/] # 导入落库(幂等 job + JSON/HTML 报告) pnpm sf import --site <key> [--snapshot <id>] [--products-path /products/] [--news-path /news/] # 导入落库(幂等 job + JSON/HTML 报告)
pnpm sf content:publish --site <key> [--confirm <token>] [--actor <label>] # 显式发布(缺省 dry-run 出清单+token;--confirm 执行;导入永不发布)
pnpm sf build:create --site <key> [--key <idempotencyKey>] # 冻结 Render Snapshot 六文件 → astro 构建 → 媒体落盘/sitemap/robots → 五门禁 → 产物登记
pnpm sf preview:start --build <jobId> [--port 4173] # 路由驱动本地预览(只读 dist+route-manifest;回环绑定;X-Robots-Tag noindex)
# 全链示例(register → import;同快照重复 import 复用既有 job 不重跑) # 全链示例(register → import → publish → build → preview;同快照/同发布集合重复执行均幂等复用)
pnpm sf source:register --site demo --wxr storage/incoming/export.xml --media storage/incoming/media --media-map storage/incoming/media-map.json pnpm sf source:register --site demo --wxr storage/incoming/export.xml --media storage/incoming/media --media-map storage/incoming/media-map.json
pnpm sf import --site demo # 报告落 artifacts/import/<jobId>/reports/import.{json,html} pnpm sf import --site demo # 报告落 artifacts/import/<jobId>/reports/import.{json,html}
pnpm sf content:publish --site demo # dry-run:差异清单 + confirm token(零写入)
pnpm sf content:publish --site demo --confirm <token> # published ← current + audit_logs + site_config v1
pnpm sf build:create --site demo # 产物落 artifacts/jobs/<buildJobId>/{input,dist,manifests,reports}
# input/ 六文件快照 · dist/ 页面+媒体+sitemap.xml+robots.txt+404.html
# manifests/route-manifest.json · reports/build.{json,html}(HTML 先写)
pnpm sf preview:start --build <buildJobId> # http://127.0.0.1:4173/ ——六类页面/301/资产/404 按真实路由响应
``` ```
日常质量门(等价于 CI): 日常质量门(等价于 CI):
@@ -37,12 +46,14 @@ pnpm lint && pnpm typecheck && pnpm test
## 目录结构 ## 目录结构
``` ```
apps/api/ NestJS standalone 骨架 + Drizzle schema/迁移(21 表,数据库约束即规格)+ source 快照登记 + 导入管道(四写入器编排) apps/api/ NestJS standalone 骨架 + Drizzle schema/迁移(21 表,数据库约束即规格)+ source 快照登记 + 导入管道(四写入器编排)+ 发布/构建/门禁服务
apps/preview-server/ 本地预览服务器(纯 node:http 零依赖;route-manifest 驱动;回环绑定纪律)
packages/contracts/ UniversalRow / Route / Render / Theme 跨源契约(冻结,变更须升版) packages/contracts/ UniversalRow / Route / Render / Theme 跨源契约(冻结,变更须升版)
packages/adapter-wordpress/ WXR 适配器(saxes 流式解析 + PHP unserialize + 映射 + HTML 白名单清洗,纯函数无 DB) packages/adapter-wordpress/ WXR 适配器(saxes 流式解析 + PHP unserialize + 映射 + HTML 白名单清洗,纯函数无 DB)
tests/ 单元 + 集成测试(fixtures:WXR/Sitemap/媒体样例) themes/pilot-minimal/ pilot 极简主题(Astro 5 静态构建;渲染数据唯一入口 load-render-data——五禁纪律的结构保证)
tests/ 单元 + 集成测试(fixtures:WXR/Sitemap/媒体样例;临时库 globalSetup)
infra/ docker-compose:PostgreSQL 16(端口 5434,避开 cms 项目 5433) infra/ docker-compose:PostgreSQL 16(端口 5434,避开 cms 项目 5433)
scripts/ migrate-test.sh 等运维脚本 · sf.ts(wxr:probe / source:register / import CLI) scripts/ migrate-test.sh 等运维脚本 · sf.ts(probe/register/import/publish/build/preview CLI)· e2e-real.mjs(真实数据端到端编排)
docs/ adr/ 决策记录 · evidence/ 基线证据 · superpowers/ 规格与计划 docs/ adr/ 决策记录 · evidence/ 基线证据 · superpowers/ 规格与计划
``` ```
@@ -50,12 +61,13 @@ docs/ adr/ 决策记录 · evidence/ 基线证据 · superpowers/
- 设计基线(表结构/技术决策/阶段划分):`docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design-v2.md` - 设计基线(表结构/技术决策/阶段划分):`docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design-v2.md`
- 阶段 0+1 实施计划:`docs/superpowers/plans/2026-10-20-slice-foundation-phase0-1.md` - 阶段 0+1 实施计划:`docs/superpowers/plans/2026-10-20-slice-foundation-phase0-1.md`
- 架构决策记录(ADR-001~017):`docs/adr/` - 架构决策记录(ADR-001~018):`docs/adr/`
- 基线证据(环境/真实 WXR/阶段 3 导入验收):`docs/evidence/` - 基线证据(环境/真实 WXR/阶段 3 导入/阶段 4 端到端预览验收):`docs/evidence/`
## 当前状态 ## 当前状态
- **阶段 1 已完成**:契约冻结、21 表数据模型全量落地(不可变触发器/唯一约束/CHECK 均落数据库并被 75 项测试锁定)、空环境一键启动+迁移验收通过。 - **阶段 1 已完成**:契约冻结、21 表数据模型全量落地(不可变触发器/唯一约束/CHECK 均落数据库并被 75 项测试锁定)、空环境一键启动+迁移验收通过。
- **阶段 2 已完成**:WXR 流式适配器(saxes 流式解析 → PHP unserialize → UniversalRow 映射 → HTML 白名单清洗 → 导入前报告)、`source:register` 快照登记(content-addressed、幂等)与 `pnpm sf` CLI;真实 hesupipe WXR(5.5MB)冒烟 18 项计数对照全过(峰值堆 62MiB < 100MiB,证据见 `docs/evidence/phase2-smoke.md`)。 - **阶段 2 已完成**:WXR 流式适配器(saxes 流式解析 → PHP unserialize → UniversalRow 映射 → HTML 白名单清洗 → 导入前报告)、`source:register` 快照登记(content-addressed、幂等)与 `pnpm sf` CLI;真实 hesupipe WXR(5.5MB)冒烟 18 项计数对照全过(峰值堆 62MiB < 100MiB,证据见 `docs/evidence/phase2-smoke.md`)。
- **阶段 3 已完成**:内容导入管道(`sf import`——content/term/media/route 四家写入器串行编排、稳定身份双轨幂等、checksum skip、route_registry §6.1 全表处置、JSON+HTML 导入报告与错误封顶);真实 hesupipe 全量导入验收通过——192 内容条目/1122 source_records/927 terms/2772 routes 落库,二导幂等(0 新修订)、published 指针全程未动(ADR-010),333 测试全绿(证据见 `docs/evidence/phase3-import.md`)。 - **阶段 3 已完成**:内容导入管道(`sf import`——content/term/media/route 四家写入器串行编排、稳定身份双轨幂等、checksum skip、route_registry §6.1 全表处置、JSON+HTML 导入报告与错误封顶);真实 hesupipe 全量导入验收通过——192 内容条目/1122 source_records/927 terms/2772 routes 落库,二导幂等(0 新修订)、published 指针全程未动(ADR-010),333 测试全绿(证据见 `docs/evidence/phase3-import.md`)。
- **下一步:阶段 4**——content:publish 命令 + Render Snapshot + pilot-minimal 主题 + Astro 构建 + 本地 Preview;输入清单见 Plan 3 文末"移交 Plan 4 清单"。 - **阶段 4 已完成**:发布与静态预览全链(`content:publish` 显式发布/审计/站点配置首版 → Render Snapshot 六文件冻结(ADR-011)→ pilot-minimal 主题 Astro 构建(媒体本地化/sitemap/robots/route-manifest §12.1)→ 五项最小门禁 → 路由驱动本地 Preview(回环绑定/noindex));**真实 hesupipe 浏览器时刻兑现**——190 页全 200、1746 redirect 全 301、编码中文路径字节精确贯穿(ADR-018)、验收 23 断言 + 双视口 6 截图全过、五门禁 PASS(p0=0);主题与构建决策入册 ADR-018,**408 测试全绿**(证据见 `docs/evidence/phase4-preview.md`,截图 `docs/evidence/screenshots/`)。
- **下一步:阶段 5(Plan 5)**——SEO 三方差异 + 完整门禁 = M1 完整迁移预览;输入清单见 Plan 4 文末"移交 Plan 5 清单"(`docs/superpowers/plans/2026-10-21-slice-build-preview-phase4.md`)。
+43
View File
@@ -0,0 +1,43 @@
# ADR-018:主题与构建决策(Astro 选型/渲染信任链/分页不做/P0 发布门禁偏离/编码路径贯穿/预览回环与 CSP/报告写序)
- **状态**:已采纳(Plan 4 Task 9 收尾,阶段 4;实现锚点 `themes/pilot-minimal/` · `apps/api/src/modules/build/` · `apps/api/src/modules/publish/publish.service.ts` · `apps/preview-server/server.mjs`;验收证据 `docs/evidence/phase4-preview.md`——真实 hesupipe 190 页浏览器预览,23 断言 + 6 截图全过)
## 决策一:Astro 5.18 选型与依赖布局(MIT)
1. **主题引擎 = Astro 5(锁定 5.18.x minor,实装 5.18.2)**:静态优先(零客户端运行时)、`[...slug].astro` 单入口 + `getStaticPaths` 读输入目录的路由模型与"route-manifest 是唯一权威"的落盘规则天然咬合、文件级布局组件(BaseLayout/SeoHead/RichText)贴合六类页面分派。**许可证 MIT**(前置事实 6 的版本纪律复查通过),依赖只进 `themes/pilot-minimal/package.json`(根 workspace 不引入——构建工具不污染 API/测试面)。真实数据规模实测:190 页纯 astro 构建 4.9-7.5s。
2. **vitest 双版本并存(根 2.1.9 / 主题 3.2.7)**:主题测试要 `import .astro` 必须挂 astro 的 vite 插件(`getViteConfig`),astro 5.18 携 vite 6——根 vitest 2.x 系不兼容,故主题包独立 devDependency `vitest@^3.2`。两套 runner 各跑各的 package(根 `vitest.config.ts` 不含 themes,主题自有 `vitest.config.ts`),`pnpm test` 串联(382 + 26 = 408)。**不把根升 3.x**:根栈(pg 临时库 globalSetup/长时集成用例)在 2.1.9 上稳定,为一个子包升根属无收益风险面。
3. **主题五禁的结构保证**:渲染数据唯一入口 `src/lib/load-render-data.ts`(只读 `SF_BUILD_INPUT` 目录)——不查库/不调 API/不读源快照/无 seed/构建期断网由入口单一性 + 集成测试五禁断言(含 DATABASE_URL 置空、fetch/net stub)共同锁定。
## 决策二:渲染信任链——bodyHtml 不二次清洗
4. **主题端 `set:html` 直出,不清洗**(`RichText.astro`):bodyHtml 的信任链是三段接力——① Plan 2 适配器 HTML 白名单清洗(ADR-009,入库存的就是清洗产物);② Plan 4 Task 2 快照物化期 URL 重写(`rewriteBodyHtml`——仅做媒体 URL 到根相对 public_path 的映射改写,命中面以 media ready 集合为界,未命中保留原样 + issue);③ 主题经 load-render-data 唯一入口消费。三段之外**无第四个写方**,主题端再清洗是冗余面(且会破坏与 source_rendered 的逐字节对拍——Plan 5 SEO 三方 diff 的比较基准)。**禁"顺手加固"**:任何在主题层加清洗/改写的行为都属破坏信任链,须先升版本评审。
## 决策三:分页 V1 一律不做——路由登记义务随迁移移交
5. **列表页单页全列**(/news/ 129 条、taxonomy 列表同;`Pagination.astro` 保留为禁用态接口组件):分页路由(/news/page/2/ 等)不在 route_registry——未登记路由即不可达,硬做分页会**自破门禁①死链**(分页链接指向无路由路径)。V1 不做的完整语义:主题不做、管线不造、门禁不查。**移交义务**:Plan 5 起做分页必须**先扩路由登记**(route_registry 造行 → routes.json → dist → manifest 全链),路由登记是分页的前置义务而非主题层决定——该义务随迁移移交 Plan 5+,触发时连门禁死链白名单一并定。
## 决策四:P0 发布门禁偏离(前置事实 8②)
6. **V1 publish 不读导入报告文件做 P0 阻断**(`publish.service.ts` 头注记):spec §8 语义是发布前拒绝未解决 P0 issue,但 import issue 只落报告文件(artifacts/)、**无 DB 查询源**——publish 要阻断只能重读报告 JSON,而真实数据 206 条 issue 现状下严格门禁会**反堵发布**(其中大量是源数据固有形态而非阻断级问题)。真实防线不缺位:missing 媒体等在 build/gate 层有实物防线(artifact-completeness/死链/SEO 门禁 + validation_runs 落库)。**Plan 5 SEO 三方 diff 时重审**——届时 issue 分级(P0-P3)落地,publish 门禁改读 DB 化的 issue 源。
## 决策五:编码路径贯穿规则 + 资产键编码归一(236f3dc 修补)
7. **全程编码原样(字节精确)**:routes.json → dist 文件路径 → route-manifest → preview 匹配,四层对百分号编码路径(真实 54 条如 `/%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac/`)**不解码不归一**。理由:解码归一是歧义面(编码大小写/二次编码/非法序列),V1 无消费方需要解码形态;Windows/NTFS 下 '%' 是合法文件名字符,编码原样落盘无障碍。解码归一归 SP3 验证器(届时作为校验规则而非数据改写)。
8. **资产键例外——URL 线形归一**(验收内实证修补 236f3dc):route-manifest 资产行 exact_key 若用文件名本体(裸全角括号/顿号/间隔号),浏览器对非 ASCII **一律百分号编码后发请求**,裸键永不命中 → 预览破图(生产面 nginx 先解码不受影响)。修补:`pipeline.toManifestRoute` 把 exact_key 归一为 URL 线形(可打印 ASCII 原样、其余 UTF-8 百分号编码大写 hex),target_file 仍盘上裸名、DB/渲染快照域不变。配套不变量:manifest 全键纯 ASCII(e2e 断言 ⑦c)。同源修补:预览 Location 头 ASCII 化(`asciiLocation()`——RFC 9110 头域是 ASCII,裸全角会 ERR_INVALID_CHAR 崩 500)。
## 决策六:预览零依赖 + host 白名单收紧(比 spec 更严)
9. **零新依赖**:预览服务器 `apps/preview-server/server.mjs` 纯 node:http(.mjs 不进 TS 链,类型面由相邻 server.d.mts 供给)——匹配器(exact_key + query_key)、MIME 表、流式文件服务(createReadStream 直 pipe,64MB RSS 平坦实测)全手写;route-manifest 是唯一输入。
10. **host 白名单比 spec §12.2 更严**:spec 允许"除非显式指定"其他地址,实现收紧为**只允许 127.0.0.1/localhost**——库级在服务构造点即拒(先于监听),CLI 级先于一切(含 DB 访问前)拒绝。理由:预览产物带 `X-Robots-Tag: noindex` 但无鉴权,回环纪律是唯一防线;"显式指定对外地址"没有正当用例,留口子只会被误用。要对外 = 走部署器(Plan 7 的领域)。
11. **安全面全套**:穿越预检先于 URL 解析(new URL 会规范化点段——实测洞)、realpath + lexical 双检防 junction 逃逸、目录即 404 禁列表、仅 GET/HEAD、全响应 noindex。
## 决策七:CSP style-src 'unsafe-inline'(Astro scoped 样式兼容)
12. **预览 CSP = `default-src 'self'; style-src 'self' 'unsafe-inline'`**:Astro 5 静态产物把 scoped 样式**内联为 `<style>` 元素**(无外部样式表),`default-src 'self'` 会拦掉全部内联样式——页面在浏览器裸奔(HTTP 级测试看不见,浏览器时刻才显影,Task 8 实证)。只放开样式面:脚本/连接/图片等仍走 default-src 'self'。**不换 Astro 配置规避**(external stylesheet 会改变产物形态与可复现面),接受这一条最小放行并在此记档。
## 决策八:报告 HTML 先写——两域对齐
13. **build 报告与 import 报告同序:HTML 先写**(`report-html.ts` + build.service 落盘点):build.json 在则 build.html 必在——HTML 渲染失败时磁盘不留"JSON 新终态 + HTML 旧终态"的窄窗口(Plan 3 移交项⑤的兑现,两个域从此同纪律)。HTML 渲染面全动态文本转义、静态骨架无 script(Plan 2 Task 6 模板纪律)。
- **理由**:主题层"最薄可信渲染"(信任链三段接力 + 唯一入口)、预览层"最窄暴露面"(零依赖 + 回环 + 只读两文件)、门禁层"实物防线优先于台账阻断"(P0 偏离的真实防线论证)——三条线索共同把复杂度压在数据侧(快照冻结),消费侧全部薄层。
- **权衡/陷阱**:① 分页不做意味着 /news/ 129 条单页——真实站可浏览性达标(验收过),但内容量再涨一个量级前 Plan 5 必须落路由登记义务;② P0 发布门禁偏离是**有意欠账**——issue DB 化前 publish 无阻断,靠 build/gate 层兜底,Plan 5 重审时若引入 publish 阻断须先做 issue 分级落库;③ 编码路径"原样"与资产键"归一"并存不矛盾——前者是四层贯穿的不解码纪律,后者是键的**构造规则**(进 manifest 前就写成 URL 线形),排查时先分清键在哪一层;④ vitest 双版本是过渡态——根升 3.x 的时机是 vitest 2.x 出安全分支停维护或根需要 vite 6 特性时,届时一次性迁移并复跑全套。
@@ -106,20 +106,32 @@ scripts/sf.ts # + content:publish / build:create / preview:star
- [ ] **Task 4 审查移交的 Task 8 观察项**:分派器对以下形态是有意明错(build 期 throw)——`/news/` 硬编码特判外的其他主列表路径、分类嵌套(/product-category/parent/child/)、分类索引路径(/product-category/ 裸前缀);真实 190 路由集若含这些形态构建会失败——**届时按错误信息补分派规则即可,非缺陷** - [ ] **Task 4 审查移交的 Task 8 观察项**:分派器对以下形态是有意明错(build 期 throw)——`/news/` 硬编码特判外的其他主列表路径、分类嵌套(/product-category/parent/child/)、分类索引路径(/product-category/ 裸前缀);真实 190 路由集若含这些形态构建会失败——**届时按错误信息补分派规则即可,非缺陷**
- [ ] 耗时记录(快照物化/astro 构建/媒体复制/门禁分解——对照 Plan 3 基线 51.8s);差异如实分析。证据文档 + commit `test(evidence): 阶段4端到端预览验收`;临时库 DROP;storage 副本保留 - [ ] 耗时记录(快照物化/astro 构建/媒体复制/门禁分解——对照 Plan 3 基线 51.8s);差异如实分析。证据文档 + commit `test(evidence): 阶段4端到端预览验收`;临时库 DROP;storage 副本保留
### Task 9: 收尾 ### Task 9: 收尾 ✅ 完成(5c2ee7d helper 收敛 + docs 收官 commit;ADR-018 八决策/README 全链命令链与阶段 4 状态/Plan 5 移交清单 7 项;三绿全量 408(根 382 + 主题 26)+ lint + typecheck 五段;commit 两个不 push——推送由用户执行)
- [ ] ADR-018(Astro 5 选型与许可证/渲染信任链——bodyHtml 不二次清洗决策/分页 V1 不做及路由登记义务移交/P0 发布门禁偏离/编码路径贯穿规则/预览零依赖);README(preview 截图/命令链 publish→build→preview);typecheck 链含 themes/pilot-minimal 与 preview-server(如 TS);lint;三绿全量 - [x] ADR-018(`docs/adr/ADR-018-主题与构建决策.md` 八决策:①Astro 5.18 选型 MIT/依赖只进主题包/vitest 双版本(根 2.1.9 + 主题 3.2.7——astro vite6 兼容,不升根)②渲染信任链——bodyHtml 不二次清洗(三段接力:Plan 2 清洗 + Task 2 重写 + load-data 唯一入口)③分页 V1 一律不做(未登记路由自破门禁①死链——路由登记义务随迁移移交)④P0 发布门禁偏离(issue 无 DB 查询源,206 条真实现状会反堵发布;Plan 5 SEO diff 时重审)⑤编码路径贯穿规则(四层字节精确 + 资产键 URL 线形归一 236f3dc)⑥预览零依赖 + host 白名单收紧(比 spec §12.2 更严——回环纪律)⑦CSP style-src 'unsafe-inline'(Astro scoped 样式内联实证)⑧报告 HTML 先写两域对齐);README(快速开始 publish→build→preview 全链命令链含产物路径/目录结构补 themes 与 preview-server/当前状态阶段 4 完成 + 下一步指向移交清单/ADR-001~018);typecheck 五段链(含 themes/pilot-minimal);lint;三绿全量 408
- [ ] **Task 6 审查遗留**:① cleanupSiteRows/makeWork/seedPublishedSite 三份测试 helper 迁 tests/integration/helpers.ts(schema 再演进防漂移)② build.html 尾换行与 import 域对齐(若 Task 6 快修未覆盖)③ Plan 5 前置:绝对 origin 链接归一纳入死链检查;redirect 环/OG/hreflang 完整门禁;build:validate CLI;sitemap 重复 loc multiset 语义 - [x] **Task 6 审查遗留**:① cleanupSiteRows/makeWork/seedPublishedSite 三份测试 helper 迁 tests/integration/helpers.ts(5c2ee7d——Task 6 审查时三份,Task 7 preview-server 又落第四份一并收敛;seedPublishedSite 增 keyPrefix 参数保各文件站点 key 派生前缀不变;零行为变更由 408 测试守护)② build.html 尾换行与 import 域对齐——**Task 6 快修 fa27a3a 已覆盖**(writeAtomically(reportHtmlPath, `${render}\n`),与 import 域 `${html}\n` 同款,核实无需再动)③ Plan 5 前置四项(绝对 origin 归一/redirect 环/OG/hreflang 完整门禁、build:validate CLI、sitemap 重复 loc multiset)全部入文末移交清单第 2 项
- [ ] 移交 Plan 5 清单:SEO 三方 diff(source_rendered 抓取需求)/完整门禁(redirect 环/OG/hreflang)/可复现双构建/部署前置(836 nginx conf 输出);续传 Task 0.2 备份 - [x] 移交 Plan 5 清单(见文末新节定稿 7 项:SEO 三方 diff(source_rendered 抓取——旧站 HTML 抓取器是新输入源 + P0-P3 分级门禁)/完整门禁补齐六件(含可复现双构建与 footer 年份固定化)/sitemap-robots 预览可达/og:type=article·og:image·/category/ NewsCard/836 条 301 nginx conf 输出/续传 Task 0.2 cms 备份(阶段 6 L3 前必补)/度量基线(~64s 首跑·~15s 复跑·import 51s 占大头))
- [ ] commit + push 由用户执行 - [x] commit 两个(refactor(test) + docs);push 由用户执行
> 组件清单省略注记:快线 §8.2 的 ProductGrid/NewsList 由模板内联网格实现、TaxonomyList V1 无消费场景(首页不做分类导航)——避免与 §8.2 对照误判遗漏。 > 组件清单省略注记:快线 §8.2 的 ProductGrid/NewsList 由模板内联网格实现、TaxonomyList V1 无消费场景(首页不做分类导航)——避免与 §8.2 对照误判遗漏。
## 验收(对照快线 §8.7 + V2 §15 阶段 4 子集) ## 验收(对照快线 §8.7 + V2 §15 阶段 4 子集)✅ 全过(Task 8 验收 + 23 断言/6 截图,证据 `docs/evidence/phase4-preview.md`)
- [ ] 显式发布可用且导入产物发布前不可见(published 指针语义) - [x] 显式发布可用且导入产物发布前不可见(published 指针语义——publish.test.ts + build-snapshot ADR-010 双修订测试锁定)
- [ ] Render Snapshot 冻结六文件 + 构建五禁(无库无网无 seed) - [x] Render Snapshot 冻结六文件 + 构建五禁(无库无网无 seed——build-pipeline 五禁断言:loader 单一入口/DATABASE_URL 置空/fetch+net stub/无 seed/不读源快照)
- [ ] 六类页面真实数据渲染 + 图片本地化 - [x] 六类页面真实数据渲染 + 图片本地化(断言 ①a-①f + ② 10/10;媒体本地化 836 文件 sha256 全量校验)
- [ ] Preview 200/301/404/noindex/回环 - [x] Preview 200/301/404/noindex/回环(断言 ④a/④b/④c + ⑦a 190 全 200 + ⑦b 1746 全 301;绑定 127.0.0.1 断言)
- [ ] 最小门禁五项全绿 - [x] 最小门禁五项全绿(断言 ⑨:min-gates-v1 PASS p0=0 p2=0——validation_runs 落库)
- [ ] 真实 hesupipe 端到端验收 + 移动端截图证据 - [x] 真实 hesupipe 端到端验收 + 移动端截图证据(23/23 PASS + 双视口 6 截图 `docs/evidence/screenshots/phase4-{mobile-375,desktop-1440}-*.png`)
---
## 移交 Plan 5 清单(Plan 4 收官定稿——阶段 5:SEO 三方差异 + 完整门禁 = M1 完整迁移预览,计划的必读输入)
1. **SEO 三方差异(source_rendered 抓取需求——新输入源)**:旧站 HTML 抓取器是新输入通道(按 route origin 爬旧站页面取 source_rendered,与 dist 渲染输出、导入期 source Record 三方对拍——title/description/canonical/H1/og 逐项 diff);这是 Plan 2 以来第一个新输入源,register 侧需扩 source_type 与快照形态。**比较基准纪律**:source_rendered 对拍要求 dist 逐字节可信——ADR-018 决策二的信任链(不二次清洗)是该 diff 的前提,任何主题层改写都会破坏对拍。**P0-P3 分级门禁同此落地**(issue DB 化 + 分级——ADR-018 决策四 publish 门禁重审的前置)。
2. **完整门禁补齐**(gates.ts 扩展,五门禁 → 完整集):① redirect 环/多跳(manifest 范围内沿 Location 链跟随,环 = P0);② OG/hreflang 面;③ 绝对 origin 死链归一(internal-links 吸收 `https://<origin>/...` 形态——当前按外域豁免);④ sitemap 重复 loc multiset 语义(重复 loc = issue——当前集合语义测不出);⑤ build:validate CLI(对既有产物独立重跑门禁——runGates 运行面已有,缺 sf.ts 入口);⑥ 可复现双构建(冻结输入两次全构建 dist 哈希一致——**footer 年份固定化也在此时**:© 年份现取构建时刻年份,破坏可复现,双构建门禁前必须先固定化)。
3. **sitemap/robots 预览可达**:dist 已含两文件但 manifest 无对应行 → 预览 404(Task 7 真实发现,记 Plan 5);方案二选一——manifest 补 asset 行或预览发布道(不进 manifest 的直通道),与第 2 项 sitemap 门禁一起定(避免两处 sitemap 语义漂移)。
4. **SEO 元数据补齐**:og:type=article / og:image(SeoHead 扩展——Task 3 审查移交);/category/ 的 NewsCard 扩展(分类归档页新闻卡渲染——Task 4 时真实路由集未消费该形态,分派模式已预留)。origin 空守卫已在 load-render-data 落地(类型防御 + 空串缺省)——此项闭环。
5. **deploy 前置**:836 条 301 的 nginx conf 输出(V2 §14 route compile——route_registry redirect 行含 location,数据已齐);输出时 Location 复用 pipeline 的 URL 线形归一(ADR-018 决策五同款——生产面 nginx 先解码,与预览键归一同理防裸非 ASCII)。
6. **续传**:Task 0.2 cms 数据备份(用户暂缓——**阶段 6 L3 验收前必须补做**,责任链自 Plan 2 清单第 6 项 → Plan 3 清单第 7 项延续)。
7. **度量基线(Plan 5 优化对照)**:全链首跑 ~64s(import 51.1s + build 11.9s)/ spec 复审复跑 ~70s / 幂等复跑 ~15s(register 1.8s + import 复用 94ms + publish 164ms + build 复用读原报告);**import 占大头——优化方向 = 批量锚点解析**(逐事件串行分派的交互式往返口径,phase3-import.md §5 记录在案);astro 纯构建 4.9-7.5s + 媒体复制 836 文件 ~164.5MiB 占 building 相大头(量级自洽对账见 phase4-preview.md §4)。