docs(plan): Plan 3 实施计划——阶段3 内容导入管道(8任务)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -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 <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/<jobId>/;空流(无 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:<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)
|
||||
|
||||
**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/<yyyy>/<mm>/<file>` 相对路径在清单中**精确命中**(移交项② uploads 约定)→ media_assets(public_path=旧路径、sha256 从清单取)+ media_usages(正文/featured/gallery 引用关系从 row.attachmentRefs+清洗后 HTML 提取)
|
||||
- **魔数校验**:png/jpg/gif/webp/pdf 签名表手写(~40 行);声明 MIME 与检测不符 → 报告 issue;**SVG 检出(文本 `<svg` 特征)→ status=quarantined 不进产物**(spec §11.1)
|
||||
- **失配三级**(Plan 1 复审口径):清单无此文件 → missing + 报告;URL 命中但清单 sha 与实体不符 → 报告 issue;多候选(同名不同目录)→ **歧义不自动选**,全部列报告(spec §5.5 匹配顺序 4 的纪律)
|
||||
- 855 attachment 真实规模冒烟(Task 7 一并)
|
||||
- [ ] 实现 + 绿 → commit `feat(import): 媒体登记与安全校验`
|
||||
|
||||
### Task 5: route_registry 生成(§6.1 处置全表)
|
||||
|
||||
**Files:** `route-writer.ts`;Test `tests/integration/import-routes.test.ts`
|
||||
|
||||
- [ ] 失败测试(spec §6.1 每行一用例,mini 数据手构补充):
|
||||
- 内容 permalink → html(路径保真含 .html;target_content_id 关联);主列表页 `/products/`、`/news/`(站点配置声明)→ html;产品/文章分类 → html + 分页占位规则(page 1 即列表本体);tag/product_tag/日期/作者 → redirect 301 主列表;attachment 页 → 默认规则三态(sitemap 有→html/仅 WXR→301 媒体文件/确认删→410);`?p=` → excluded_with_reason 登记
|
||||
- **exact/collision 双键**:`/About` 与 `/about` 两条并存;同 exact_key 冲突(两内容同路径)→ 报告 conflict 不静默覆盖
|
||||
- **重导幂等**:同路径同目标 → 0 新行;目标变 → 更新(route 表可变,非不可变表)
|
||||
- [ ] 实现 + 绿 → commit `feat(import): 路由注册表生成`
|
||||
|
||||
### Task 6: 编排集成 + 导入报告
|
||||
|
||||
**Files:** `import.service.ts` 补全;Test `tests/integration/import-full.test.ts`
|
||||
|
||||
- [ ] 失败测试:mini.xml register→import 全链:site/connection/snapshot→job→terms(2+自建0)→entries(5)→media(2+对拍)→routes(首页+2列表+5内容+2分类+2 attachment+tags…)→报告(各计数+版本块+冲突/失配清单);**published_revision_id 全 NULL**(导入不发布断言)
|
||||
- [ ] 报告落 artifacts/import/<jobId>/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 指针全程未动
|
||||
Reference in New Issue
Block a user