diff --git a/docs/superpowers/plans/2026-10-21-slice-import-pipeline-phase3.md b/docs/superpowers/plans/2026-10-21-slice-import-pipeline-phase3.md new file mode 100644 index 0000000..f68e66d --- /dev/null +++ b/docs/superpowers/plans/2026-10-21-slice-import-pipeline-phase3.md @@ -0,0 +1,119 @@ +# 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 通道、差量删除检测。 + +--- + +## 前置事实(执行者必读) + +1. **Plan 2 移交清单 9 项**是本计划必读输入(Plan 2 文末);尤其第 1 项(term 表缺 category/post_tag,按 (domain,nicename) 自建)、第 2 项(uploads 路径对拍)、第 5 项(消费接口契约)。 +2. 表结构已冻结(Plan 1):source_records 六列稳定身份 UNIQUE、content_revisions 不可变触发器、entry_locales 双指针、route_registry exact/collision 双键 + NULLS NOT DISTINCT。 +3. 既有纪律全套沿用:TDD、临时库测试、身份断言、无包级 test 脚本、typecheck 四段链、cms 只读。 +4. **导入语义核心**(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,不影响条目身份。 +5. wppop/extras 等未知字段已在 UniversalRow.extras——落 revision.data.extras 原样保留。 + +## 仓库结构增量 + +```text +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 1: import 命令骨架 + import_jobs 生命周期 + +**Files:** `apps/api/src/modules/import/{import.service.ts,import.module.ts}`;sf.ts + `import`;Test `tests/integration/import-job.test.ts` + +- [ ] 失败测试:`sf import --site --snapshot `(或按 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 物化(含自建缺失) + +**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:`、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) + +**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: 媒体登记(魔数/隔离/对拍) + +**Files:** `media-writer.ts` + `magic-bytes.ts`;Test `tests/integration/import-media.test.ts` + +- [ ] 失败测试: + - attachment row + media_bundle 清单对拍:attachment_url 的 `/wp-content/uploads///` 相对路径在清单中**精确命中**(移交项② uploads 约定)→ media_assets(public_path=旧路径、sha256 从清单取)+ media_usages(正文/featured/gallery 引用关系从 row.attachmentRefs+清洗后 HTML 提取) + - **魔数校验**:png/jpg/gif/webp/pdf 签名表手写(~40 行);声明 MIME 与检测不符 → 报告 issue;**SVG 检出(文本 `/reports/import.json+html(复用 Task 6 模板纪律:转义/封顶) +- [ ] 绿 → commit `feat(import): 全链编排与导入报告` + +### Task 7: 真实 hesupipe 全量导入(阶段 3 验收) + +**Files:** `scripts/import-real.mjs`(或直接 sf import 命令 + 采样);`docs/evidence/phase3-import.md` + +- [ ] register 真实 WXR + 媒体目录(`D:/project/cms/storage/media/54f91c49-*` 只读副本到 storage/incoming——**先 robocopy 约 500MB,耗时记录**)→ sf import +- [ ] 验收断言:entries=1047(855 attachment + 129+48+15);terms=451+自建数(报告对照冒烟证据的 807 引用覆盖);routes 计数与处置分布报告;**二导 0 新修订**;数据库行数与导入报告逐项一致 +- [ ] 内存/耗时记录;证据文档;commit `test(evidence): 阶段3真实全量导入验收` +- [ ] 任何计数不符 → 如实报告停下分析(同 Task 8 纪律) + +### Task 8: 收尾 + +- [ ] ADR-017(导入幂等语义:稳定身份键选择/checksum 输入域/自建 term 策略);README 状态更新;typecheck 四段链/lint/test 三绿;阶段 4 移交清单(publish 命令消费的接口:current 指针集合/审计动作形状;site_config_revisions 首版生成时机建议) +- [ ] commit + push 由用户执行 + +## 验收(对照 spec §15 阶段 3) + +- [ ] 同 Snapshot 二次导入 0 新 Revision +- [ ] Permalink 变化不创建重复 Content Entry +- [ ] 媒体歧义不自动匹配(多候选全列报告) +- [ ] URL 冲突和不支持对象全部进入报告 +- [ ] 真实全量导入成功 + 幂等重跑 + 报告对照 +- [ ] 三绿;published 指针全程未动