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>
This commit is contained in:
浅念
2026-10-21 15:56:00 +08:00
parent 0e8b2b5356
commit eb188da5c2
4 changed files with 293 additions and 66 deletions
+20 -10
View File
@@ -180,18 +180,28 @@ program
console.log(
`[import] job=${result.jobId} status=${result.status}${result.reused ? '(复用既有 job,未重跑)' : ''}`,
)
console.log(` site=${result.siteKey} connection=${result.connectionId}`)
console.log(
` wxr 快照: ${result.wxrSnapshot.sha256.slice(0, 12)}… (${result.wxrSnapshot.id})`,
)
console.log(
` media_bundle: ${result.mediaBundleSnapshot === null ? '(无)' : result.mediaBundleSnapshot.id} / manual_url_list: ${result.manualUrlListSnapshot === null ? '(无)' : result.manualUrlListSnapshot.id}`,
)
console.log(
` 版本: adapter=${result.versions.adapter} mapping=${result.versions.mapping} sanitizer=${result.versions.sanitizer} parser=${result.versions.parser}`,
)
console.log(` site=${opts.site} mapping_version=${result.mappingVersion}`)
// 复用口径忠实化:快照/版本块只在新跑时解析返回——复用时权威记录在既有 job 报告。
// 非复用路径结构性填满这些可选字段(服务侧不变量),此处非空断言表达该约定
if (!result.reused) {
console.log(` connection=${result.connectionId!}`)
console.log(
` wxr 快照: ${result.wxrSnapshot!.sha256.slice(0, 12)}… (${result.wxrSnapshot!.id})`,
)
console.log(
` media_bundle: ${result.mediaBundleSnapshot! === null ? '(无)' : result.mediaBundleSnapshot!.id} / manual_url_list: ${result.manualUrlListSnapshot! === null ? '(无)' : result.manualUrlListSnapshot!.id}`,
)
console.log(
` 版本: adapter=${result.versions!.adapter} mapping=${result.versions!.mapping} sanitizer=${result.versions!.sanitizer} parser=${result.versions!.parser}`,
)
}
if (result.reportPath !== null) console.log(` report: ${result.reportPath}`)
console.log(` total ${result.durationMs.toFixed(1)}ms`)
// 崩溃残留哨兵:复用命中非终态(queued/running)= 上一次执行未走完终态更新——
// 本命令不自动重跑(幂等纪律),操作者显式换 --key 才发起新执行
if (result.reused && (result.status === 'queued' || result.status === 'running')) {
console.error(` 警告:job 处于 ${result.status}——可能为崩溃残留,重试请显式换 --key`)
}
if (result.status === 'failed') process.exitCode = 1
} finally {
await (db as { $client: { end: () => Promise<void> } }).$client.end()