- ADR-017 导入管道决策五项:幂等双轨身份键(六列快照行+五列内容身份)与 canonical.ts 共享 checksum 输入域/收编 UPDATE 语义;自建 term((domain,nicename) 兜底 + auto: 前缀 + name 回退 + 双序终态等价含 null-id 收编);counts 键全局 命名空间求和聚合;stale 回收维持 created_at 近似 + 24h 不加心跳列(REST 化重审) 与报告 issues 封顶;编排串行分派契约(锚点依赖推导) - README:快速开始加 sf import 全链示例(register→import);当前状态更新 (阶段 3 完成:192 条目真实落库/二导幂等 0 新修订/333 测试);ADR-001~017 - Plan 3 计划文档:Task 8 勾选 + 验收 6 项全过核验 + 文末新增"移交 Plan 4 清单" 8 项(publish 命令/Render Snapshot 六文件/pilot-minimal 六类页面+Astro 五禁/ Preview Server/最小门禁/导入产物消费接口与副本路径/续传项/度量基线 51.8s·20.3s·304MiB) Co-Authored-By: Claude Code <noreply@anthropic.com>
26 KiB
SiteForge 纵向切片 · Plan 3:内容导入管道(阶段 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: 把阶段 2 的 UniversalRow 流落库:稳定身份幂等导入(内容修订 + current 指针)、term 物化(含自建缺失 taxonomy)、媒体登记(魔数/SVG 隔离/uploads 对拍)、route_registry 生成(§6.1 全表处置);终点 = 真实 hesupipe 全量导入成功且二导幂等(0 新修订)。
Architecture: 依据 spec §15 阶段 3、§5 数据模型(Plan 1 已建 21 表)、§6.1 路径处置、§7.5 checksum;消费 Plan 2 产出(runWxrAdapter 四事件流 / registerSource 快照 / 版本块)。新增 apps/api/src/modules/import/(编排服务)+ sf import 命令。导入不发布(published 指针不动——ADR-010)。
Tech Stack: 既有栈零新增依赖(file-type 类魔数检测手写常用签名表,避免新依赖——见 Task 4)。
边界(不做):显式 publish 命令(阶段 4)、构建/主题(阶段 4)、SEO diff(后续)、REST 通道、差量删除检测。
前置事实(执行者必读)
- Plan 2 移交清单 9 项是本计划必读输入(Plan 2 文末);尤其第 1 项(term 表缺 category/post_tag,按 (domain,nicename) 自建)、第 2 项(uploads 路径对拍)、第 5 项(消费接口契约)。
- 表结构已冻结(Plan 1):source_records 六列稳定身份 UNIQUE、content_revisions 不可变触发器、entry_locales 双指针、route_registry exact/collision 双键 + NULLS NOT DISTINCT。
- 既有纪律全套沿用:TDD、临时库测试、身份断言、无包级 test 脚本、typecheck 四段链、cms 只读。
- 导入语义核心(spec §7.5/§8.1):同一 (site, connection, source_type, source_id, locale) 的 row → 已有 source_record 且 normalized_checksum 相同 → skip(0 新修订);不同 → 新修订 + entry_locales.current_revision_id 前移(published 不动)。permalink 变化只影响 route,不影响条目身份。
- wppop/extras 等未知字段已在 UniversalRow.extras——落 revision.data.extras 原样保留。
- normalized checksum 输入域(P0 钉死,评审 Issue 2):
canonicalJson({ universal_row_version, adapter_version, mapping_version, sanitizer_version, locale, mapped_content: {title, slug, **status**, bodyHtml, seo, knownFields, extras}, taxonomy_relations: taxonomyRefs, media_reference_identities: attachmentRefs, route_source_values: {sourceUrl} })——rawLocation 显式排除(位置性字段,进哈希破坏跨快照 skip);版本三常量必入(bump 语义);status 必入(draft↔publish 翻转必须可见)。revision.data 顶层结构钉死:{ title?, slug?, status, bodyHtml, seo, knownFields, extras }。canonicalJson 帮助函数从 register.service.ts 提取到apps/api/src/common/canonical.ts共享导出(register 侧改 import,零行为变更)。注:sourceUrl 在哈希内(route_source_values)→ permalink 变化会产新修订(spec §7.5 一致),只是不建新 entry。 - 媒体映射侧车(P0 钉死,评审 Issue 1——真实数据实证):真实媒体目录为扁平 GUID 命名(2625 文件无 yyyy/mm),与 WXR attachment_url 零精确命中;唯一映射在旧 cms 库 media_assets 表(source_url→file_path)。阶段 3 增加映射侧车输入:Task 0 从 cms PG(5433,只读 SELECT)导出该站 (source_url, file_path) 清单 JSON → 登记为
manual_url_list快照(register 复用既有 kind)。对拍三级:①清单 uploads 相对路径精确命中 → ②侧车 source_url 命中(file_path 再对清单实体)→ ③missing 报告。清单 miss 的 attachment 不插 media_assets 行(sha256 NOT NULL 插不进),只进报告。 - locale 接缝坑(评审提醒):适配器 mapConfig.locale 缺省空串——import 必须传
site.default_locale,否则 1047 行全被拒为未知 locale。Task 6 编排钉死。 - 阶段 3 站点路径来源(评审 Issue 4):主列表路径用 CLI 参数
--products-path /products/ --news-path /news/(默认值即此,记入 job metadata);taxonomy public_base_path 常量钉死(WP 默认):category→/category/、post_tag→/tag/、product_cat→/product-category/、product_tag→/product-tag/(本切片该路径不生效仅记录;spec §19.5 权威路径确认后可调——记入报告备注)。collision_key 折叠函数钉死:NFC + lowercase + 去尾斜杠。 - attachment 页三态降级(评审 Issue 3):阶段 3 无 sitemap 解析器——attachment 页**一律按"仅 WXR→301 媒体文件"**处理(sitemap→html 分支留待 sitemap 解析能力落地,报告注明降级)。
- attachment 行不建 content_entries:wp:attachment 行只进媒体管道(media_assets)+ 路由(301);content_entries 仅 129 post + 48 product + 15 page = 192(Task 7 计数以此为准)。sourceType→typeKey 映射钉死:wp:post→news_article、wp:page→basic_page、wp:product→product。
- unsupported item 也写 source_records(评审 Issue ②选项①):nav_menu_item 等 75 条 → source_records 行(parse_status='unsupported'、locale=站点默认、source_id=post_id、不关联 content_entry)——"不支持对象有明确结果"的可审计性;source_records 总数 = 1122。前置工作项(Task 1 内):@sf/adapter-wp 的 unsupported/error 事件加法式补
postId?: string(纯加法不触发 UniversalRow 契约升版;error 分支 failed>0 时同理按报告计数、不硬造 source_id)。 - import_jobs 与三快照的绑定:job.source_snapshot_id 指向 wxr 快照(主输入);media_bundle 与 manual_url_list 由 CLI 按 (connection, kind) 解析最新并记入 job 报告(job 行不扩列)。Task 0 导出 SQL 加
AND source_url IS NOT NULL(867 中 8 行 NULL 不带出)。
仓库结构增量
apps/api/src/modules/import/
import.service.ts # 编排:事件流→各写入器→报告
term-writer.ts # wp:term 事件 + 自建缺失 term
content-writer.ts # row→entry/locale/revision/source_record(checksum skip)
media-writer.ts # attachment row + media_bundle 对拍→media_assets/usages
route-writer.ts # §6.1 处置表→route_registry
magic-bytes.ts # 常用文件签名检测(png/jpg/gif/webp/pdf/svg 判别 + svg 文本特征)
scripts/sf.ts # + import 命令
tests/integration/import-*.test.ts
Task 0: 媒体映射侧车导出(P0 前置,评审 Issue 1)✅ 完成(a227cfa + 加固 a185254,规格审查通过含 859/859 独立比对;导出 859 行、WXR 命中 836/855=97.8%、未命中 19 全列证据文档;register --media-map → manual_url_list 幂等)
Files: scripts/export-media-map.mjs;docs/evidence/media-map-export.md
- 只读查询 cms PG(5433,infrastructure-postgres-1,只读 SELECT——绝不写):
SELECT source_url, file_path FROM media_assets WHERE site_id = '<54f91c49 站点>' AND source_url IS NOT NULL ORDER BY source_url→ JSON 数组落storage/incoming/media-map-hesupipe.json - 登记为 manual_url_list 快照(
pnpm sf source:register --site … --sitemap?需扩:register 支持--media-map <json>输入→kind=manual_url_list——小改 register.service 输入枚举) - 证据文档:行数、与冒烟 855 attachment 的覆盖率预估(URL 前缀归一后 join 对照)
- commit
feat(source): 媒体映射侧车导出与登记
Task 1: import 命令骨架 + import_jobs 生命周期 ✅ 完成(0e8b2b5 + 修复 eb188da,双审通过;282 测试;审查战果:复用口径忠实化、管道 Outcome 契约定缝(counts/issues + ctx.report 累加器 + succeeded_with_issues 派生)、报告 tmp+rename 先写后落行、stale 警告、真 PG 23505 探针。遗留两行入 Task 2:ImportErrorCode 补 REPORT_WRITE_FAILED、ctx.db 事务边界 JSDoc) ✅ 完成
Files: apps/api/src/modules/import/{import.service.ts,import.module.ts};sf.ts + import;Test tests/integration/import-job.test.ts
(本任务含前置事实 12 的 adapter 事件加法扩展:unsupported/error 事件补 postId?: string)
- 失败测试:
sf import --site <key> --snapshot <id>(或按 connection+kind 选最新快照)→ import_jobs 行 created→running→succeeded(或 succeeded_with_issues/failed);idempotency_key 唯一(重复提交同 key 复用既有 job 不重跑);job 记录 mapping/adapter/sanitizer 版本块(来自 versions.ts) - 实现:job 状态机 + 报告落 artifacts/import//;空流(无 snapshot)→ 明确报错
- 绿 → commit
feat(import): job 骨架与生命周期
Task 2: term 物化(含自建缺失)✅ 完成(191c275 + 修复 8485c51,双审通过;289 测试;审查战果:双序终态等价(收编 UPDATE + name-only 收编,混合序独立探针证实)、同 slug 双 id 报告、handleEvent 分派形状(Task 6 编排就绪)。遗留入 Task 3/6:content_terms 跨快照 is_primary 翻转决策;autoCreatedTerms null-id 注脚;REPORT_WRITE_FAILED 测试)
Files: term-writer.ts;Test tests/integration/import-terms.test.ts
- 失败测试(用 mini.xml 全链 + 手构缺表场景):
- wp:term 事件 → taxonomies(按 term_taxonomy 映射 object_type:category→post/product_cat→product/post_tag→post…)+ terms(source_id=term_id)+ 幂等重跑不重复
- 自建缺失 term(移交项①):row 的 taxonomyRefs 中 (domain, nicename) 无对应 term → 自动建(source_id=
auto:<nicename>、slug=nicename、name=引用 value)+ 报告计数 autoCreatedTerms;同一 nicename 多次引用只建一条(唯一 (site, taxonomy, source_id) 兜底) - content_terms 写入(is_primary:首个为 true 余 false——WP 单主分类语义近似,JSDoc 注明)
- 实现 + 绿 → commit
feat(import): term 物化与缺失自建
Task 3: 内容写入器(稳定身份 + checksum skip)✅ 完成(2b61431 + rider fe72df3,双审通过;296 测试;幂等三态含跨快照五列身份、checksum 三方独立对拍、published 全程未碰;审查沉淀:counts 键全局命名空间契约、unsupported/failed 分项键、NULL-entry 缝归属 Task 6)
Files: content-writer.ts;Test tests/integration/import-content.test.ts
- 失败测试:
- 首导:每 row → source_records + content_entries + entry_locales(current_revision_id 设、published NULL)+ content_revisions(source=import、checksum=normalized checksum、data 含 title/knownFields/extras/清洗后 bodyHtml/seo)
- 二导同快照:0 新修订(checksum skip)、0 新 entry
- 内容变化(改 checksum 手构):新修订 + current 前移 + published 仍指旧值(ADR-010 断言)
- permalink 变化(改 sourceUrl 手构):不建新 entry(身份键不含 URL)+ route 更新走 Task 5;source_records.source_url 更新
- locale 语义:row.locale 必须匹配站点 default_locale 或显式支持列表——未知 locale 记错误跳过(Plan 1 纪律)
- 实现 + 绿 → commit
feat(import): 内容稳定身份写入
Task 4: 媒体登记(魔数/隔离/对拍)✅ 完成(ed081ec + 加固 7e6ce91,双审通过;310 测试;三级对拍含侧车扁平名候选/两阶段 usage 双序一致/SVG quarantine 不建 usage/幂等定向冲突;加固:manifest 形态守卫/侧车门控/约束名精确认领(constraint_name 实证)/窗口边界。Task 7 度量口径已固化计划)
Files: media-writer.ts + magic-bytes.ts;Test tests/integration/import-media.test.ts
- 失败测试:
- attachment row 对拍三级(前置事实 7):①uploads 相对路径清单精确命中 ②侧车 source_url→file_path→清单实体 ③missing 报告(不插 media_assets 行);media_assets(public_path=旧 uploads 路径、sha256/storage_path 从清单取)+ media_usages(正文/featured/gallery 引用从 row.attachmentRefs+清洗后 HTML 提取)
- 悬空引用(mini 203/204):引用无对应 attachment/清单记录 → 报告 issue 计数、不静默丢弃(§11.2 一级/§7.6 禁静默)
- 魔数校验:png/jpg/gif/webp/pdf 签名表手写(~40 行);声明 MIME 与检测不符 → 报告 issue;SVG 检出(文本
<svg特征)→ status=quarantined 不进产物(spec §11.1) - 多候选(同名不同目录)→ 歧义不自动选,全部列报告(spec §5.5)
- 855 attachment 真实规模冒烟(Task 7 一并)
- 实现 + 绿 → commit
feat(import): 媒体登记与安全校验
Task 5: route_registry 生成(§6.1 处置全表)✅ 完成(9d2cc0a + 修复 bb2d381 + docs 15d7c91,双审通过;317 测试;§6.1 阶段 3 可产生的行全覆盖(mini 全链 12 行逐行对拍),?p= 排除钉死"不插行+计数+issue"(route_kind 六值无 excluded——查询路由 SP3),attachment 301 暂存到 finish 反查 media_assets(时序契约),host 单值 = production_origin(旧 host 变体 SP3),upsert 三分(owner = route_kind+target_content_id+target_file),日期/作者归档与人工映射/410 同属无输入源合理空缺;审查修复:finish ②④ 域 isSafePath 统一(反斜杠穿越探针双重验证)、attachment ?p= 排除对齐、WriteOutcome 四态降级计数精化、permalink 形态覆盖)
Files: route-writer.ts;Test tests/integration/import-routes.test.ts
- 失败测试(所有阶段 3 可产生的 §6.1 行均有对应用例——含评审补的 asset 与首页两行,mini 数据手构补充):
- 内容 permalink → html(路径保真含 .html;target_content_id 关联);首页
/→ html;媒体上传路径/wp-content/uploads/...→ asset(media public_path 生成);主列表页/products/、/news/(CLI 参数来源,默认即此)→ html;产品/文章分类 → html(public_base_path 常量按前置事实 9,product_tag 记/product-tag/——WooCommerce 默认,本切片该路径不生效仅记录);tag/product_tag/日期/作者 → redirect 301 主列表;attachment 页 → 一律 301 媒体文件(前置事实 10 降级,报告注明);?p=→ excluded_with_reason 登记。"所有阶段 3 可产生的 §6.1 行"均有用例(人工映射/确认删除两行无输入源,属合理空缺) - exact/collision 双键:
/About与/about两条并存(collision_key 折叠函数按前置事实 9 钉死);同 exact_key 冲突(两内容同路径)→ 报告 conflict 不静默覆盖 - 重导幂等:同路径同目标 → 0 新行;目标变 → 更新(route 表可变,非不可变表)
- 内容 permalink → html(路径保真含 .html;target_content_id 关联);首页
- 实现 + 绿 → commit
feat(import): 路由注册表生成
Task 6: 编排集成 + 导入报告 ✅ 完成(8003de3 + 修复 77687e2 + 终验补完 fa6e8a3,双审通过;325 测试;九项编排义务全落地(真流/串行分派/finish 硬序/counts 求和/NULL-entry 收编/共享基类型/stale 回收/REPORT_WRITE_FAILED 测试/清洗统计);审查战果:失败报告部分冲刷(eventsDispatched+纯 finish 补算)、列表路径建行前校验(INVALID_LIST_PATH)、reason 归档分层。HTML 报告推迟 Task 8;orchestrator.ts 抽取触发器已记)
Files: import.service.ts 补全;Test tests/integration/import-full.test.ts
- 失败测试:mini.xml register→import 全链:site/connection/snapshot→job→terms(2+自建0)→content_entries(3:post/product/page——attachment 不建,前置事实 11)→media(2+对拍)→routes(首页+2列表+3内容+2媒体 asset+2 attachment 301+tags…)→报告(各计数+清洗统计+版本块+冲突/失配清单);published_revision_id 全 NULL(导入不发布断言);mapConfig.locale=site.default_locale 接线断言(前置事实 8)
- Task 3 审查移交的编排义务:① NULL-entry 占位行收编(content-writer 六列分支 UPDATE 挂 entry——精确位置已标注在代码缝注释)② 多写入器 counts 按求和聚合(import.service 现为⊕覆盖——键名全局唯一已 JSDoc 契约化;编排层改或约定各写入器键前缀,二选一定死)③ 写入器 identity 参数共享基类型(Pick<ImportJobContext,…>)防漂移 ④ stale 回收(已在清单)⑤ REPORT_WRITE_FAILED 测试补 ⑥ wp:attachment 双写入方单一 owner 决策(content-writer record-only 足迹 vs media-writer——媒体域归 Task 4,编排广播时 media-writer 消费 attachment 行、content-writer 只留 record 足迹,Task 6 定分派规则)
- Task 5 审查移交的汇流契约(Task 6 实现底本):事件期严格串行 await 分派
content.handleEvent → term → media → route(三家锚点都查 content 同事件产物,禁 Promise.all);finish 期唯一硬序await media.finish()先于route.finish()(attachment 反查与 asset 全表读 media_assets);term/content finish 同步无库依赖任意位置。测试 fullPipeline(import-routes.test.ts)即抄写底本。Task 7 度量备注:routesCollisions 重跑如实再现非缺陷;WP 首页=page 场景的首页冲突 issue 有既定解释;真实规模 ~2815 route 行 ~9300 RT——DB 须同机/近端跑并记录耗时,超预期先加(site_id,host,collision_key)与media_assets(source_id)两索引 - 报告落 artifacts/import//reports/import.json(HTML 版与错误封顶推迟至 Task 8 收尾——Task 6 审查确认;JSON 已含 18 键 counts/issueGroups 六组/清洗统计/版本块)
- stale 回收:超时 running → stale 置位与心跳列决策(ADR-017)
- 绿 → commit
feat(import): 全链编排与导入报告
Task 7: 真实 hesupipe 全量导入(阶段 3 验收)✅ 完成(commit 1c4c163,规格审查通过含独立复跑——21 断言全 PASS:192/1122/927(自建 476 精确)/836 全侧车/2772 逐桶/二导全零/published 0;复验含 836 301 全量 JOIN、192 修订零 script、missing-19 双向零差、cms 只读。首跑 51.8s 口径差异已对账。副本保留供 Plan 4)
Files: scripts/import-real.mjs(或直接 sf import 命令 + 采样);docs/evidence/phase3-import.md
- register 真实 WXR + 媒体目录(
D:/project/cms/storage/media/54f91c49-*只读副本到 storage/incoming——实测约 171MB/2625 文件(评审勘误:非 500MB,勿拷错站点目录),耗时记录)+ Task 0 侧车 → sf import - 验收断言:content_entries=192(129+48+15);media_assets=侧车+清单命中数(报告对照 855 attachment 的三级分布);source_records=1122;terms=451+自建数(对照冒烟 807 引用覆盖);routes 计数与处置分布报告;二导 0 新修订;数据库行数与导入报告逐项一致
- Task 4 审查移交的度量口径:mediaRegistered 只计首跑插入——838 命中数必须来自干净库首跑;missing/quarantined/dangling 重跑如实再现;quarantined/unsupported 资产的引用不落任何 count 桶(非 dangling 非 usage)——"侧车 23 悬空"对拍时勿期望它们在 mediaDanglingRefs 里(只能经 mediaQuarantined/issues 可见)
- 内存/耗时记录;证据文档;commit
test(evidence): 阶段3真实全量导入验收 - 任何计数不符 → 如实报告停下分析(同 Task 8 纪律)
Task 8: 收尾 ✅ 完成(ADR-017 五决策/HTML 报告与 issues 封顶/orchestrator.ts 抽取(import.service 849→754 行,零行为变更)/README 全链示例与阶段 3 状态/移交 Plan 4 清单 8 项;333 测试全绿;两个 commit:feat(import) + docs,不 push——推送由用户执行)
- ADR-017(导入幂等语义:稳定身份键选择/checksum 输入域/自建 term 策略)——
docs/adr/ADR-017-导入管道决策.md五决策:①幂等双轨身份键(六列快照行 + 五列内容身份)/canonical.ts 共享 checksum 输入域/收编 UPDATE 语义 ②自建 term((domain,nicename) 兜底 + auto: 前缀 + name 回退 + 双序终态等价含 null-id 收编)③counts 键全局命名空间 + 求和聚合 ④stale 回收 24h 常量 + 不加心跳列(单进程 CLI 下收益低,REST 化重审)⑤编排串行分派契约(锚点依赖推导);附带报告 issues 封顶(REPORT_ISSUES_CAP=10k)决策入册 - HTML 报告 + 错误封顶(Task 6 推迟项兑现):
report-html.tsrenderImportReportHtml(Plan 2 Task 6 模板纪律——全动态文本转义/静态骨架无 script/分组固定序)+ import.html 落盘(与 JSON 同 try 同失败口径);import.json issues/issueGroups 封顶 10k + issuesTruncated 计数(终态判定用全量长度——截断不吞 succeeded_with_issues 事实);测试 7 单元(恶意 payload 转义/结构/封顶注记)+ 1 集成(10_003 条注入:截断计数/原文保留/HTML 转义) - orchestrator.ts 抽取(审查触发器:import.service 849 行):orchestratedPipeline + mergeOutcomes + sumCounts + trySyncFinish 迁至
apps/api/src/modules/import/orchestrator.ts(纯函数 over ctx;契约类型单源 import.service.ts 不动——写入器同款 type-only 反向引用);既有测试全绿即证零行为变更 - README 状态更新:快速开始加
pnpm sf import全链示例(register→import);当前状态更新(阶段 3 完成:192 条目真实落库/二导幂等 0 新修订/333 测试);ADR-001~017;下一步阶段 4 指向文末移交清单 - 阶段 4 移交清单(见文末新节定稿——含 publish 消费接口、Render Snapshot 六文件、pilot-minimal 六类页面、Preview、最小门禁、度量基线 8 项)
- typecheck 四段链/lint/test 三绿(333 测试);commit 两个(feat(import) + docs);push 由用户执行
验收(对照 spec §15 阶段 3)✅ 全过(Task 7/8 收官核验)
- 同 Snapshot 二次导入 0 新 Revision(Task 7 独立复跑 21 断言全 PASS:revisionsCreated=0;集成测试 import-full/import-content 钉死)
- Permalink 变化不创建重复 Content Entry(五列内容身份与 URL 解耦——import-content 用例 + 真实 836 301 反查)
- 媒体歧义不自动匹配(多候选全列报告)(mediaAmbiguous 计数 + 逐条 issue)
- URL 冲突和不支持对象全部进入报告(routesCollisions/issueGroups 六组 + unsupported → source_records parse_status='unsupported' 可审计)
- 真实全量导入成功 + 幂等重跑 + 报告对照(192/1122/927(自建 476)/836/2772 逐桶对拍,见
docs/evidence/phase3-import.md) - 三绿;published 指针全程未动(333 测试;published_revision_id 全库 0 非 NULL——Task 6/7 双重断言)
移交 Plan 4 清单(Plan 3 收官定稿——阶段 4:content:publish + Render Snapshot + pilot-minimal 主题 + Astro 构建 + 本地 Preview,计划的必读输入)
- content:publish 命令(V2 spec §8):dry-run/confirm(preview-token)/批量/审计动作形状——
published_revision_id = current_revision_id单语句语义、不得 UPDATE content_revisions、拒绝未解决 P0 issue、拒绝跨 site、重复发布幂等。published 指针此刻才开始动(Plan 3 全程未碰——ADR-010;查询起点:entry_locales where current_revision_id is not null and published_revision_id is null)。site_config_revisions 首版生成时机建议:随首次 publish 物化(site 行快照进 Render Snapshot 的 site.json 来源),本清单第 2 项消费。 - Render Snapshot 生成(V2 §9.2 六冻结文件):从 published 修订 + routes + media + site 配置物化
site.json / content.json / taxonomies.json / routes.json / assets.json / render-manifest.json(不可变派生产物,hash 锚定——§10.2 冻结输入)。 - pilot-minimal 主题 + Astro 构建管线:六类页面按迁移快线第 8.1 节顺序(首页 → 普通页 → 产品列表 → 产品详情 → 新闻列表 → 新闻详情);ThemeManifest 最小契约(V2 §9.1);构建只读快照——禁查 PG/禁 Control API/禁读 storage/ 原始输入/禁站点 seed/禁构建期网络(V2 §9.2 五禁)。
- Preview Server(V2 §12):route_manifest 驱动(§12.1 字段面);只绑 127.0.0.1、只读指定 build 的 Dist+Manifest;301/410/404 按 Manifest 声明;全响应
X-Robots-Tag: noindex;路径穿越/目录列表防御。 - 最小门禁(V2 §13 子集):死链(内链命中 route manifest 全量非抽样)/sitemap 双向对照/title-canonical-H1 四恰一/大小写冲突(lowercase collision)。
- 导入产物消费接口:published 修订集合(entry_locales.published_revision_id → content_revisions.data)、routes(route_registry 全表)、media_assets ready 集合(status='ready'——quarantined/unsupported 排除语义:其路由行是旧 URL 空间台账,构建/产物侧不复制文件);
storage/incoming/副本路径保留(WXR 副本 + 媒体副本 2625 文件/164.53 MiB + 侧车,含 storage/snapshots content-addressed 双份共约 345MB——见docs/evidence/phase3-import.md§6)供构建期媒体复制。 - 续传携带:Task 0.2 cms 数据备份(用户暂缓——阶段 6 L3 验收前必须补做,Plan 2 清单第 6 项责任链延续);836 条 attachment 301 的 nginx conf 输出(V2 §14 route compile)——Plan 4 或 Plan 5 定,数据已齐(route_registry redirect 行含 location)。
- 度量基线(Plan 4 构建耗时对照):导入首跑 51.8s / 二导 20.3s(逐事件串行分派的交互式往返口径——提速方向批量锚点解析,见 phase3-import.md §5);内存峰值 304.3 MiB(进程集上界口径);登记 media_bundle 19.0s(2625 文件哈希+副本)。