Commit 7ad3aa93 by luoqi

fix(摄入): reparse 源表回溯两处失效 —— 治疗类 reparse 从来没工作过且报成功

验证 加牙 补词时发现:reparse 治疗类恒 0 变更(superseded=0 created=0),
exit 0、日志写"重衍完成"。dry-run 用同一过滤条件能查到流水,实跑却把 0 行
喂给 transform —— 问题在源表回溯。

traceRawSourceTable 实测:
  treatment_actual_rows   → _treatment_actual_raw_emr   ← 中间表
  treatment_planned_rows  → _treatment_planned_raw_emr  ← 中间表
  treatment_review_rows   → _treatment_review_raw       ← 中间表
  diagnosis_rows          → fact_emr_treatment_out      ← 正确

两个独立 bug,只修一个仍失效:
① route_by_pattern 的字段名是 `routes`,代码找的是 `outputs` → 恒 undefined,
   route 从不登记进 byOutput → 凡链路过 route 的资源回溯都停在中间表。
   diagnosis 没踩到只因它的链 split→derive→derive 不过 route。
② `_treat_plan_raw → _treat_plan_raw` 这类**原地 derive** 被登记成指向自己,
   resolve 撞环保护当场返回 → 修完①仍停在 _treat_plan_raw。

后果:rawPayload 被灌进中间表,随即被 transform 链用空结果覆盖 → 0 变更。
守卫 `raw === cfg.primary.table` 判不出来(中间表 ≠ primary 表),所以不跳过、
不报错、报成功。任何治疗类字典/parser 修补都无法回填存量,且无人会发现 ——
本次 加牙/SRP/缝线 补词若不修这个,只对新摄入生效。

既有测试 trace-raw-source.spec.ts 的 fixture 也写的 `outputs` —— 跟实现犯同一个错,
所以一直绿。fixture 已按契约(transforms.schema.ts)改正并留注,新增 8 条断言直接
拿真实 manifest 回溯四个资源。

