docs: 阶段2收尾——ADR-016/typecheck链/阶段3移交清单/README

- ADR-016:saxes ^6.0.0(勘误:计划曾误写 ^0.9)/commander ^14 根依赖/清洗三件套
  同 range + 镜像直调 launder.naughtyHref/tsx ^4 CLI 直吃 TS;跨包接线(根 tsconfig
  paths 三方消费、build 侧 dist+prebuild、tsconfig 双链型并存);根 engines 升
  >=22.12(对齐 sanitize-html 2.18 engines,CI node 22 与本机 24 均满足)
- 根 typecheck 链扩为四段:追加 pnpm --filter @sf/adapter-wp typecheck
- Plan 2 文末新增"移交阶段 3 清单"定稿(7 项:taxonomyRefs 自建缺失 term、
  media_bundle 映射、connection 库级唯一索引、REST 重审项、导入消费接口、
  Task 0.2 cms 备份续传携带、versions.ts bump 纪律);Task 9 与验收节勾选
- README:快速开始加 pnpm sf 示例;目录结构/文档导航/当前状态更新为阶段 2 完成
- 三绿:lint / typecheck(四段)/ test 263 全部通过(数量不变)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 13:34:15 +08:00
parent 57357cd493
commit 2a0d830520
6 changed files with 64 additions and 20 deletions
+14 -5
View File
@@ -4,7 +4,7 @@ WordPress 站点到静态站点迁移引擎:读取 WXR/Sitemap/媒体归档
## 快速开始
前置:Node.js >= 22、Docker、[corepack](https://nodejs.org/api/corepack.html)。
前置:Node.js >= 22.12、Docker、[corepack](https://nodejs.org/api/corepack.html)。
```bash
corepack enable # 激活 pnpm(或自行安装 pnpm@10)
@@ -14,6 +14,13 @@ pnpm db:migrate # 开发库全量迁移(apps/api/drizzle
pnpm db:test # 一键验收:起库 + 跑全部测试(测试用临时库,开发库零写入)
```
WXR 适配器(阶段 2)速览——`pnpm sf` CLI(tsx 直吃 TS,无需构建):
```bash
pnpm sf wxr:probe --wxr path/to/export.xml # 只读诊断:流式解析→映射→清洗→计数报告(不入库)
pnpm sf source:register --site <key> --wxr <path> [--sitemap <path>] [--media <dir>] # 快照登记(content-addressed、幂等)
```
日常质量门(等价于 CI):
```bash
@@ -25,11 +32,12 @@ pnpm lint && pnpm typecheck && pnpm test
## 目录结构
```
apps/api/ NestJS standalone 骨架 + Drizzle schema/迁移(21 表,数据库约束即规格)
apps/api/ NestJS standalone 骨架 + Drizzle schema/迁移(21 表,数据库约束即规格)+ source 快照登记
packages/contracts/ UniversalRow / Route / Render / Theme 跨源契约(冻结,变更须升版)
packages/adapter-wordpress/ WXR 适配器(saxes 流式解析 + PHP unserialize + 映射 + HTML 白名单清洗,纯函数无 DB)
tests/ 单元 + 集成测试(fixtures:WXR/Sitemap/媒体样例)
infra/ docker-compose:PostgreSQL 16(端口 5434,避开 cms 项目 5433)
scripts/ migrate-test.sh 等运维脚本
scripts/ migrate-test.sh 等运维脚本 · sf.ts(source:register / wxr:probe CLI)
docs/ adr/ 决策记录 · evidence/ 基线证据 · superpowers/ 规格与计划
```
@@ -37,10 +45,11 @@ docs/ adr/ 决策记录 · evidence/ 基线证据 · superpowers/
- 设计基线(表结构/技术决策/阶段划分):`docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design-v2.md`
- 阶段 0+1 实施计划:`docs/superpowers/plans/2026-10-20-slice-foundation-phase0-1.md`
- 架构决策记录(ADR-001~015):`docs/adr/`
- 架构决策记录(ADR-001~016):`docs/adr/`
- 基线证据(环境/真实 WXR):`docs/evidence/`
## 当前状态
- **阶段 1 已完成**:契约冻结、21 表数据模型全量落地(不可变触发器/唯一约束/CHECK 均落数据库并被 75 项测试锁定)、空环境一键启动+迁移验收通过。
- **下一步:阶段 2**——WXR 适配器(流式 SAX 解析 → UniversalRow),计划基于阶段 1 经验另立。
- **阶段 2 已完成**:WXR 流式适配器(saxes 流式解析 → PHP unserialize → UniversalRow 映射 → HTML 白名单清洗 → 导入前报告)、`source:register` 快照登记(content-addressed、幂等)与 `pnpm sf` CLI;263 测试全绿,真实 hesupipe WXR(5.5MB)冒烟 18 项计数对照全过(峰值堆 62MiB < 100MiB,证据见 `docs/evidence/phase2-smoke.md`)。
- **下一步:阶段 3**——内容/分类/媒体入库(source_records 落库、稳定身份 upsert、checksum skip、route_registry 生成);输入清单见 Plan 2 文末"移交阶段 3 清单"。
@@ -0,0 +1,23 @@
# ADR-016:阶段 2 新增依赖与跨包接线
- **状态**:已采纳(Plan 2 Task 9 收尾,阶段 2;许可证已逐项核验——saxes ISC,commander/sanitize-html/parse-srcset/launder/tsx 均 MIT)
## 决策一:新增依赖(选型 / 版本范围 / 定位)
1. **saxes `^6.0.0`**(@sf/adapter-wp 运行时依赖)——WXR 流式 SAX 解析的唯一解析库。**勘误**:Plan 2 Task 3 曾误写"锁 ^0.9"(0.x 是多年前的旧版本线),实施时按 npm 实际主线锁 `^6.0.0`;默认不解析外部实体(XXE 面由测试钉死)。`versions.ts` 的 `PARSER_VERSION` 常量须与实际解析版本同步(当前 `'saxes 6.0.0'`)。
2. **commander `^14.0.0`(根依赖)**——CLI 入口在仓库根 `scripts/sf.ts`,而 @sf/adapter-wp 定位纯函数包、不消费 CLI 框架;commander 放根,不给库包带入无关运行时依赖。
3. **清洗三件套 `sanitize-html ^2` + `parse-srcset ^1` + `launder ^1`**(@sf/adapter-wp 运行时依赖,三条 range 并存)——sanitize-html 是清洗本体;srcset/imagesrcset 审计用 parse-srcset(sanitize-html 内部即同库依赖);URL 拒绝判定**直接调用 `launder.naughtyHref`**——sanitize-html 2.18 内部即解构调用同一函数,镜像判定与库本体零漂移(依赖升级时由参数化对抗测试护栏兜底)。三件套用同一主版本 range 跟随解析:launder/parse-srcset 本是 sanitize-html 的传递依赖,若单独锁死版本,会造成"镜像调用的 launder"与"库内部用的 launder"版本 skew。类型:@types/sanitize-html、@types/parse-srcset 走 devDependencies;launder 无官方类型,本包自带 d.ts 垫片(`src/types/launder.d.ts`)。
4. **tsx `^4`(根 devDependency)**——CLI 直吃 TS 源码:`pnpm sf` = `tsx scripts/sf.ts`,无需预构建 dist(apps/api 的 Nest 装饰器源码同样可跑);tsx 属开发工具链,不进任何运行时依赖。
## 决策二:跨包接线
5. **根 tsconfig paths 三方消费(单源)**:`@sf/contracts` 与 `@sf/adapter-wp` 到各自 `src/index.ts` 的映射只在根 `tsconfig.json` compilerOptions.paths 维护一份——包级 tsc 经 extends 继承;vitest 不读 tsconfig paths,由 `vitest.config.ts` 的 `resolve.alias` 镜射同一映射;tsx 原生读 tsconfig paths。三方(包级 tsc / vitest / tsx)全部直连 `src/index.ts`——日常开发链 import 不经 dist、无预构建依赖。
6. **build 侧 dist + prebuild**:@sf/adapter-wp 的 `tsconfig.build.json` 把 `@sf/contracts` 改指 `packages/contracts/dist/src/index.d.ts`(emit 程序不得包含包外源文件,TS6059),由 prebuild 先 `pnpm --filter @sf/contracts build` 供给 dist 声明。dist 只为发布产物完整性负责;typecheck / test / CLI 通道不依赖它。
7. **tsconfig 双链型并存**:contracts extends `tsconfig.base.json`(无跨包 import,不需要 paths);adapter-wp extends **根 tsconfig.json**(为继承 baseUrl/paths 跨包映射)。两条 extends 链并存是有意的:包是否需要跨包映射决定挂哪条链,不为形式统一强行并链。**src-vs-dist 的最终口径即上述 5+6:开发链 src 直连、build 链 dist+prebuild,双轨并存**(Plan 1 终审前瞻项在此落定)。
## 决策三:engines 口径
8. **根 engines 升 `>=22.12`**:sanitize-html 2.18 的 engines 要求 node `>=22.12.0`,根原口径 `>=22` 会放行 sanitize-html 不支持的 22.0–22.11。升根一行对齐(CI 的 node 22 取最新 22.x、本机 24 均满足);否决的替代方案是 pin sanitize-html 旧版——为迁就旧 patch 版 node 反向锁死清洗库,不取。后续新增依赖若 engines 高于根,同理评估升根而非降依赖。
- **理由**:全部依赖均为单一职责引入(解析/清洗/CLI/TS 执行各一),无重叠;版本范围统一锁主版本(`^`),与"锁版本 + 核许可证 + 记 ADR"的新增依赖三纪律一致;跨包接线坚持单源配置(paths 一份、alias 镜射),把 skew 面压到最小。
- **权衡/陷阱**:① vitest alias 是根 paths 的人工镜射,新增 workspace 包时两处要同步改;② build 链与开发链对 `@sf/contracts` 的解析目标不同(dist 声明 vs src),dist 过期只影响产物不影响日常——发布前跑 build 即可暴露;③ launder 镜像判定的等价性依赖"sanitize-html 内部继续用 launder 同一函数",升级 sanitize-html 大版本时须重验(对抗测试在 `test/html-sanitize.test.ts`);④ engines 对齐的是传递依赖里最严的要求,属"就高"策略,接受它对低版本 node 的排斥。
@@ -138,18 +138,30 @@ cms-one/
- [ ] Step 2: 峰值内存记录——smoke 脚本内 `setInterval(100ms)` 采样 `heapUsed` 与 `rss` 取 max(瞬时值无意义);流式断言:解析 5.5MB 文件峰值 heapUsed < 100MB(不整读)
- [ ] Step 3: 耗时记录;结果写 evidence 文档;commit `test(evidence): 阶段2真实WXR冒烟通过`
### Task 9: 阶段 2 收尾
### Task 9: 阶段 2 收尾 ✅ 完成(ADR-016 定稿 / typecheck 四段链 / 根 engines 升 >=22.12 / 移交阶段 3 清单 / README,同批一个 docs commit;三绿 263 不变;不 push——推送由用户执行)
- [ ] ADR-016(saxes/commander/sanitize-html 选型与许可证);根 typecheck 链加 @sf/adapter-wp;lint 全绿;contracts 首个消费者接线验证(adapter-wp workspace 依赖 contracts——**Plan 1 终审前瞻项落实**:import 路径用 dist 还是 src 的决策一并入 ADR-016)
- [ ] **Task 7 审查移交**:① connection (site_id,name) 库级唯一索引须在第二个写入方(REST 通道)出现前完成 ② media_bundle→WXR attachment URL 的映射策略(uploads 路径约定)记入阶段 3 计划输入 ③ REST 化时重审:白名单 TOCTOU、cwd 自动放行、connection.kind 枚举扩 ④ sanitize-html engines>=22.12 与根 engines>=22 的口径(升根或记 ADR)
- [ ] 全量三绿 + push;plan 勾选;**移交阶段 3 清单**(内容/分类/媒体入库:media 魔数校验、source_records 落库、稳定身份 upsert、checksum skip、route_registry 生成;**续传携带:Task 0.2 cms 数据备份(用户暂缓,阶段 6 L3 验收前必须补做**——责任链不断)
- [x] ADR-016(saxes/commander/sanitize-html 选型与许可证);根 typecheck 链加 @sf/adapter-wp;lint 全绿;contracts 首个消费者接线验证(adapter-wp workspace 依赖 contracts——**Plan 1 终审前瞻项落实**:import 路径用 dist 还是 src 的决策一并入 ADR-016)
- [x] **Task 7 审查移交**:① connection (site_id,name) 库级唯一索引须在第二个写入方(REST 通道)出现前完成 ② media_bundle→WXR attachment URL 的映射策略(uploads 路径约定)记入阶段 3 计划输入 ③ REST 化时重审:白名单 TOCTOU、cwd 自动放行、connection.kind 枚举扩 ④ sanitize-html engines>=22.12 与根 engines>=22 的口径(升根或记 ADR)——①~④ 均已沉淀至文末"移交阶段 3 清单",④ 已落 ADR-016 决策三(根 engines 升 >=22.12)
- [x] 全量三绿 + plan 勾选;**移交阶段 3 清单**(见文末新节定稿);push 由用户执行(本任务不 push)
## 验收(对照 spec §15 阶段 2)
- [ ] `source:register` 三类输入登记幂等,快照 content-addressed 且不可变
- [ ] 脱敏夹具全链全绿(mini.xml → 5 row + 0 意外 unsupported)
- [ ] 大文件不整读内存(5.5MB 峰值 <100MB)
- [ ] 恶意/畸形输入安全失败(外部实体/超限/截断——带位置错误,不崩溃不静默)
- [ ] 解析器不访问网络(测试钉死)
- [ ] 真实 WXR 冒烟计数与基线逐项一致;unsupported 75 条分类清晰
- [ ] 三绿(lint/typecheck/test)+ 全部推送
- [x] `source:register` 三类输入登记幂等,快照 content-addressed 且不可变
- [x] 脱敏夹具全链全绿(mini.xml → 5 row + 0 意外 unsupported)
- [x] 大文件不整读内存(5.5MB 峰值 <100MB)
- [x] 恶意/畸形输入安全失败(外部实体/超限/截断——带位置错误,不崩溃不静默)
- [x] 解析器不访问网络(测试钉死)
- [x] 真实 WXR 冒烟计数与基线逐项一致;unsupported 75 条分类清晰
- [x] 三绿(lint/typecheck/test,263 测试);推送由用户执行(Task 9 不 push)
---
## 移交阶段 3 清单(Plan 2 收官定稿——散落各任务的移交项汇总,阶段 3 计划的必读输入)
1. **taxonomyRefs 解析:wp:term 表不完备(真实数据实证)**——本 WXR 的 term 表缺 category(148 引用/5 nicename)与 post_tag(659 引用/471 nicename)条目(WordPress 7.1.3 导出行为)。阶段 3 做 slug→term 解析时须按 **(domain, nicename) 自建缺失 term**(item 引用的 value 作 name),不能只依赖 wp:term 事件。锚点设计已被实数据正向验证:term 表覆盖的 taxonomy 内 nicename≡slug 100%(706/706,见 `docs/evidence/phase2-smoke.md`)。
2. **media_bundle → WXR attachment URL 映射策略**——media 清单(snapshot metadata.manifest,相对路径)与 WXR item 的 attachment_url/图库引用之间的映射须定策略(WordPress uploads 路径约定 `wp-content/uploads/<yyyy>/<mm>/<file>`;路径对拍 + 失配报告)——消费清单时面对。
3. **connection (site_id, name) 库级唯一索引**——现为应用层 select-then-insert(事务 + advisory lock 保护,register.service.ts 注释已注明);**第二写入方(REST 通道)出现前**必须落库级唯一约束。
4. **REST 通道落地时重审**——路径白名单 TOCTOU(校验与 open 之间的窗口)、CLI cwd 自动放行的去留、connection.kind 枚举扩('cli' 之外的新来源值)。
5. **导入管道消费接口**——registerSource 返回值与 snapshot metadata 形状(含 storageRoot/manifest)是阶段 3 的输入契约,消费侧若需变形优先加法扩展;source_records 落库用稳定身份键(ADR-006);checksum skip 语义:changed=false → skip(报告 created/skipped 即导入前投影,spec §7.5)。
6. **续传携带:Task 0.2 cms 数据备份**(用户暂缓)——**阶段 6 L3 验收前必须补做,责任链不断**。
7. **versions.ts bump 纪律提醒**——映射表(DEFAULT_TYPE_ROUTES / STATUS_MAP / META_KEYS)或清洗白名单(SANITIZE_CONFIG)任何实质变更,须 bump 对应 MAPPING_VERSION / SANITIZER_VERSION(bump 语义 JSDoc 已冻结于 `packages/adapter-wordpress/src/versions.ts`);下游以版本块判断是否需要重新标准化(spec §7.5)。
+2 -2
View File
@@ -4,11 +4,11 @@
"type": "module",
"packageManager": "pnpm@10.7.1",
"engines": {
"node": ">=22"
"node": ">=22.12"
},
"scripts": {
"test": "vitest run",
"typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter @sf/api typecheck && pnpm --filter @sf/contracts typecheck",
"typecheck": "tsc -p tsconfig.json --noEmit && pnpm --filter @sf/api typecheck && pnpm --filter @sf/contracts typecheck && pnpm --filter @sf/adapter-wp typecheck",
"lint": "eslint .",
"db:up": "docker compose -f infra/docker-compose.yml up -d --wait",
"db:migrate": "pnpm --filter @sf/api db:migrate",
@@ -6,7 +6,7 @@
// Task 4 起 src 引用 @sf/contracts(type-only):emit 程序不得含包外源文件
// (rootDir 下 TS6059),故 build 侧改经 contracts 预构建 dist 声明解析(先
// `pnpm --filter @sf/contracts build`);typecheck/vitest/tsx 仍走根 paths 的
// src 直连。src-vs-dist 的最终统一归 ADR-016(Task 9)。paths 相对继承的根
// src 直连。src-vs-dist 双轨决策见 ADR-016。paths 相对继承的根
// baseUrl(仓库根)解析。
"paths": {
"@sf/contracts": ["./packages/contracts/dist/src/index.d.ts"]
+1 -1
View File
@@ -1,4 +1,4 @@
// extends 根而非 base:为继承 baseUrl/paths 跨包映射(contracts 走 base 不需要)——决策将记入 ADR-016
// extends 根而非 base:为继承 baseUrl/paths 跨包映射(contracts 走 base 不需要)——决策见 ADR-016
{
"extends": "../../tsconfig.json",
"compilerOptions": {