Files
cms-website/docs/superpowers/plans/2026-10-21-slice-build-preview-phase4.md
T
浅念 66ca95d37f docs(plan): P4 Task 7 勾选
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 13:59:23 +08:00

21 KiB
Raw Blame History

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。
  7. 路径形态事实(评审实测):真实 hesupipe 有 54 条百分号编码中文 permalink(如 /%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac/,多为 page/news)且 0 条 .html permalink(该规则仅夹具用)。编码路径贯穿规则钉死:routes.json→dist 文件路径→route-manifest→preview 匹配全程编码原样(字节精确),V1 不做解码归一(Windows/NTFS '%' 合法;解码归一属 SP3 验证器)。.html 规则保留但标注"仅夹具防御"。
  8. 发布补充钉死:① --actor 参数(CLI 必传或缺省 super_admin@cli,audit_logs.actor_label NOT NULL——本计划是全仓第一个审计写入方);② P0 门禁决策:V1 publish 不读报告文件做 P0 阻断(ADR-018 记偏离——import issue 无 DB 查询源,206 条真实现状下严格门禁会反堵发布;missing 媒体等已在 build/gate 层有真实防线),Plan 5 SEO diff 时重审。

仓库结构增量

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 命令(显式发布)✅ 完成(c35f108 + rider 6a1d9a3,双审通过;340 测试;dry-run/confirm 单事务/审计首写入方/config v1/anchors helper 三处合并逐字保真。审查移交:brand 容忍契约已入 Task 3;audit 第二写入方时抽共享 helper)

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, actor})——查询起点 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=随机;actor 按 --actor 传入、缺省 super_admin@cli(前置事实 8);P0 门禁按前置事实 8 决策不做(ADR-018 记偏离);五列锚点共享 helper 顺手抽 apps/api/src/modules/import/anchors.ts(三写入器改 import——零行为变更由既有测试守护)
  • 绿 → commit feat(publish): 显式发布命令(dry-run/confirm/审计/站点配置首版)

Task 2: Render Snapshot 物化 + build job ✅ 完成(8f42c83 + 修复 5434afe,双审通过;354 测试;审查战果:哈希链===文件字节互证、media 块 first-wins 确定性、job 复用只认 succeeded+#a 谱系崩溃回收、repeatable read 隔离探针实证。遗留 Task 5/6:stale 宣卫/幂等键事务外窗口/existsSync 出事务)

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→validating(空)→succeeded——dist 尚空;Task 6 接真实构建时新建 build job而非改行——build_input_snapshots 有不可变触发器)+ build_input_snapshots 行(三哈希/site_config_revision_id/theme_key='pilot-minimal');② 六文件物化:site.json(config v1)/content.json(published 修订——bodyHtml URL 重写钉死匹配键:URL 剥 host 后按 path 与 media ready 集合的 public_path/source_url 路径段比对(覆盖绝对/根相对/协议相对变体),命中→根相对 public_path;未命中保留原样+issue)/taxonomies.json(term+taxonomy)/routes.json(html+asset 行,redirect 并入)/assets.json(ready:public_path→storage 实体路径+sha256)/render-manifest.json(版本块+总数+源哈希);③ canonical JSON 确定性(键排序);④ 未 publish 站点 → NOT_PUBLISHED;⑤ ADR-010 双修订测试:发布 A→导入改内容 current=B→快照 content.json 仍为 A(published 冻结语义,spec §15 阶段 4 验收明文)
  • 实现:一致性读取(单事务 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 主题骨架 + 布局组件 ✅ 完成(1cd309d + 修复 eb81a57,双审通过;372 测试;审查战果:payload 契约上提 contracts 双端咬合、空路由明错(静默 0 页消除)、eslint astro ignore、routeHrefFor 预备。遗留:og:type/og:image、footer 年份可复现、origin 空守卫——均记 Plan 5/Task 5)

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 视觉优先级:宽度/字体/颜色/间距先对,不动像素级还原);brand 恒可能为 {} 且 name 可能是 siteKey 派生占位(Task 1 审查移交)——主题必须对空 brand/占位 name 缺省容忍,禁止直接读 brand.xxx 落空;组件:RichText(set:html——输入已清洗+重写,信任链 JSDoc 记录,不二次清洗)/ResponsiveMedia(alt/尺寸/懒加载)/Breadcrumbs/ProductCard/NewsCard/Pagination/NotFound
  • 绿 → commit feat(theme): pilot-minimal 骨架与布局组件

