chore(import): unsupported/failed 分项计数与键契约 JSDoc;NULL-entry 缝注释;lineage 语义注记

审查 cheap riders 四项:handleUnmappable 分项 unsupportedRecords/
failedRecords(按事件计——拆分只在分派点可知,Task 6 无从重derive,测试
对拍两键);ImportPipelineOutcome.counts 键契约注记(跨写入器扁平全局
命名空间、同名 CLOBBER 不求和、各键归属声明、Task 4 勿复用 'skipped');
六列 NULL-entry 已知死角缝注释(23505 → Task 6 收编);态 1 checksum-diff
的 import_job_id lineage 语义注记(source_records 留建行 job、新 job 记
content_revisions)。296 全绿;typecheck/lint 通过。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-21 18:19:37 +08:00
parent 2b61431da8
commit fe72df3afa
3 changed files with 48 additions and 4 deletions
+38 -4
View File
@@ -127,6 +127,23 @@ export interface ContentEventHandler {
finish(): ImportPipelineOutcome
}
/**
* 本写入器的 counts 键与归属(**counts 键是跨写入器的扁平全局命名空间**——import.service
* 按"返回值覆盖累加器同名键"合并,同名键 CLOBBER 不求和,各写入器必须各持独占键名,见
* ImportPipelineOutcome.counts 的键契约注记)。本写入器独占:contentEntries /
* revisionsCreated / sourceRecords / skipped / unsupportedRecords / failedRecords——
* **Task 4 media-writer 起新键名,勿复用 'skipped'**(语义不同会互相覆盖)。
* 前四键恒出现在报告;unsupportedRecords/failedRecords 只在 >0 时出现(分项按事件计)。
*/
interface ContentWriterCounts {
contentEntries: number
revisionsCreated: number
sourceRecords: number
skipped: number
unsupportedRecords?: number
failedRecords?: number
}
/**
* 内容写入器的事件分派句柄。产出 counts:
* - contentEntries:新建 content_entries 数(幂等重跑/变化路径为 0);
@@ -134,7 +151,9 @@ export interface ContentEventHandler {
* - sourceRecords:新建 source_records 数(内容/attachment/unsupported/error 全计;
* 跨快照重导按快照补行故可为正);
* - skipped:内容行未产修订的跳过数(同快照 checksum 命中、跨快照无业务变化、locale
* 拒收三类;attachment 命中不计——媒体域是 Task 4 主场)。
* 拒收三类;attachment 命中不计——媒体域是 Task 4 主场);
* - unsupportedRecords/failedRecords:unsupported/error 事件分项数(**按事件计**非按
* 插入行——重跑行幂等而计数如实;拆分只在分派点可知,Task 6 报告层无从重derive)。
* issues:locale 不匹配、未知 sourceType 兜底——显式报告(§7.6 禁静默)。
*/
export function createContentEventHandler(identity: {
@@ -152,7 +171,14 @@ export function createContentEventHandler(identity: {
versions: ImportVersionBlock
}): ContentEventHandler {
const { db, siteId } = identity
const counts = { contentEntries: 0, revisionsCreated: 0, sourceRecords: 0, skipped: 0 }
// 交叉 Record:finish() 要把 counts 交还给 ImportPipelineOutcome(Record<string, number>)
// ——接口无索引签名不可赋值;交叉后已知键类型不变、动态赋值兼容
const counts: ContentWriterCounts & Record<string, number> = {
contentEntries: 0,
revisionsCreated: 0,
sourceRecords: 0,
skipped: 0,
}
const issues: string[] = []
/** 修订插入(纯 INSERT——不可变表;data 与 checksum 同构于 mapped_content) */
@@ -259,14 +285,18 @@ export function createContentEventHandler(identity: {
const data = mappedContentOf(row)
// 态 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) {
counts.skipped++
return
}
// checksum 异:快照行前移(checksum/source_url)+ 新修订 + current 前移(事务)
// checksum 异:快照行前移(checksum/source_url)+ 新修订 + current 前移(事务)。
// lineage 语义(有意):source_records.import_job_id 留在建行 job(行的溯源=创建它的
// 导入),本次 job 记在 content_revisions.import_job_id(谁产了新修订)
const entryId = snapRow.contentEntryId // 事务回调外的 const 收窄(属性收窄不进闭包)
await db.transaction(async (tx) => {
await tx
@@ -396,6 +426,10 @@ export function createContentEventHandler(identity: {
): Promise<void> {
// postId 缺失不硬造身份——source_id 以 rawLocation 回退(行可审计、身份不虚构)
const sourceId = ev.postId ?? ev.rawLocation
// 分项计数按事件计(非按插入行):unsupported/failed 的拆分只在分派点可知,
// Task 6 报告层无法从 sourceRecords 总数重derive;重跑时行幂等但计数如实。
if (parseStatus === 'unsupported') counts.unsupportedRecords = (counts.unsupportedRecords ?? 0) + 1
else counts.failedRecords = (counts.failedRecords ?? 0) + 1
const inserted = await db
.insert(sourceRecords)
.values({
@@ -127,6 +127,14 @@ export interface ImportJobContext {
* 无返回值,累加器现值原样进报告(部分产出可审计)。
*/
export interface ImportPipelineOutcome {
/**
* 计数块(原样进报告)。**键契约:counts 键是跨写入器的扁平全局命名空间**——
* 汇合规则按"返回值覆盖累加器同名键"合并(ctx.report.counts ⊕ outcome.counts),
* 同名键 **CLOBBER 不求和**:各写入器必须各持独占键名并在自身 JSDoc 声明归属
* (Task 2 term-writer:termsProcessed/autoCreatedTerms/contentTermsCreated;
* Task 3 content-writer:contentEntries/revisionsCreated/sourceRecords/skipped/
* unsupportedRecords/failedRecords——Task 4 media-writer 起新键名,勿复用 'skipped')。
*/
counts: Record<string, number>
issues: string[]
}