docs(plan): Plan 3 修复评审两 P0+四项——媒体映射侧车(真实数据实证扁平目录零命中)、checksum 输入域钉死(排 rawLocation)、attachment 降级 301、站点路径来源、悬空引用用例、§6.1 全行覆盖、locale 接缝、计数勘误

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 14:10:36 +08:00
parent 664aade131
commit d392370fda
@@ -19,6 +19,12 @@
3. 既有纪律全套沿用:TDD、临时库测试、身份断言、无包级 test 脚本、typecheck 四段链、cms 只读。 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,不影响条目身份。 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 原样保留。 5. wppop/extras 等未知字段已在 UniversalRow.extras——落 revision.data.extras 原样保留。
6. **normalized checksum 输入域(P0 钉死,评审 Issue 2)**:`canonicalJson({ universal_row_version, adapter_version, mapping_version, sanitizer_version, locale, mapped_content: {title, slug, bodyHtml, seo, knownFields, extras}, taxonomy_relations: taxonomyRefs, media_reference_identities: attachmentRefs, route_source_values: {sourceUrl} })`——**rawLocation 显式排除**(位置性字段,进哈希破坏跨快照 skip);版本三常量必入(bump 语义)。canonicalJson 帮助函数从 register.service.ts 提取到 `apps/api/src/common/canonical.ts` 共享导出(register 侧改 import,零行为变更)。
7. **媒体映射侧车(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 插不进),只进报告。
8. **locale 接缝坑(评审提醒)**:适配器 mapConfig.locale 缺省空串——import 必须传 `site.default_locale`,否则 1047 行全被拒为未知 locale。Task 6 编排钉死。
9. **阶段 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 + 去尾斜杠。
10. **attachment 页三态降级(评审 Issue 3)**:阶段 3 无 sitemap 解析器——attachment 页**一律按"仅 WXR→301 媒体文件"**处理(sitemap→html 分支留待 sitemap 解析能力落地,报告注明降级)。
11. **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。
## 仓库结构增量 ## 仓库结构增量
@@ -36,6 +42,15 @@ tests/integration/import-*.test.ts
--- ---
### Task 0: 媒体映射侧车导出(P0 前置,评审 Issue 1)
**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 站点>' 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 生命周期 ### 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` **Files:** `apps/api/src/modules/import/{import.service.ts,import.module.ts}`;sf.ts + `import`;Test `tests/integration/import-job.test.ts`
@@ -71,9 +86,10 @@ tests/integration/import-*.test.ts
**Files:** `media-writer.ts` + `magic-bytes.ts`;Test `tests/integration/import-media.test.ts` **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 提取) - 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) - **魔数校验**: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 的纪律) - 多候选(同名不同目录)→ **歧义不自动选**,全部列报告(spec §5.5)
- 855 attachment 真实规模冒烟(Task 7 一并) - 855 attachment 真实规模冒烟(Task 7 一并)
- [ ] 实现 + 绿 → commit `feat(import): 媒体登记与安全校验` - [ ] 实现 + 绿 → commit `feat(import): 媒体登记与安全校验`
@@ -81,9 +97,9 @@ tests/integration/import-*.test.ts
**Files:** `route-writer.ts`;Test `tests/integration/import-routes.test.ts` **Files:** `route-writer.ts`;Test `tests/integration/import-routes.test.ts`
- [ ] 失败测试(spec §6.1 每行一用例,mini 数据手构补充): - [ ] 失败测试(spec §6.1 全部行有对应用例——含评审补的 asset 与首页两行,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 登记 - 内容 permalink → html(路径保真含 .html;target_content_id 关联);**首页 `/` → html**;**媒体上传路径 `/wp-content/uploads/...` → asset**(media public_path 生成);主列表页 `/products/`、`/news/`(**CLI 参数来源**,默认即此)→ html;产品/文章分类 → html(public_base_path 常量按前置事实 9);tag/product_tag/日期/作者 → redirect 301 主列表;attachment 页 → **一律 301 媒体文件**(前置事实 10 降级,报告注明);`?p=` → excluded_with_reason 登记
- **exact/collision 双键**:`/About` 与 `/about` 两条并存;同 exact_key 冲突(两内容同路径)→ 报告 conflict 不静默覆盖 - **exact/collision 双键**:`/About` 与 `/about` 两条并存(collision_key 折叠函数按前置事实 9 钉死);同 exact_key 冲突(两内容同路径)→ 报告 conflict 不静默覆盖
- **重导幂等**:同路径同目标 → 0 新行;目标变 → 更新(route 表可变,非不可变表) - **重导幂等**:同路径同目标 → 0 新行;目标变 → 更新(route 表可变,非不可变表)
- [ ] 实现 + 绿 → commit `feat(import): 路由注册表生成` - [ ] 实现 + 绿 → commit `feat(import): 路由注册表生成`
@@ -91,16 +107,16 @@ tests/integration/import-*.test.ts
**Files:** `import.service.ts` 补全;Test `tests/integration/import-full.test.ts` **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**(导入不发布断言) - [ ] 失败测试: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)
- [ ] 报告落 artifacts/import/<jobId>/reports/import.json+html(复用 Task 6 模板纪律:转义/封顶) - [ ] 报告落 artifacts/import/<jobId>/reports/import.json+html(沿用 Plan 2 Task 6 报告纪律:动态文本转义/错误封顶/清洗统计入报告)
- [ ] 绿 → commit `feat(import): 全链编排与导入报告` - [ ] 绿 → commit `feat(import): 全链编排与导入报告`
### Task 7: 真实 hesupipe 全量导入(阶段 3 验收) ### Task 7: 真实 hesupipe 全量导入(阶段 3 验收)
**Files:** `scripts/import-real.mjs`(或直接 sf import 命令 + 采样);`docs/evidence/phase3-import.md` **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 - [ ] register 真实 WXR + 媒体目录(`D:/project/cms/storage/media/54f91c49-*` 只读副本到 storage/incoming——**实测约 171MB/2625 文件**(评审勘误:非 500MB,勿拷错站点目录),耗时记录)+ **Task 0 侧车** → sf import
- [ ] 验收断言:entries=1047(855 attachment + 129+48+15);terms=451+自建数(报告对照冒烟证据的 807 引用覆盖);routes 计数与处置分布报告;**二导 0 新修订**;数据库行数与导入报告逐项一致 - [ ] 验收断言:content_entries=**192**(129+48+15);media_assets=侧车+清单命中数(报告对照 855 attachment 的三级分布);source_records=1122;terms=451+自建数(对照冒烟 807 引用覆盖);routes 计数与处置分布报告;**二导 0 新修订**;数据库行数与导入报告逐项一致
- [ ] 内存/耗时记录;证据文档;commit `test(evidence): 阶段3真实全量导入验收` - [ ] 内存/耗时记录;证据文档;commit `test(evidence): 阶段3真实全量导入验收`
- [ ] 任何计数不符 → 如实报告停下分析(同 Task 8 纪律) - [ ] 任何计数不符 → 如实报告停下分析(同 Task 8 纪律)