feat(import): 全链编排与导入报告
Task 6(Plan 3):四家写入器汇流为默认编排管道。 - import.service:注入 events/pipeline 缺席时走真实事件流(runWxrAdapter 惰性 async generator,mapConfig.locale=site.default_locale 前置事实 8); 分派严格串行 content→term→media→route(禁 Promise.all);finish 硬序 term(sync)→content(sync)→media(await)→route(await);counts 由 ⊕ 覆盖 改为逐 key 求和聚合;新增 stale 回收(同站点 queued/running >24h 一条 SQL 置 stale);报告新增 paths/productsPath+newsPath、issueGroups 六组、 sanitize 清洗统计(事件流 changedRows/byDiffType 聚合)、adapterTiming。 - content-writer:NULL-entry 收编(快照行在而 content_entry 空,五列先验 三分支:无先验新建挂接/同 checksum 挂接 skipped/异 checksum 挂接新版本)。 - route-writer:attachment 降级汇总按去重 sourceId 计(重跑仍必现); normalizeListPath 导出;writeAttachmentRoute 返回 written/resolved/blocked。 - 四家写入器 identity 收敛共享基类型 WriterContext = Pick<ImportJobContext,'db'|'siteId'|'connectionId'>(type-only import, 无运行时环依赖)。 - CLI import 增 --products-path/--news-path(前置事实 9,缺省 /products/ /news/)记入 job 报告。 - 测试:tests/integration/import-full.test.ts 6 例(全链 zh-CN 接线证明 +published 全 NULL/报告四域计数+sanitize+分组/幂等重跑零写语义/ 无 media 快照域缺失不失败/NULL-entry 收编/stale 回收/REPORT_WRITE_FAILED 遗留补测);import-job 契约测试钉死 counts 求和(100⊕3=103)。 全量 323 通过;typecheck/lint 干净。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -50,7 +50,7 @@ import { UNIVERSAL_ROW_VERSION, type UniversalRow } from '@sf/contracts'
|
||||
import type { AdapterEvent } from '@sf/adapter-wp'
|
||||
import { canonicalJson } from '../../common/canonical.js'
|
||||
import { contentEntries, contentRevisions, entryLocales, sourceRecords } from '../../db/schema.js'
|
||||
import type { ImportDb, ImportPipeline, ImportPipelineOutcome, ImportVersionBlock } from './import.service.js'
|
||||
import type { ImportDb, ImportPipeline, ImportPipelineOutcome, ImportVersionBlock, WriterContext } from './import.service.js'
|
||||
|
||||
/** 事务句柄类型(db.transaction 回调参数——从 ImportDb 的事务签名提取,避免重复泛型展开) */
|
||||
type Tx = Parameters<Parameters<ImportDb['transaction']>[0]>[0]
|
||||
@@ -149,18 +149,15 @@ interface ContentWriterCounts {
|
||||
* - contentEntries:新建 content_entries 数(幂等重跑/变化路径为 0);
|
||||
* - revisionsCreated:新建 content_revisions 数(checksum 命中为 0——G2 验收锚点);
|
||||
* - sourceRecords:新建 source_records 数(内容/attachment/unsupported/error 全计;
|
||||
* 跨快照重导按快照补行故可为正);
|
||||
* 跨快照重导按快照补行故可为正;NULL-entry 占位行收编**不计**——行已在,UPDATE
|
||||
* 挂接非新建,见 handleContentRow 态 1b);
|
||||
* - skipped:内容行未产修订的跳过数(同快照 checksum 命中、跨快照无业务变化、locale
|
||||
* 拒收三类;attachment 命中不计——媒体域是 Task 4 主场);
|
||||
* - unsupportedRecords/failedRecords:unsupported/error 事件分项数(**按事件计**非按
|
||||
* 插入行——重跑行幂等而计数如实;拆分只在分派点可知,Task 6 报告层无从重derive)。
|
||||
* issues:locale 不匹配、未知 sourceType 兜底——显式报告(§7.6 禁静默)。
|
||||
*/
|
||||
export function createContentEventHandler(identity: {
|
||||
db: ImportDb
|
||||
siteId: string
|
||||
/** 快照行六列身份的 connection 侧 + 五列内容身份的解析锚点 */
|
||||
connectionId: string
|
||||
export function createContentEventHandler(identity: WriterContext & {
|
||||
/** 本 job 的 wxr 快照 id(六列身份的 snapshot 侧 + 新行 source_snapshot_id) */
|
||||
snapshotId: string
|
||||
/** 本 job id(source_records.import_job_id 与 content_revisions.import_job_id 落库锚点) */
|
||||
@@ -286,8 +283,6 @@ export function createContentEventHandler(identity: {
|
||||
|
||||
// 态 1:本快照行已存在(挂 entry——本写入器是 content 行 source_records 的唯一写入方,
|
||||
// 挂接不变量由写入路径自身保证)。
|
||||
// 已知死角:NULL-entry 占位行(外源 seed/未来写入方)+ 后续映射扩充重导 → 全新建分支
|
||||
// 插入撞六列唯一约束 23505;Task 6 收编(UPDATE 该行挂 entry)——见审查记录
|
||||
const snapRow = await findSnapshotRecord(row)
|
||||
if (snapRow !== null && snapRow.contentEntryId !== null) {
|
||||
if (snapRow.normalizedChecksum === checksum) {
|
||||
@@ -315,6 +310,67 @@ export function createContentEventHandler(identity: {
|
||||
counts.revisionsCreated++
|
||||
return
|
||||
}
|
||||
if (snapRow !== null) {
|
||||
// 态 1b:NULL-entry 占位行收编(Task 6 义务①——Task 3 审查移交的已知死角在此闭合):
|
||||
// 六列命中而 content_entry_id NULL 的行(外源 seed / attachment record-only 足迹 +
|
||||
// 映射扩充后该 sourceType 升格为内容类型重导)→ 走全新建分支会撞六列唯一约束
|
||||
// 23505,改为就地 UPDATE 挂 entry。语义与态 2/3 对齐:
|
||||
// - 无五列先行者 → 全新建变体(entry+revision+locale),占位行 UPDATE 挂接;
|
||||
// - 先行者 checksum 同 → 只挂接(skipped——0 新修订);
|
||||
// - 先行者 checksum 异 → 挂接 + 新修订 + current 前移。
|
||||
// counts:sourceRecords 不计(行已在,收编非新建);lineage 留在占位行建行 job。
|
||||
const prior = await findPriorLinkedRecord(row)
|
||||
const adoptValues = { contentEntryId: null as string | null, normalizedChecksum: checksum, sourceUrl: row.sourceUrl ?? null }
|
||||
if (prior === null) {
|
||||
await db.transaction(async (tx) => {
|
||||
const entry = await tx
|
||||
.insert(contentEntries)
|
||||
.values({ siteId, typeKey, lifecycleStatus: 'active' })
|
||||
.returning({ id: contentEntries.id })
|
||||
const entryId = entry[0]!.id
|
||||
await tx
|
||||
.update(sourceRecords)
|
||||
.set({ ...adoptValues, contentEntryId: entryId })
|
||||
.where(eq(sourceRecords.id, snapRow.id))
|
||||
const revisionId = await insertRevision(tx, {
|
||||
entryId,
|
||||
row,
|
||||
data,
|
||||
checksum,
|
||||
sourceRecordId: snapRow.id,
|
||||
})
|
||||
await pointCurrent(tx, entryId, row, revisionId)
|
||||
})
|
||||
counts.contentEntries++
|
||||
counts.revisionsCreated++
|
||||
return
|
||||
}
|
||||
const entryId = prior.contentEntryId // 事务回调外的 const 收窄(属性收窄不进闭包)
|
||||
if (prior.normalizedChecksum === checksum) {
|
||||
await db
|
||||
.update(sourceRecords)
|
||||
.set({ ...adoptValues, contentEntryId: entryId })
|
||||
.where(eq(sourceRecords.id, snapRow.id))
|
||||
counts.skipped++
|
||||
return
|
||||
}
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(sourceRecords)
|
||||
.set({ ...adoptValues, contentEntryId: entryId })
|
||||
.where(eq(sourceRecords.id, snapRow.id))
|
||||
const revisionId = await insertRevision(tx, {
|
||||
entryId,
|
||||
row,
|
||||
data,
|
||||
checksum,
|
||||
sourceRecordId: snapRow.id,
|
||||
})
|
||||
await pointCurrent(tx, entryId, row, revisionId)
|
||||
})
|
||||
counts.revisionsCreated++
|
||||
return
|
||||
}
|
||||
|
||||
// 态 2/3 前置:内容身份(五列)解析——跨快照复用既有 entry(permalink/快照变化不建新)
|
||||
const prior = await findPriorLinkedRecord(row)
|
||||
|
||||
Reference in New Issue
Block a user