feat(import): term 物化与缺失自建

Plan 3 Task 2:createTermWriter 管道写入器——双源物化分类域。
- wp:term 事件:taxonomies(key→object_type 映射 + public_base_path 前置事实 9
  常量)+ terms(source_id=term_id 字符串)
- row 的 taxonomyRefs miss 时自建(source_id=auto:<nicename>、slug=nicename;
  phase2-smoke 实证真实 WXR 的 wp:term 表缺 category/post_tag 整域);同 nicename
  多次引用只建一条(内存 (taxonomy,slug) 索引 + UNIQUE 兜底)
- 单遍流式 + 收编:WXR 文档序 items 先于 wp:term——row 先到自建、wp:term 后到
  就地 UPDATE 扶正 source_id/name,两种顺序终态一致(测试钉死顺序无关性)
- content_terms:is_primary 每 (entry, taxonomy) 首个 true 余 false(WP 单主分类
  近似);row 无 content entry → term 仍物化、关联跳过 + issue(禁静默)
- ctx 加法扩展 events: AsyncIterable<AdapterEvent>(测试手构注入,Task 6 接真流)
- Task 1 遗留两行:ImportErrorCode 补 REPORT_WRITE_FAILED、ctx.db 事务边界 JSDoc

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 16:19:00 +08:00
parent 2d57c3c814
commit 191c275713
3 changed files with 802 additions and 1 deletions
+26 -1
View File
@@ -34,6 +34,7 @@ import {
MAPPING_VERSION,
PARSER_VERSION,
SANITIZER_VERSION,
type AdapterEvent,
} from '@sf/adapter-wp'
import { importJobs, sites, sourceConnections, sourceSnapshots } from '../../db/schema.js'
import type * as schema from '../../db/schema.js'
@@ -51,6 +52,7 @@ export type ImportErrorCode =
| 'SNAPSHOT_NOT_FOUND'
| 'NO_WXR_SNAPSHOT'
| 'PIPELINE_FAILED'
| 'REPORT_WRITE_FAILED'
/** 导入失败(可预期错误):CLI 以 code 区分呈现;不裹程序 bug(那些原样上抛) */
export class ImportError extends Error {
@@ -83,6 +85,11 @@ export interface SnapshotRef {
/** 管道执行上下文:Task 6 真实管道的全部输入(站点 locale 接缝见前置事实 8) */
export interface ImportJobContext {
/**
* 写入器自行管理事务:ctx.db 为直连(非事务代理),需要原子性的多语句写入用
* db.transaction 自行包裹;单语句 upsert(on conflict do nothing/update)本身原子,
* 无需显式事务。
*/
db: ImportDb
jobId: string
siteId: string
@@ -100,6 +107,13 @@ export interface ImportJobContext {
* ctx.report.issues.push(...))。合并规则见 {@link ImportPipeline}——返回值优先。
*/
report: ImportPipelineOutcome
/**
* 适配器事件流(runWxrAdapter 产物:row/unsupported/error/term 按文档序交错)。
* Task 2 加法扩展:测试经 {@link CreateImportJobInput.events} 手构注入;Task 6 编排
* 时改为从 wxr 快照构造真流(runWxrAdapter(storagePath 流))——多个写入器共享同流
* 时的重迭代策略(每写入器一新生成器等)由 Task 6 决定,本字段只承诺"可迭代一次"。
*/
events: AsyncIterable<AdapterEvent>
}
/**
@@ -130,6 +144,12 @@ export interface CreateImportJobInput {
connectionName?: string
/** 报告根(落 <reportRoot>/<jobId>/reports/import.json);缺省 repoRoot/artifacts/import */
reportRoot?: string
/**
* 适配器事件流(ctx.events 的注入位):Task 2 测试手构注入;缺省空流(空管道下无消费
* 方)。Task 6 编排时由此改为从 wxr 快照构造 runWxrAdapter 真流(前置事实 8:mapConfig
* .locale 必须传 site.default_locale)。
*/
events?: AsyncIterable<AdapterEvent>
/** 管道钩子(测试/Task 6 注入);缺省空管道 */
pipeline?: ImportPipeline
}
@@ -344,7 +364,7 @@ export async function createImportJob(
// 阶段 5:管道(本任务缺省空管道;Task 6 接真实写入器)——抛错 → failed 终态。
// 产出汇合规则(ImportPipeline JSDoc 钉死):返回值 counts 覆盖累加器同名键、issues 追加在后。
let failed: { code: string; summary: string } | null = null
let failed: { code: ImportErrorCode; summary: string } | null = null
const ctx: ImportJobContext = {
db,
jobId,
@@ -357,6 +377,7 @@ export async function createImportJob(
manualUrlListSnapshot,
versions: VERSIONS,
report: { counts: {}, issues: [] },
events: input.events ?? emptyEvents(),
}
let outcome: ImportPipelineOutcome | null = null
try {
@@ -497,3 +518,7 @@ function slimRef(ref: SnapshotRef): {
async function emptyPipeline(_ctx: ImportJobContext): Promise<ImportPipelineOutcome> {
return { counts: {}, issues: [] }
}
/** 缺省空事件流(input.events 缺席时的 ctx.events 占位;每次调用产新迭代器) */
async function* emptyEvents(): AsyncGenerator<AdapterEvent, void, void> {}