Commit Graph

9 Commits

Author SHA1 Message Date
浅念 eb188da5c2 fix(import): 复用口径忠实化;管道 Outcome 契约与 succeeded_with_issues;报告写入时序;stale 警告
评审修复(Plan 3 Task 1 复审):
- 复用路径只返回 job 锚定字段(jobId/status/reused/siteId/mapping_version/
  report_path),快照/版本块仅在非复用时解析返回——复用结果不再回显编造数据
- ImportPipeline 契约钉死:返回 Promise<ImportPipelineOutcome>
  ({ counts, issues });服务按"累加器 ⊕ 返回值"合并(counts 同键返回值
  覆盖、issues 累加器在前),issues 非空 → succeeded_with_issues;
  ctx.report 即累加器(管道抛错时部分产出仍入报告)
- 报告时序:mkdir + tmp 写入 + rename 先于终态行更新——report_path 落库
  即文件存在;写失败 → failed(REPORT_WRITE_FAILED),不挂悬空路径
- CLI 复用命中非终态(queued/running)→ 崩溃残留警告(重试需显式换 --key)
- 管道抛 ImportError 保留原 code 入 error_code,summary 冠 [pipeline]
- 确定性 23505 探针:未提交孪生 job 行事务内持唯一键触发真实 PG 唯一
  冲突 → 败者复用既有 job
- 报告补 idempotencyKey / attempt 字段
- 计划文档 Task 6 增补 stale 回收决策项(ADR-017)

测试 282 通过(279 + 3:Outcome 合并/ImportError 前缀/23505 探针);
typecheck 四段链 + lint 全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 15:56:00 +08:00
浅念 0e8b2b5356 feat(import): job 骨架与生命周期
Plan 3 Task 1:sf import 命令骨架 + import_jobs 生命周期(空管道,Task 6 接真实管道)。

- import.service.ts(零装饰器):createImportJob——站点/快照解析(缺省按 (connection,
  kind='wxr') 选最新;三快照绑定按前置事实 13 记入报告,job 行不扩列)、幂等建行
  (默认 key=site+wxr快照+三语义版本;重复提交复用既有 job 不重跑,并发撞唯一约束
  重查收敛)、状态推进 queued→running→succeeded/failed(attempt/started_at/finished_at/
  error_code/error_summary)、报告落 artifacts/import/<jobId>/reports/import.json
  (全四版本块:adapter/mapping/sanitizer/parser;job 行落 mapping_version)
- 可预期错误带 code:SITE_NOT_FOUND / SNAPSHOT_NOT_FOUND / NO_WXR_SNAPSHOT /
  PIPELINE_FAILED——解析期失败零 job 行
- adapter 事件加法式补 postId(前置事实 12):unsupported/error 事件可选携带
  String(postId),缺失时键缺席不硬造 source_id;row 事件与报告聚合零影响、不触发版本 bump
- canonicalJson 提取至 apps/api/src/common/canonical.ts 共享导出(前置事实 6),
  register 侧改 import 零行为变更(既有清单哈希测试守护)