测试:新增 8 条,全量 1564 条通过;type-check 17 个错为 main 既有,引入 0 个。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent 66ed2e46
......@@ -2476,16 +2476,24 @@ export function traceRawSourceTable(primaryTable: string, transforms: ReadonlyAr
input?: string;
output?: string;
inputs?: string[];
outputs?: Array<{ output?: string }>;
/// ⚠️ route_by_pattern 的字段名是 `routes`(见 transforms.schema.ts RouteByPatternOpSchema),
/// 不是 `outputs` —— 早先这里写成 outputs,恒为 undefined,导致**凡链路经过 route 的资源
/// 回溯都停在中间表**(_treatment_actual_raw_emr 等)。后果:reparse 把 rawPayload 灌进中间表,
/// 随即被 transform 链用空结果覆盖 → 治疗类 reparse 恒 0 变更**且报成功**(exit 0)。
/// diagnosis 没踩到只因它的链 split→derive→derive 不过 route。
routes?: Array<{ output?: string }>;
};
if (t.kind === 'union' && t.output && Array.isArray(t.inputs)) {
unionInputs.set(t.output, t.inputs);
continue;
}
if (t.output && t.input) byOutput.set(t.output, t.input);
// ⚠️ 跳过**原地 derive**(output === input,如 `_treat_plan_raw → _treat_plan_raw` 补 treat_name):
// 它不改变这张表的来源。登记进去会让 byOutput 指向自己 → resolve 撞环保护当场返回,
// 回溯同样停在中间表(与 routes 那条是**两个独立 bug**,只修一个仍然失效)。
if (t.output && t.input && t.output !== t.input) byOutput.set(t.output, t.input);
// route_by_pattern 多 output:每个 output 都回到同一 input
if (Array.isArray(t.outputs) && t.input) {
for (const o of t.outputs) if (o?.output) byOutput.set(o.output, t.input);
if (Array.isArray(t.routes) && t.input) {
for (const o of t.routes) if (o?.output) byOutput.set(o.output, t.input);
}
}
const resolve = (tbl: string, seen: Set<string>): string => {
......
/**
* traceRawSourceTable —— reparse 的源表回溯。
*
* 回溯错了不会报错:rawPayload 被灌进**中间表**,随即被 transform 链用空结果覆盖 →
* reparse 恒 0 变更、exit 0、日志写"重衍完成"。2026-08-27 实测治疗类三个资源全中,
* 意味着任何治疗字典修补都无法回填存量,而且没人会发现。
*
* 跑:
* pnpm test -- trace-raw-source-table
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as yaml from 'js-yaml';
import { traceRawSourceTable } from '../src/modules/sync/cold-import/cold-import.service';
const MANIFEST = path.join(__dirname, '../data/jvs-dw/manifest.yaml');
const manifest = yaml.load(fs.readFileSync(MANIFEST, 'utf8')) as { transforms?: unknown[] };
const TF = manifest.transforms ?? [];
const trace = (t: string) => traceRawSourceTable(t, TF);
describe('traceRawSourceTable · 真实 manifest', () => {
// ⛔ 这四个必须都回溯到真正的 DW 原始表,任何一个停在中间表 = reparse 静默失效
it.each([
'treatment_actual_rows',
'treatment_planned_rows',
'treatment_review_rows',
'diagnosis_rows',
])('%s → fact_emr_treatment_out', (tbl) => {
expect(trace(tbl)).toBe('fact_emr_treatment_out');
});
it('⛔ 不能停在 route 的输出表上(本次 bug 的指纹)', () => {
for (const tbl of ['treatment_actual_rows', 'treatment_planned_rows', 'treatment_review_rows']) {
expect(trace(tbl)).not.toMatch(/^_/); // 下划线开头 = manifest 里的中间表
}
});
it('原始表回溯到自身(reparse 据此判定"非 transform 产出"并跳过)', () => {
expect(trace('fact_emr_treatment_out')).toBe('fact_emr_treatment_out');
});
});
describe('traceRawSourceTable · 合成用例', () => {
it('route_by_pattern 用的是 routes 字段,不是 outputs', () => {
const tf = [
{ kind: 'split_json_array', input: 'raw_src', output: '_mid' },
{ kind: 'route_by_pattern', input: '_mid', field: 'x', routes: [{ output: '_a' }, { output: '_b' }] },
{ kind: 'derive', input: '_a', output: 'final_rows' },
];
expect(traceRawSourceTable('final_rows', tf)).toBe('raw_src');
});
it('union 各分支回溯不一致 → 停在 union 输出(既有语义不变)', () => {
const tf = [
{ kind: 'derive', input: 'raw_a', output: 't1' },
{ kind: 'union', inputs: ['t1', 'raw_b'], output: 'merged' },
];
expect(traceRawSourceTable('merged', tf)).toBe('merged');
});
});
......@@ -17,7 +17,10 @@ const transforms = [
{ kind: 'filter', input: 'patient_settlement_spec', output: '_refund_item_raw', where: {} },
{ kind: 'lookup', input: '_refund_item_raw', output: 'refund_item_rows', from: 'patient_settlement', select: {} },
// route_by_pattern 多输出回同一 input
{ kind: 'route_by_pattern', input: '_treat_raw', outputs: [{ output: '_actual_raw' }, { output: '_rec_raw' }] },
// ⚠️ 2026-08-27 修正:字段名是 `routes`(见 transforms.schema.ts RouteByPatternOpSchema),
// 本 fixture 原先写 `outputs` —— 跟当时的实现犯了同一个错,于是测试一直绿、生产一直坏
// (治疗类三个资源 reparse 恒 0 变更且报成功)。fixture 必须照**契约**写,不能照实现写。
{ kind: 'route_by_pattern', input: '_treat_raw', routes: [{ output: '_actual_raw' }, { output: '_rec_raw' }] },
];
describe('traceRawSourceTable', () => {
......
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