From ecff5a34afb412d1be1bfb166895d33b47f7a035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 20 Oct 2026 22:46:37 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20Plan=202=20=E5=AE=9E=E6=96=BD?= =?UTF-8?q?=E8=AE=A1=E5=88=92=E2=80=94=E2=80=94=E9=98=B6=E6=AE=B52=20WXR?= =?UTF-8?q?=20=E6=B5=81=E5=BC=8F=E9=80=82=E9=85=8D=E5=99=A8=EF=BC=889?= =?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .../2026-10-20-slice-wxr-adapter-phase2.md | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-20-slice-wxr-adapter-phase2.md diff --git a/docs/superpowers/plans/2026-10-20-slice-wxr-adapter-phase2.md b/docs/superpowers/plans/2026-10-20-slice-wxr-adapter-phase2.md new file mode 100644 index 0000000..41669d9 --- /dev/null +++ b/docs/superpowers/plans/2026-10-20-slice-wxr-adapter-phase2.md @@ -0,0 +1,147 @@ +# SiteForge 纵向切片 · Plan 2:WXR 流式适配器(阶段 2)实施计划 + +> **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:** 建立 `source:register` 输入登记(content-addressed 快照)与 WXR 流式解析适配器:任意 WXR → 限额安全的流式解析 → UniversalRow 流 + 未支持对象/清洗差异报告;真实 hesupipe WXR(5.5MB)冒烟计数对照基线通过。 + +**Architecture:** 依据设计基线 `docs/superpowers/specs/2026-10-20-siteforge-vertical-slice-design-v2.md` §7(WXR 导入管道)、§15 阶段 2、ADR-004/005/006/009。新增 `packages/adapter-wordpress`(saxes 流式解析 + PHP unserialize + 映射器 + HTML 清洗,纯函数无 DB);`apps/api` 内新增 source 模块(快照登记服务,复用既有表);`scripts/sf.ts` CLI 入口。Plan 1 地基复用:contracts 守卫、21 表、globalSetup 测试基建、mini.xml 夹具。 + +**Tech Stack:** saxes(流式 SAX 解析)、commander(CLI)、sanitize-html(白名单清洗)+ 既有栈。**新增依赖三条 ADR 纪律**:锁版本、检查许可证(saxes ISC / commander MIT / sanitize-html MIT 均可)、记入 ADR-016。 + +**边界(不做)**:内容入库/修订写入(阶段 3)、媒体魔数校验与 usage(阶段 3)、REST 通道、增量/差量、爬虫。 + +--- + +## 前置事实(执行者必读) + +1. Plan 1 已收官:75 测试全绿、21 表、四契约冻结(**UniversalRow 任何字段变更须升版**——本阶段只消费不修改)。 +2. **Plan 1 移交的责任清单**(本计划落实):canonical `status` 值域钉在转换测试;适配器 property 测试(taxonomyRefs/attachmentRefs 元素形状、sourceId/sourceTermId 非空);如需字段级错误用**加法式** `validateUniversalRow(): issue[]`(不动布尔守卫)。 +3. 夹具:`tests/fixtures/wxr/mini.xml`(5 item/2 term,含 wppop_sections PHP 串与**有意悬空**的图库引用 203/204);真实基线:`docs/evidence/real-wxr-baseline.md`(1122 item = 855 attachment/129 post/48 product/15 page/68 nav_menu_item/3 wpcf7_contact_form/2 elementor_library/2 wp_navigation;451 term)。 +4. 解析器**绝不访问网络**(saxes 默认不解析外部实体——用测试钉死);媒体输入只接受已解包目录。 +5. 既有纪律:TDD(红→绿→commit)、测试不碰开发库(globalSetup 临时库 + `DATABASE_URL_TEST`)、身份断言、无包级 test 脚本、typecheck 门禁、cms 目录绝对只读。 + +## 仓库结构增量 + +```text +cms-one/ + packages/adapter-wordpress/ + src/parse/wxr-stream.ts # saxes 流式解析 → AsyncIterable + src/parse/php-unserialize.ts # 受限 PHP serialize 解析(wppop_sections) + src/map/item-to-row.ts # WxrItem → UniversalRow(类型路由/meta 提取/known-extras 分流) + src/sanitize/html.ts # 白名单清洗 + 差异收集 + src/adapter.ts # 组装:parse→map→sanitize → AsyncIterable + src/limits.ts # 限额配置类型与默认值 + test/*.test.ts # 纯单元(不连库) + apps/api/src/modules/source/ + register.service.ts # 快照登记(copy→sha256→source_snapshots 落库) + source.module.ts + scripts/sf.ts # CLI(source:register / wxr:probe) + tests/integration/source-register.test.ts +``` + +--- + +### Task 1: adapter-wordpress 包骨架 + 限额类型 + +**Files:** Create `packages/adapter-wordpress/package.json`(@sf/adapter-wp,无 runtime 依赖本任务)、`tsconfig.json`/`tsconfig.build.json`(照 contracts 先例)、`src/limits.ts`;Test `test/limits.test.ts` + +- [ ] Step 1 失败测试:默认限额值冻结(maxWxrBytes 64MB、maxItems 200k、maxDepth 64、maxCdataLength 1MB、maxTermCount 50k、maxMetaPerItem 200、timeBudgetMs 120s、phpMaxDepth 8、phpMaxElements 1000)+ 类型字段齐全 +- [ ] Step 2 实现 `WxrLimits` 接口 + `DEFAULT_WXR_LIMITS` 常量(as const)+ 类型锁(照 contracts 单源派生先例锁值域) +- [ ] Step 3 绿 → commit `feat(adapter-wp): 包骨架与限额契约` + +### Task 2: PHP unserialize 受限解析器 + +**Files:** Create `src/parse/php-unserialize.ts`;Test `test/php-unserialize.test.ts` + +- [ ] Step 1 失败测试(TDD 用例集): + - 夹具真实串 `a:1:{i:0;a:2:{s:5:"title";s:8:"SpecsTab";s:7:"content";s:10:"