Task 4: 六类页面路由与渲染 ✅ 完成(f916f72 + rider 0a7578b,双审通过;380 测试;审查战果:sf-literal-html-paths 折平集成(Astro 5.18 三态均无法产字面文件的实证解法)、真实数据字段探测三纠偏(title@knownFields/media键=fieldPath 全路径/产品无结构化参数表)、stub 清理+format 显式契约、195 页规模实测 5.3s/113MB。Task 8 分派观察项已记)

Files: themes/pilot-minimal/src/pages/[...slug].astro + 模板分派;Test themes/pilot-minimal/test/render-cases.test.ts(Astro 构建级测试:六类夹具数据 → 构建产物断言)

  • 失败测试(六夹具页 = 真实 hesupipe 抽样数据的脱敏化形状——home(/)、generic page(/products/ 内容页 + 一条编码中文路径页如 /%e5%85%b3%e4%ba%8e%e6%88%91%e4%bb%ac/)、product-category 列表、product 详情、news 列表 /news/、news 详情 .html(仅夹具防御——真实 0 条)):构建产物存在且含关键内容(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。分页:V1 一律不做(/news/ 129 条单页全列 + taxonomy 列表同——分页路由不在 routes.json 会自破门禁①死链;触发条件与路由登记义务一并记 ADR-018 移交 Plan 5)
  • 绿 → commit feat(theme): 六类页面渲染与路由分派

Task 5: 构建管线(astro 构建 + 媒体落盘 + sitemap/robots)✅ 完成(1ed3ac0 + 修复 41829e2 + nit 06ba069,双审通过;385 测试;审查战果:Windows taskkill 树杀(孤儿 astro 实证复现后封堵)、错误码分阶段包裹+PIPELINE_INTERNAL、dist 双位置纪律、五禁含无网 stub(telemetry 实联网发现)、/products/ 主列表分派真修。审查移交 Task 6/8 三项已记)

Files: build.service.ts 扩展(spawn astro build→媒体复制→sitemap→产物登记);Test tests/integration/build-pipeline.test.ts

  • 失败测试(小夹具全链):① spawn pnpm --filter @sf/theme-pilot-minimal build(SF_BUILD_INPUT 指输入目录;Task 3 备忘:包全名 filter;Windows astro 构建失败判据="非零退出+stderr 标记"勿匹配精确码——127+libuv 噪音)→ dist/ 页面按 routes.json 的 html 行路径落盘——映射规则钉死:尾斜杠→<path>index.html;.html 后缀→字面文件(仅夹具);百分号编码路径→编码原样目录结构(Astro 产物若不符则构建后按 manifest 重命名/移动对齐——manifest 的 target_file 是唯一权威);② 媒体复制:assets.json 逐条复制到 dist 的 public_path(跳过 quarantined);sha256 抽检;③ sitemap.xml(origin+indexable html 行,编码 URL 原样进 sitemap)+ robots.txt(Allow / + Sitemap 行);④ route-manifest.json 生成与字段面断言(spec §12.1:manifest_version/site_id/build_job_id/origin/generated_from_route_snapshot_hash/routes[] 含 exact_key/query_policy/kind/target_file/status/location/content_type/indexable——含 redirect 行与 ?attachment_id= 查询形态)+ build_artifacts 行(dist/manifest 各 kind+哈希);⑤ 构建环境五禁断言:SF_BUILD_INPUT 之外零数据源(loader 单一入口 grep + DATABASE_URL 置空跑构建仍成功 + 无网证明:构建进程内 stub global fetch 抛错仍成功——spec §10.4 明文要求)
  • 实现:build.service 编排(快照→astro→媒体→sitemap→登记);产物落 artifacts/jobs/<buildJobId>/{input,dist,manifests}
  • 绿 → commit feat(build): astro 构建管线与媒体落盘

Task 6: 最小门禁 + 完整 build 状态机 ✅ 完成(8f2b982 + 快修 fa27a3a,双审通过;393 测试;五门禁 htmlparser2 零新依赖/P0 阻断/issues 落库/报告 HTML 先写两域对齐/spawn 缩减 97→76s。审查战果:非文件路由兜底语义、1101 条分批插入防回归、注入面选侧(dist 侧走真实解析面)。遗留入 Task 9/Plan 5)

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 报告;HTML 解析用 parser 复用清洗管线,禁纯正则 spec §13.4);build 状态机补 validating→succeeded/failed;报告落盘 HTML 先写(Plan 3 窄窗口顺手修)
  • Task 5 审查移交:① 测试套件 spawn 缩减——build-snapshot 7 处成功路径改 materializeRenderSnapshot 直调或复用单次 job(spawn 降到 1-2,五禁覆盖留 build-pipeline);Task 8 开工前完成 ② CLI 与测试禁止同时构建(themeDistDir 结构性共享暂存)③ Task 8 时间预算:全链分钟级(astro 5.3s+媒体 IO+门禁+1746 回环+Playwright),300s spawn 超时余量充足
  • 绿 → commit feat(build): 最小门禁与状态机闭环

