Commit e679ec2e by luoqi

fix(friday): 患者关系边方向反了 —— enum_mapping 去掉多余的逆映射

patient_relation.yaml 的注释断言「码 = 本人(customer)是对方(referee)的 X」(依据
RecommendRelationshipEnum.reverseValue() 源码 + 早期年龄差推断),于是 enum_mapping
整体取逆:码2 爸爸→child、码3 子女→father/mother、码5 爷爷→grandchild……
数据把这个假设推翻了:码本来就是「对方是本人的 X」,与 PAC 契约同向,不该反转。

【证据一:PAC 侧方向矛盾率】按「关系人比本人年长/年轻是否合理」判定,只算两边都建档
且都有生日的边:
  friday   1187/1255 = 94.6%   ← 系统性反转
  jvs-dw    432/10842 =  4.0%   ← 人工零星录错的正常水平(其 yaml 是对的,不动)
互反对更硬:motherchild 326 对里只有 8 对方向正确(2.5%);fatherchild 231 对里 9 对。
实样:秦佳(47) --mother--> 韩秦瑜(21),同时 韩秦瑜(21) --child--> 秦佳(47)
—— 两条边互相自洽但都与年龄矛盾,去掉逆映射两条就都对了。

【证据二:源库独立复核(不经 PAC 摄入)】join customer_basic_info 两侧 birthday:
  码10 妈妈  关系人年长 419/430(97%),平均大 27.4 岁
  码 3 子女  本人年长   643/675(95%),平均小 26.8 岁
  码 2 爸爸  关系人年长 252/277(91%),平均大 26.1 岁
  码15 外公  关系人年长  20/22 (91%)  |  码11 奶奶 17/19(89%)  |  码16 外婆 15/17(88%)
  码17 外孙  本人年长    31/36 (86%)  |  码 6 孙辈 32/46(70%)
方法学对照:对称码(1配偶 348/354、4兄弟 56/55、7朋友 265/264)全是 50/50,说明这个
年龄判据本身干净,有方向码上的强烈偏斜不是判据偏差。
剩余 3–5% 符合逆读法的边判为源侧人工录错(与 jvs-dw 的 4% 同量级),不为它们反转全局。

顺手修掉一个静默 bug:旧版码3 只配了 "3|1"/"3|2",没有 "3|" —— 对方性别缺失的边
(源库 15 条)会掉进 _default: other。本版每个码的 |1 / |2 / | 三个变体都列全。

同向映射后性别不再参与判定(码2 自带 father、码10 自带 mother、码3→child 不分性别),
transforms 里的 _referee_with_sex + push_fallback 成了死配置 —— 留待单独一次清理,
不跟方向修正混在一起改。

回归闸 tests/friday-relation-direction.spec.ts:不比对字符串字面量,而是用源库实测的
辈分方向锁语义(长辈码不得映射成晚辈词,反之亦然)。已验证该闸对旧 yaml 报 21 项失败。