Tab

";}}` → 深层对象等值 + - 标量串 `s:5:"hello"` / `i:42` / `b:1` + - **长度前缀错误**(s:7:"SpecsTab")→ 抛 `PhpParseError`(不静默) + - 深度超 phpMaxDepth → 抛限额错误;元素数超 phpMaxElements → 抛 + - 空串/截断串 → 抛(带位置信息) +- [ ] Step 2 实现递归下降解析器(约 80 行:i:/b:/d:/s:/a: 四类型,NUL 分隔,位置追踪) +- [ ] Step 3 绿 → commit `feat(adapter-wp): PHP serialize 受限解析器` +- [ ] 注:只支持 saxes 产出的字符串输入;**绝不 eval**;深度/元素数双限额 + +### Task 3: saxes 流式 WXR 解析器 + +**Files:** Create `src/parse/wxr-stream.ts`(依赖 saxes,锁 ^0.9);Test `test/wxr-stream.test.ts`(用 mini.xml) + +- [ ] Step 1 失败测试: + - mini.xml 流式产出 5 个 item + 2 个 term(顺序与文档一致);每 item 含 post_id/post_type/status/post_name/link/title/content:encoded/excerpt/post_date/post_modified/attachment_url/全部 postmeta/以及**原始字节区间**(rawLocation: `wxr://item/<序号>`) + - term 产出:term_id/taxonomy/slug/name + - CDATA 正确解包;`` 聚合为 `{key, value}[]` + - **限额**:maxItems=2 时跑到第 3 个 item 抛限额错误;maxCdataLength=10 时超长 CDATA 抛 +- [ ] Step 2 实现:`parseWxrStream(readable: Readable, limits): AsyncGenerator`——saxes 前缀解析、状态机聚合 item/term、每 item 完成即 yield(**不整文件进内存**:断言峰值内存——用大夹具在 Task 8 验证,此处以"不 buffer item 列表"的代码结构保证) +- [ ] Step 3 绿 → commit `feat(adapter-wp): saxes 流式 WXR 解析器` +- [ ] **安全测试并入**:外部实体(``/onclick/`javascript:`/`