Task 7: Preview Server ✅ 完成(7194d1b + 修复 5f78dde,双审通过;406 测试;审查战果:穿越预检先于 URL 解析(实测 new URL 规范化洞)、realpath+junction 逃逸双检、流式文件服务(64MB RSS 平坦实测)、quiet 开关、匹配优先序微探针。sitemap/robots 预览 404 记 Plan 5)

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;⑦ 路径穿越拒绝(../ 与编码形式);⑧ gone 行 410(夹具手构——真实数据暂无)+ 目录列表禁用 + HTML 响应基础 CSP 头(self-only)
  • 实现: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 + 桌面 1440px 截图无明显破版(Playwright MCP 各 3 页存证据——快线 §8.7 原文双端);⑦ 全量 URL 冒烟:route-manifest html 行全 200(190 页)+ redirect 全量 1746 逐条 301+Location 校验(回环零成本)
  • 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 副本保留

Task 9: 收尾

  • ADR-018(Astro 5 选型与许可证/渲染信任链——bodyHtml 不二次清洗决策/分页 V1 不做及路由登记义务移交/P0 发布门禁偏离/编码路径贯穿规则/预览零依赖);README(preview 截图/命令链 publish→build→preview);typecheck 链含 themes/pilot-minimal 与 preview-server(如 TS);lint;三绿全量
  • 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 语义
  • 移交 Plan 5 清单:SEO 三方 diff(source_rendered 抓取需求)/完整门禁(redirect 环/OG/hreflang)/可复现双构建/部署前置(836 nginx conf 输出);续传 Task 0.2 备份
  • commit + push 由用户执行

组件清单省略注记:快线 §8.2 的 ProductGrid/NewsList 由模板内联网格实现、TaxonomyList V1 无消费场景(首页不做分类导航)——避免与 §8.2 对照误判遗漏。

验收(对照快线 §8.7 + V2 §15 阶段 4 子集)

  • 显式发布可用且导入产物发布前不可见(published 指针语义)
  • Render Snapshot 冻结六文件 + 构建五禁(无库无网无 seed)
  • 六类页面真实数据渲染 + 图片本地化
  • Preview 200/301/404/noindex/回环
  • 最小门禁五项全绿
  • 真实 hesupipe 端到端验收 + 移动端截图证据