️ 未含数据修复:patient_relations 唯一键是 (patient_id, related_external_id,
relationship),relationship 变了就是新行而非更新 —— 重摄只会在错边旁边新增对边。
必须先删 friday 存量关系边再重摄。该步骤待部署后单独执行。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent f23bfefe
Pipeline #3493 failed in 0 seconds
......@@ -6,11 +6,35 @@
# friday-saas/customer …/enums/RecommendRelationshipEnum.java:
# 1配偶 2爸爸 3子女 4兄弟 5爷爷 6孙子女 7朋友 8其他亲属 9姐妹 10妈妈 11奶奶
# 12姐弟 13兄妹 14其他 15外公 16外婆 17外孙子女
# ⚠️ 语义方向(该类 reverseValue() + 年龄差数据双证):存储的码 = **本人(customer)是
# 推荐人(referee)的 X**。PAC patient_relation 契约要"对方是本人的 X" → 此处映射 = 逆关系:
# 码2(本人是对方爸爸)→对方是本人的 child;码3(本人是对方子女)→对方是本人的父/母
# (按对方性别拆,transforms 已 lookup 对方 sex 拼 "码|性别" 键);余类推。
# (早期统计推断大multi命中,唯一错:码8 其他亲属曾误标 friend,本版修正为 other。)
#
# ── 语义方向:码 = **对方(referee)是本人(customer)的 X**,与 PAC 契约同向,直译不取逆 ──
# 2026-07-30 源库全量实测(join customer_basic_info 两侧 birthday,只算两边都有生日的边):
# 码10 妈妈 → 关系人年长 419/430(97%),平均大 27.4 岁
# 码 3 子女 → 本人年长 643/675(95%),平均小 26.8 岁
# 码 2 爸爸 → 关系人年长 252/277(91%),平均大 26.1 岁
# 码15 外公 → 关系人年长 20/22 (91%),平均大 38.9 岁
# 码11 奶奶 → 关系人年长 17/19 (89%),平均大 40.9 岁
# 码16 外婆 → 关系人年长 15/17 (88%),平均大 40.0 岁
# 码17 外孙女→ 本人年长 31/36 (86%),平均小 38.5 岁
# 码 6 孙子女→ 本人年长 32/46 (70%)
# 方法学对照:对称码(1配偶 348/354、4兄弟 56/55、7朋友 265/264)全是 50/50 —— 说明这个
# 年龄判据本身干净,有方向的码上的强烈偏斜不是判据偏差。
#
# ⚠️ 本版修正的历史错误:此前注释称「码 = 本人是对方的 X」(依据 reverseValue() 源码 +
# 早期年龄差推断),于是 enum_mapping 全部取逆(码2→child、码3→father/mother、
# 码5→grandchild…)。实测把这个假设推翻了:PAC 侧 friday 父母/子女/祖孙边的方向矛盾率
# 高达 94.6%(1187/1255),而同口径走 cold-import 的 jvs-dw 只有 4.0%(432/10842,人工零星
# 录错水平)。互反对更硬:mother↔child 326 对里只有 8 对方向正确(2.5%)。
# 实样:秦佳(47) --mother--> 韩秦瑜(21) —— 47 岁的人有个 21 岁的「妈妈」。
# 剩余 3–5% 符合逆读法的边判为源侧人工录错(与 jvs-dw 的 4% 同量级),不为它们反转全局。
#
# 【为什么不再需要 rel_sex_key 的性别拆分】
# 逆映射时码3(子女)要按对方性别拆成 father/mother,才需要 lookup 对方 sex。同向之后
# 码2→father、码10→mother **本身就带性别**,码3→child 不分性别 —— 性别彻底用不上了。
# transforms 里的 _referee_with_sex(含 push_fallback)因此变成死配置,留待单独一次清理,
# 不跟本次方向修正混在一起改。这里仍按 "码|性别" 建键,但每个码的 |1 / |2 / |(性别缺失)
# 三个变体都显式列全 —— 旧版码3 只有 "3|1"/"3|2" 没有 "3|",对方性别缺失的边(源库 15 条)
# 会静默掉进 _default: other,顺手一并修掉。
canonical: patient_relation
primary:
......@@ -20,7 +44,7 @@ primary:
field_mapping:
patientExternalId: customer_id
relatedExternalId: referee_patient_id
relationship: rel_sex_key # "码|对方性别" → 下方 enum_mapping 逆向归一
relationship: rel_sex_key # "码|对方性别";同向直译,性别不参与判定(见上方说明)
enum_mapping:
relationship:
......@@ -28,16 +52,17 @@ enum_mapping:
"1|1": spouse
"1|2": spouse
"1|": spouse
# 2 本人是对方爸爸 / 10 妈妈 → 对方是本人的 child
"2|1": child
"2|2": child
"2|": child
"10|1": child
"10|2": child
"10|": child
# 3 本人是对方子女 → 对方是本人的父/母(按对方性别;性别缺 → other 不硬猜)
"3|1": father
"3|2": mother
# 2 爸爸 → 对方是本人的 father;10 妈妈 → mother(码本身带性别)
"2|1": father
"2|2": father
"2|": father
"10|1": mother
"10|2": mother
"10|": mother
# 3 子女 → 对方是本人的 child(不分性别)
"3|1": child
"3|2": child
"3|": child
# 4 兄弟 / 9 姐妹 / 12 姐弟 / 13 兄妹 → sibling(对称)
"4|1": sibling
"4|2": sibling
......@@ -51,26 +76,26 @@ enum_mapping:
"13|1": sibling
"13|2": sibling
"13|": sibling
# 5 爷爷 / 11 奶奶 / 15 外公 / 16 外婆(本人是对方祖辈)→ 对方是本人的 grandchild
"5|1": grandchild
"5|2": grandchild
"5|": grandchild
"11|1": grandchild
"11|2": grandchild
"11|": grandchild
"15|1": grandchild
"15|2": grandchild
"15|": grandchild
"16|1": grandchild
"16|2": grandchild
"16|": grandchild
# 6 孙子女 / 17 外孙子女(本人是对方孙辈)→ 对方是本人的 grandparent
"6|1": grandparent
"6|2": grandparent
"6|": grandparent
"17|1": grandparent
"17|2": grandparent
"17|": grandparent
# 5 爷爷 / 11 奶奶 / 15 外公 / 16 外婆 → 对方是本人的 grandparent
"5|1": grandparent
"5|2": grandparent
"5|": grandparent
"11|1": grandparent
"11|2": grandparent
"11|": grandparent
"15|1": grandparent
"15|2": grandparent
"15|": grandparent
"16|1": grandparent
"16|2": grandparent
"16|": grandparent
# 6 孙子女 / 17 外孙子女 → 对方是本人的 grandchild
"6|1": grandchild
"6|2": grandchild
"6|": grandchild
"17|1": grandchild
"17|2": grandchild
"17|": grandchild
# 7 朋友
"7|1": friend
"7|2": friend
......
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import * as yaml from 'js-yaml';
/**
* FRIDAY 患者关系边的**方向**必须与 PAC 契约同向:码 = 「对方(referee)是本人(customer)的 X」。
*
* 踩过的坑(2026-07-20 引入 → 07-30 发现):
* assembler 注释曾断言「码 = 本人是对方的 X」(依据 RecommendRelationshipEnum.reverseValue()
* 源码 + 早期年龄差推断),于是 enum_mapping 整体取逆:码2 爸爸→child、码3 子女→father/mother、
* 码5 爷爷→grandchild…… 结果 FRIDAY 的父母/子女/祖孙关系方向**系统性反转**:
* PAC 侧方向矛盾率 friday 94.6%(1187/1255) vs jvs-dw 4.0%(432/10842,人工零星录错)
* 互反对:mother↔child 326 对里只有 8 对方向正确(2.5%)
* 实样:秦佳(47) --mother--> 韩秦瑜(21) —— 47 岁的人有个 21 岁的「妈妈」
*
* 本测用**源库实测的年龄方向**作为判据来锁语义,而不是比对字符串字面量 ——
* 这样将来谁再把映射取逆,都会在这里被抓住,而不是等有人在工作台上看见一个 21 岁的妈妈。
*
* 源库全量实测(2026-07-30,join customer_basic_info 两侧 birthday):
* 码10 妈妈 关系人年长 419/430(97%) | 码3 子女 本人年长 643/675(95%)
* 码 2 爸爸 关系人年长 252/277(91%) | 码15 外公 关系人年长 20/22(91%)
* 码11 奶奶 关系人年长 17/19(89%) | 码16 外婆 关系人年长 15/17(88%)
* 码17 外孙 本人年长 31/36(86%) | 码 6 孙辈 本人年长 32/46(70%)
* 对照(方法学自检):对称码 1配偶 348/354、4兄弟 56/55、7朋友 265/264 全是 50/50。
*/
describe('FRIDAY 关系边方向(与 PAC 契约同向)', () => {
const yamlPath = join(
__dirname,
'../data/friday/assemblers/patient_relation.yaml',
);
const cfg = yaml.load(readFileSync(yamlPath, 'utf8')) as {
enum_mapping: { relationship: Record<string, string> };
};
const map = cfg.enum_mapping.relationship;
/** 取某个源码在 enum_mapping 里的归一结果(性别变体应一致) */
const resolve = (code: number, sex: '1' | '2' | '' = ''): string =>
map[`${code}|${sex}`] ?? map['_default']!;
// 源枚举:1配偶 2爸爸 3子女 4兄弟 5爷爷 6孙子女 7朋友 8其他亲属 9姐妹 10妈妈
// 11奶奶 12姐弟 13兄妹 14其他 15外公 16外婆 17外孙子女
/** 该码描述的「对方」相对本人的辈分:older = 长辈,younger = 晚辈,peer = 同辈/无方向 */
const EXPECT: Array<[number, string, string, 'older' | 'younger' | 'peer']> = [
[1, '配偶', 'spouse', 'peer'],
[2, '爸爸', 'father', 'older'],
[3, '子女', 'child', 'younger'],
[4, '兄弟', 'sibling', 'peer'],
[5, '爷爷', 'grandparent', 'older'],
[6, '孙子女', 'grandchild', 'younger'],
[7, '朋友', 'friend', 'peer'],
[9, '姐妹', 'sibling', 'peer'],
[10, '妈妈', 'mother', 'older'],
[11, '奶奶', 'grandparent', 'older'],
[12, '姐弟', 'sibling', 'peer'],
[13, '兄妹', 'sibling', 'peer'],
[15, '外公', 'grandparent', 'older'],
[16, '外婆', 'grandparent', 'older'],
[17, '外孙子女', 'grandchild', 'younger'],
];
/** PAC 关系词各自蕴含的辈分方向 */
const DIRECTION: Record<string, 'older' | 'younger' | 'peer'> = {
father: 'older',
mother: 'older',
grandparent: 'older',
child: 'younger',
grandchild: 'younger',
spouse: 'peer',
sibling: 'peer',
friend: 'peer',
other: 'peer',
};
test.each(EXPECT)('码%i(%s)→ %s', (code, _cn, expected) => {
expect(resolve(code)).toBe(expected);
});
test.each(EXPECT)(
'⭐ 码%i(%s)的归一结果辈分方向必须是 %s ——(源库年龄实测)',
(code, _cn, _expected, dir) => {
expect(DIRECTION[resolve(code)]).toBe(dir);
},
);
test('⭐ 长辈码不得映射成晚辈词(逆映射回归闸)', () => {
// 这四类是当年被取逆的重灾区:爸爸/妈妈/祖辈曾被映射成 child/grandchild
for (const code of [2, 10, 5, 11, 15, 16]) {
expect(['child', 'grandchild']).not.toContain(resolve(code));
}
// 反向:晚辈码不得映射成长辈词
for (const code of [3, 6, 17]) {
expect(['father', 'mother', 'grandparent']).not.toContain(resolve(code));
}
});
test('每个码的性别变体(|1 / |2 / 缺失)结果一致 —— 同向后性别不参与判定', () => {
for (const [code] of EXPECT) {
const [a, b, c] = [resolve(code, '1'), resolve(code, '2'), resolve(code, '')];
expect(`码${code}: ${a}/${b}/${c}`).toBe(`码${code}: ${a}/${a}/${a}`);
}
});
test('性别缺失变体必须显式配置,不能掉进 _default —— 旧版码3 少 "3|" 就静默成了 other', () => {
const missing = EXPECT.map(([c]) => c).filter((c) => map[`${c}|`] === undefined);
expect(missing).toEqual([]);
});
test('码8 其他亲属 / 码14 其他 / 未知码 → other', () => {
expect(resolve(8)).toBe('other');
expect(resolve(14)).toBe('other');
expect(resolve(99)).toBe('other');
});
});
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