Commit b41acb7c by luoqi

merge: synthesizer occurredAt 确定性回退(worktree 会话产出)

parents c31e0b63 4b2a7b52
...@@ -98,13 +98,21 @@ export class TransactionSynthesizer { ...@@ -98,13 +98,21 @@ export class TransactionSynthesizer {
? input.patientIdResolver(patientExternalId) ? input.patientIdResolver(patientExternalId)
: undefined; : undefined;
// occurredAt — 从 yaml emits.occurredAtField 指定的字段拿 // occurredAt — 从 yaml emits.occurredAtField 指定的字段拿;
const occurredAt = this.pickOccurredAt(c, emits.occurredAtField); // 缺失/不可解析时确定性回退 updatedAt → createdAt(与 sourceEventId 的 updatedAtMark 同序)。
// 墙钟只在三者皆缺时兜底:否则同一源行每次摄入 occurredAt 抖动,
// 下游 fact 时间锚变更检测会产生伪 supersede 版本。
let occurredAt = this.pickOccurredAt(c, emits.occurredAtField);
if (!occurredAt) {
occurredAt =
this.pickOccurredAt(c, 'updatedAt') ?? this.pickOccurredAt(c, 'createdAt');
}
if (!occurredAt) { if (!occurredAt) {
this.logger.warn( this.logger.warn(
`Cannot determine occurredAt from field="${emits.occurredAtField}" ` + `Cannot determine occurredAt from field="${emits.occurredAtField}" ` +
`for ${input.resource} externalId=${subjectId};using now()`, `(updatedAt/createdAt 亦缺)for ${input.resource} externalId=${subjectId};using now()`,
); );
occurredAt = new Date();
} }
// sourceEventId 幂等键 // sourceEventId 幂等键
...@@ -128,7 +136,7 @@ export class TransactionSynthesizer { ...@@ -128,7 +136,7 @@ export class TransactionSynthesizer {
rawPayload: input.rawRow as Prisma.InputJsonValue, rawPayload: input.rawRow as Prisma.InputJsonValue,
canonicalPayload: input.canonicalRow as Prisma.InputJsonValue, canonicalPayload: input.canonicalRow as Prisma.InputJsonValue,
payloadHash, payloadHash,
occurredAt: occurredAt ?? new Date(), occurredAt,
}; };
} }
......
/**
* TransactionSynthesizer occurredAt 三级确定性回退单元测试。
*
* 坐实:emits.occurredAtField 缺失/不可解析时,回退 updatedAt → createdAt,
* 仅三者皆缺才落墙钟。否则同一源行(新 sourceEventId、时间字段为空)每次摄入
* 得到不同 occurredAt,下游 fact 时间锚变更检测会产生伪 supersede 版本。
*/
import { TransactionSynthesizer } from '../src/modules/sync/pipeline/transaction-synthesizer';
import type { EmitsConfig } from '../src/modules/sync/assembler/assembler.schema';
describe('synthesizer | occurredAt 确定性回退', () => {
const synth = new TransactionSynthesizer();
const emits: EmitsConfig = {
action: 'appointment_created',
subjectType: 'appointment',
occurredAtField: 'occurredAt',
};
const make = (canonicalRow: Record<string, unknown>) => ({
rawRow: {},
emits,
resource: 'appointments',
hostId: 'h1',
tenantId: 'grp_001',
patientIdResolver: () => undefined,
sourceContext: 'test:occurred-at',
sourceUnit: '',
canonicalRow: { externalId: 'apt-1', clinicId: 'C001', ...canonicalRow },
});
test('occurredAtField 有值 → 直接用,不看 updatedAt/createdAt', () => {
const tx = synth.synthesize(
make({
occurredAt: '2026-01-01T10:00:00+08:00',
updatedAt: '2026-02-01T00:00:00+08:00',
createdAt: '2026-03-01T00:00:00+08:00',
}),
);
expect(tx!.occurredAt).toEqual(new Date('2026-01-01T10:00:00+08:00'));
});
test('occurredAtField 缺失 → 回退 updatedAt', () => {
const tx = synth.synthesize(
make({
updatedAt: '2026-02-01T00:00:00+08:00',
createdAt: '2026-03-01T00:00:00+08:00',
}),
);
expect(tx!.occurredAt).toEqual(new Date('2026-02-01T00:00:00+08:00'));
});
test('occurredAtField 不可解析 → 回退 updatedAt', () => {
const tx = synth.synthesize(
make({
occurredAt: 'not-a-date',
updatedAt: '2026-02-01T00:00:00+08:00',
}),
);
expect(tx!.occurredAt).toEqual(new Date('2026-02-01T00:00:00+08:00'));
});
test('occurredAtField/updatedAt 皆缺 → 回退 createdAt', () => {
const tx = synth.synthesize(make({ createdAt: '2026-03-01T00:00:00+08:00' }));
expect(tx!.occurredAt).toEqual(new Date('2026-03-01T00:00:00+08:00'));
});
test('时间字段有值 → 重复摄入 occurredAt 逐字节一致(确定性,不受墙钟影响)', () => {
const row = { updatedAt: '2026-02-01T00:00:00+08:00' };
const a = synth.synthesize(make(row));
const b = synth.synthesize(make(row));
expect(a!.occurredAt).toEqual(b!.occurredAt);
});
test('三者皆缺 → 落墙钟兜底(列非空,不能是 null)', () => {
const before = Date.now();
const tx = synth.synthesize(make({}));
const after = Date.now();
expect(tx!.occurredAt).toBeInstanceOf(Date);
const t = (tx!.occurredAt as Date).getTime();
expect(t).toBeGreaterThanOrEqual(before);
expect(t).toBeLessThanOrEqual(after);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment