Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
b41acb7c
Commit
b41acb7c
authored
Jul 20, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
merge: synthesizer occurredAt 确定性回退(worktree 会话产出)
parents
c31e0b63
4b2a7b52
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
94 additions
and
4 deletions
+94
-4
apps/pac-service/src/modules/sync/pipeline/transaction-synthesizer.ts
+12
-4
apps/pac-service/tests/transaction-synthesizer-occurred-at.spec.ts
+82
-0
No files found.
apps/pac-service/src/modules/sync/pipeline/transaction-synthesizer.ts
View file @
b41acb7c
...
...
@@ -98,13 +98,21 @@ export class TransactionSynthesizer {
?
input
.
patientIdResolver
(
patientExternalId
)
:
undefined
;
// occurredAt — 从 yaml emits.occurredAtField 指定的字段拿
const
occurredAt
=
this
.
pickOccurredAt
(
c
,
emits
.
occurredAtField
);
// occurredAt — 从 yaml 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
)
{
this
.
logger
.
warn
(
`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 幂等键
...
...
@@ -128,7 +136,7 @@ export class TransactionSynthesizer {
rawPayload
:
input
.
rawRow
as
Prisma
.
InputJsonValue
,
canonicalPayload
:
input
.
canonicalRow
as
Prisma
.
InputJsonValue
,
payloadHash
,
occurredAt
:
occurredAt
??
new
Date
()
,
occurredAt
,
};
}
...
...
apps/pac-service/tests/transaction-synthesizer-occurred-at.spec.ts
0 → 100644
View file @
b41acb7c
/**
* 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
);
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment