docs: ADR-017/README/阶段4移交清单
- 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>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# ADR-017:导入管道决策(幂等语义/自建 term/counts 聚合/stale 回收/编排分派)
|
||||
|
||||
- **状态**:已采纳(Plan 3 Task 8 收尾,阶段 3;实现锚点 `apps/api/src/modules/import/`——import.service.ts / content-writer.ts / term-writer.ts / media-writer.ts / route-writer.ts / orchestrator.ts)
|
||||
|
||||
## 决策一:幂等语义(spec §7.5,承接 ADR-006)
|
||||
|
||||
1. **稳定身份键双轨**:
|
||||
- **快照行(六列)**:source_records 以 (site, connection, source_type, source_id, locale, **snapshot_id**) 唯一(`source_records_identity_uq`)——每快照各一行,是 normalized_checksum 的对比对象;
|
||||
- **内容身份(五列)**:六列去掉 snapshot_id——跨快照复用同一 content_entry(permalink 变化、快照重导都**不建新 entry**,只在新快照行上前移 entry 指针)。两键由 content-writer 单写入方持有,五列解析口径与 term-writer.resolveContentEntryId 同源(latest first)。
|
||||
2. **checksum 输入域**:`canonicalJson`(`apps/api/src/common/canonical.ts`——与 register 的 media_bundle 清单哈希**共享同一实现**,单一序列化基形防两处漂移)作用于 `{ universal_row_version, adapter_version, mapping_version, sanitizer_version, locale, mapped_content: {title, slug, status, bodyHtml, seo, knownFields, extras}, taxonomy_relations, media_reference_identities, route_source_values: {sourceUrl} }` 再 sha256。**rawLocation 显式排除**(位置性字段,入哈希破坏跨快照 skip);三语义版本必入(bump 即新修订);status 必入(draft↔publish 翻转必须可见);sourceUrl 入(permalink 变化产新修订——spec §7.5 一致,但不建新 entry)。
|
||||
3. **收编 UPDATE 语义**:六列身份命中的既有行不重插——checksum 相同 → skip(0 新修订,二导幂等的根基);不同 → 新修订 + entry_locales.current 前移;**published 永不碰**(ADR-010:导入不发布)。外源 NULL-entry 占位行(六列命中、content_entry_id 为 NULL)走 UPDATE 挂 entry——不删不插,避免撞唯一约束。UPDATE 只落在可变表(source_records / entry_locales.current);content_revisions 纯 INSERT(不可变触发器兼容)。
|
||||
|
||||
## 决策二:自建 term 策略(Plan 2 移交项①兑现,真实数据实证)
|
||||
|
||||
4. **(domain, nicename) 兜底自建**:真实 WXR 的 wp:term 表可整域缺席(hesupipe:category 148 引用 / post_tag 659 引用零条目——WordPress 7.1.3 导出行为)。row 的 taxonomyRefs miss 即自建 term:source_id = **`auto:<nicename>` 前缀**(与 wp:term 的 term_id 字符串源身份空间隔离)、slug = nicename、**name 回退 = nicename**(UniversalRow.taxonomyRefs 契约不携带显示名 value——契约冻结,显示名回填须升版评审)。同一 nicename 多次引用只建一条(唯一 (site, taxonomy, source_id) 兜底)。
|
||||
5. **双序终态等价(含 null-id 收编)**:WXR 文档序 items 先于 wp:term——row 先到则按 nicename 自建,对应 wp:term 事件后到时**收编**(就地 UPDATE source_id/name 扶正为权威值);反向序(wp:term 先到)直建权威行。**null-id wp:term** 照建行(source_id=`auto:<slug>`)并记 issue;同 slug 行已见时只做 **name-only 收编**(UPDATE name,source_id 不动——无权威值可扶正)。同 slug 双 term_id → 文档序 first-wins + issue(不静默)。两种事件顺序终态一致(混合序独立探针测试钉死)。
|
||||
|
||||
## 决策三:counts 键全局命名空间 + 求和聚合(Task 6 钉死)
|
||||
|
||||
6. **counts 是跨写入器的扁平全局命名空间**:汇合(ctx.report 累加器 ⊕ 管道返回值、四家 finish 产出)按**同名键求和**聚合(`orchestrator.sumCounts`/`mergeOutcomes`)——Task 1 的同名覆盖(CLOBBER)语义废弃。四家键前缀互斥(term:termsProcessed/autoCreatedTerms/contentTermsCreated;content:contentEntries/revisionsCreated/sourceRecords/skipped/…;media 六键 media 前缀;route 五键 routes 前缀;orchestrator 仅失败路径占 eventsDispatched),求和是防未来键名撞车的安全网;各写入器在自身 JSDoc 声明键归属,新写入器勿复用他家键名。
|
||||
|
||||
## 决策四:stale 回收与心跳列
|
||||
|
||||
7. **维持 created_at 近似 + 24h 回收,不加心跳列**:每次建 job 前把同 site 超过 `STALE_AFTER_MS = 24h` 仍 queued/running 的 job 置 stale(崩溃/中断残留终态化,finished_at 记宣判时刻)。评估一句话:单进程 CLI 模式下 job 就地推进、24h 远超任何真实执行时长,心跳列收益低不值当扩列;**REST 化(多进程/远程执行)时重审**。
|
||||
8. **报告 issues 封顶**(Task 8 兑现 Task 6 推迟项,对齐 Plan 2 ROW_ERRORS_CAP 纪律):import.json 的 flat issues 与 issueGroups 各组按 `REPORT_ISSUES_CAP = 10_000` 截断、`issuesTruncated` 记 flat 溢出(分组是同一集合的分划视图,各组溢出 ≤ flat 溢出不单独入账);counts/sanitize 统计始终完整;终态判定(succeeded_with_issues)在**全量**长度上做——截断不吞终态事实。HTML 版(import.html,renderImportReportHtml)与 JSON 同 try 落盘,动态文本全转义(Plan 2 Task 6 模板纪律)。
|
||||
|
||||
## 决策五:编排串行分派契约(锚点依赖推导,Task 5 审查移交钉死)
|
||||
|
||||
9. **事件期严格串行 await**:content.handleEvent → term → media → route 逐家 await——term/media/route 三家的 entry 锚点解析依赖 content 对**同事件**的落库产物(source_records.content_entry_id / entry_locales),**禁 Promise.all**(并行下锚点查询可能抢在 content 写入前发生)。wp:attachment 行媒体域归 media-writer 单一 owner,content-writer 只留 record-only 足迹(前置事实 11)。
|
||||
10. **finish 唯一硬序**:`await media.finish()` 先于 `await route.finish()`(attachment 301 反查与 asset 全表都读 media_assets——资产须已齐);term/content finish 是纯返回(无库依赖)任意位置。**失败报告部分冲刷**:分派/finish 中途抛错时 term/content 纯 finish 就地补算 + eventsDispatched 进度计数冲入累加器后重抛;media/route finish 依赖库与先序产物,失败路径不调用(部分数据不可安全取得)。
|
||||
|
||||
- **理由**:幂等三态(miss / checksum 同 skip / checksum 异新修订)全部落在数据库唯一约束与不可变触发器之上(Plan 1 的"约束即规格"),应用层零防御性锁;自建 term 与收编语义保证 WXR 文档序不敏感(真实导出序 items 先于 wp:term 是常态而非例外);串行分派以最小复杂度满足锚点依赖,杜绝并行竞态面。
|
||||
- **权衡/陷阱**:① 五列身份解析的 latest-first 口径依赖 source_records 无并发写入方(CLI 单进程成立,REST 化时需重审——与决策四同一重审点);② auto: 前缀行被后续 wp:term 收编后 source_id 变更——跨快照的 term 幂等以 (site, taxonomy, slug) 域锚定而非 source_id,行为正确但排查时须知两种 source_id 形态并存;③ counts 求和的前提"键前缀互斥"是**约定不是约束**——新写入器复用他家键名会静默相加,JSDoc 键归属声明是唯一防线;④ 24h 回收窗口内崩溃残留的 job 会阻塞同 key 重提交的语义澄清(复用命中非终态 → CLI 警告 + 显式换 --key 重试),这是有意的"不静默重跑"纪律。
|
||||
Reference in New Issue
Block a user