- sf CLI + import 命令(--site/--snapshot/--key/--connection),输出 job 摘要与版本块
- 测试:tests/integration/import-job.test.ts(5 例:生命周期观察/最新快照解析/幂等复用
  与并发收敛/可预期失败零落行/failed 路径)+ adapter postId 3 例 + canonical 单测 6 例
  (265→279 全绿);typecheck 四段链/lint 绿;临时库 CLI 冒烟(register→import→复用)通过

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 15:30:42 +08:00
浅念 a185254e19 chore(scripts): 侧车导出加固——site-id UUID 校验与 audit 非 CDATA 匹配
评审两条非阻塞加固:--site-id 拼入 SQL 前过 UUID 形态白名单(非 UUID 拒绝并
明确报错);--audit 的 attachment_url 抽取正则同时匹配 CDATA 与裸文本两形态。
对真实数据重跑数字不变(859 条 / 855 对 836 命中,输出哈希一致)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 15:12:41 +08:00
浅念 a227cfac49 feat(source): 媒体映射侧车导出与登记
Plan 3 Task 0(评审 Issue 1):旧 cms 库 media_assets 的 source_url→file_path
映射只读导出为侧车 JSON(859 条,覆盖率对照 WXR 855 attachment_url:精确命中
836 / 归一命中 0 / 未命中 19,97.8%),register 新增 --media-map 输入通道
(kind=manual_url_list 快照,形状校验 INVALID_MEDIA_MAP,metadata 含条目数,
幂等复用)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 14:42:32 +08:00
浅念 21ef37232d test(evidence): 阶段2真实WXR冒烟通过
真实 hesupipe WXR(5,490,399B,SHA-256 与 Plan 1 基线一致)跑 wxr:probe 全链:
计数 18 项对照全过(rows 1047 / terms 451 / unsupported 75 细分一致 / failed 0 /
item 总数 1122);heapUsed max 62.2MiB < 100MiB(setInterval(100ms) 双 max 口径,
全程 167ms 仅 2 样本如实注明);parseMs 含流 IO 等待口径注明。
nicename 锚点核对:在表 taxonomy 706/706 全命中(nicename≡slug,违例 0);
category/post_tag 共 807 引用因 wp:term 表缺该 taxonomy 不可解析——阶段 3 需按
(domain, nicename) 自建缺失 term,不能假设 term 表完备。
eslint.config.js:scripts/*.mjs 显式 node 全局(no-undef 误报)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 13:25:09 +08:00
浅念 c28c1655a4 fix(source): DB 事务化;storageRoot 入 metadata;siteCreated 与并发测试;CLI 体验与校验小修
评审修复(4 项 Important + 8 项 Minor):
- DB 事务化:site→connection→snapshots 全部落库包进 db.transaction 单事务
  ("任何失败零落库"字面成立);助手改收 Pick<select|insert|execute> 结构类型
  (tx 与直连 db 通用)。并发撞唯一约束不在事务内补查(语句失败后续查 25P02),
  改为 registerSource 层事务级重试(≤3 次,第二次 select 命中复用分支)
- connection 并发:select-then-insert 无约束可兜底,事务内 pg_advisory_xact_lock
  (siteId:name) 串行化同键登记(kind 枚举扩展推迟 REST 通道,注释注明)
- storageRoot 绝对路径固化进每条 snapshot metadata 与报告 inputs(快照不可变,
  事后无法补写——永久缺口封堵)
- ensureSite 返回 created;RegisterResult.siteCreated;CLI 站点已存在时提示
  --name/--locale/--origin 未生效
- 媒体复制期逐文件 sha 复核(copyAndHash 对拍清单,源中途变更 → COPY_FAILED)
- 空媒体目录拒绝(EMPTY_MEDIA_DIR)
- CLI:PATH_NOT_ALLOWED 追加 --allow 提示;--origin 经 new URL 校验(http/s)、
  --locale 语言标签形状校验,fail fast;开跑打印生效白名单(审计)
- makeRunId 服务导出、CLI 复用;.tmp 残留手工清理策略写入文件头注释
- 测试 +2:并发同输入恰好一组行/双方同 id/新建标志恰一(3 次重跑稳定);
  isUniqueViolation 形状单测(drizzle cause.code / 裸 code / 其他)

全套 263 绿;typecheck / lint 绿;CLI 冒烟对临时库验证事务路径、siteCreated
输出、defaults 落库(en-US / https://qa.example)、白名单打印与校验报错

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 13:06:48 +08:00
浅念 586d715455 feat(source): 输入登记服务与 sf CLI(content-addressed 快照)
- registerSource(db, input):wxr/sitemap/媒体目录三类输入登记为 content-addressed
  快照——流式 copy+sha256 单遍双算(64KB 块),副本落
  storage/snapshots/<kind>/<sha256前2>/<sha256>(文件)或 <sha256>/(媒体整树),
  后续任务只读副本;UNIQUE(site,kind,sha256) 幂等(重复登记复用既有行)
- media_bundle 哈希钉死:排序相对路径('/' 归一)+ 每文件 sha256 → canonical
  JSON(键排序无空白)→ 再 sha256;不含 mtime、不依赖遍历顺序;完整清单
  (path/bytes/sha256)入 metadata jsonb;符号链接(含 junction)跳过并记录
- site 按 key select-then-insert upsert(23505 竞态重查);connection 判重键
  (site_id,name) 为应用层 select-then-insert(表无库级唯一,注释注明)
- 路径安全:输入 realpath 后须在 allowedRoots 白名单内(缺省 storage/incoming),
  ENOENT/越界/类型不符抛带 code 的 SourceRegisterError;全部输入先校验后摄取,
  任何失败零落库零复制
- 报告落 artifacts/register/<runId>/reports/register.json(runId=时间戳+随机;
  输入清单/哈希/大小/耗时)
- scripts/sf.ts(commander):wxr:probe 只读诊断(任意可读路径,IO/损坏文件出
  报告不崩——Task 6 fatalError 契约)出 JSON/HTML 报告;source:register 调服务
  出摘要。CLI 机制钉死:根 devDep tsx + 根 script sf + commander 根依赖,TS 源码
  直连(@sf/* 经 tsconfig paths,不经 dist)
- source.module.ts:Nest 注册 SourceRegisterService(standalone context 用);
  register.service.ts 刻意零装饰器(CLI/测试直 import 不拉 reflect-metadata)
- launder 类型垫片改经 triple-slash 引用随 html.ts 进任意编译程序(apps/api 与
  根程序直连适配器源码的新场景);apps/api 增 @sf/adapter-wp workspace 依赖与
  paths 映射(register.service 消费 ADAPTER_VERSION 单源)
- 测试 +7(tests/integration/source-register.test.ts,临时库 + tmpdir storage/
  报告根 finally 清理):三输入登记/清单哈希定义对拍/重复登记幂等/mtime 排除/
  副本布局与内容一致/路径安全六分支/junction 跳过/报告契约;全套 260 绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-21 12:31:15 +08:00
浅念 3c32becba6 chore: 收尾清账——type:module/gitattributes/engines/lint/CI/根typecheck覆盖
Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 20:56:26 +08:00
浅念 f1c181de02 test(db): 空库一键迁移验收 + globalSetup 临时库 + 公共夹具收敛(阶段1验收)
- vitest globalSetup:每次运行 CREATE sf_test_<ts> 临时库 → drizzle-orm 程序化迁移器
  全量迁移(apps/api/src/db/migrate.ts,drizzle-kit 无法指向运行时建库)→ 设
  DATABASE_URL_TEST 让 getDb() 自动回退到临时库;teardown DROP(WITH FORCE)。
  有 default 导出时 vitest 不捕获具名 teardown,故用双具名导出。
- db.full-migrate.test.ts:21 表齐 + 四张不可变触发器在库 + sf_test_* 护栏断言
  (current_database 不符即红,杜绝静默连开发库)。
- helpers.ts 收敛:rand/rejectionMessage/createSiteFixture/withImmutableCleanup/
  withTestDb,Task 6–12 七文件内联版替换为 import(断言、对照组、清场顺序不变)。
- scripts/migrate-test.sh:docker compose up --wait + pnpm test 一键验收。
- 实证:75 测试全绿(72+3);开发库 21 表行数总和 0(零写入);无 sf_test_* 残留。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-20 20:43:13 +08:00