[B1][S-P0-2] 五注册表单源收敛 + 启动自检 #210

Closed
opened 2026-10-07 19:13:04 +08:00 by tanzk · 2 comments
Owner

批次:B1(#199 v0.7 §7.1)。依据:来源 S 报告 §4.1/§7.1;台账 S-P0-2;五注册表人工同步点 data_source_registry:77 / ingest_service:138 / _runner UPSERT_DISPATCH / metric_registry:381。

验收标准

  • 注册表映射收敛为单一来源(或生成/显式汇聚 + 单一事实定义),四处消费点引用同一处
  • 启动自检:注册表之间引用完整性在应用启动时校验(缺映射显式 fail-fast,禁止静默失配)
  • 自检可测试:构造缺映射用例验证报错
  • pytest + ruff 全绿;不改变现有采集行为(纯收敛,数字前后等价)

不做项

  • 不拆 today.py(S-P0-3 另批);不做多 worker(立此存照)
**批次**:B1(#199 v0.7 §7.1)。**依据**:来源 S 报告 §4.1/§7.1;台账 S-P0-2;五注册表人工同步点 data_source_registry:77 / ingest_service:138 / _runner UPSERT_DISPATCH / metric_registry:381。 ## 验收标准 - [x] 注册表映射收敛为单一来源(或生成/显式汇聚 + 单一事实定义),四处消费点引用同一处 - [x] 启动自检:注册表之间引用完整性在应用启动时校验(缺映射显式 fail-fast,禁止静默失配) - [x] 自检可测试:构造缺映射用例验证报错 - [x] pytest + ruff 全绿;不改变现有采集行为(纯收敛,数字前后等价) ## 不做项 - 不拆 today.py(S-P0-3 另批);不做多 worker(立此存照)
Author
Owner

实施前现状核查 + 收敛设计(待评审,未改码,2026-10-07)

按 S-P0-2「纯收敛、采集数字前后等价」要求,先用只读脚本实测了各注册表的键空间(不猜)。结论:这不是「同一键集合抄五份」,不能做朴素的全键全等校验,否则会立刻报出一堆「看似失配、实为有意」的项。

一、注册表实测清单(n=键数)

注册表 位置 键→值 n
DATA_TYPE_REGISTRY ingest_service.py:138(_TABLE_TYPES:119) data_type→{channel, dedup_key, label} 18
UPSERT_DISPATCH upsert/_runner.py:80 data_type→upsert 函数 18
DATA_SOURCE_SEEDS data_source_registry.py:77 code→{model,name_cn,category,sort_order,sync_source,internal…} 21(+PROJECTIONS 1)
_MODEL_REGISTRY analysis_engine.py:35(消费 analysis_views.py:279 / analysis_engine.py:165) table_name→model 类 13
指标注册表 工单写 metric_registry:381,但仓内无 metric_registry.py 同名文件;指标相关散在 analytics/today.py、api/v1/metrics.py、models/metric.py、alerts.py — 待实施前先定位/界定

二、客观差集(脚本实测,ENV=test)

  1. DATA_TYPE_REGISTRY(18) 与 UPSERT_DISPATCH(18):双向差集都为空——当前完全同步(纯人工双写,暂无失配)。
  2. SEEDS.code 与 model.__tablename__:18/21 不一致(code=逻辑名单数 refund/wallet/ticket,物理表名=refunds/wallet_withdrawals/service_tickets)。故 code ≠ table_name 是常态,跨表对照必须以 model.tablename 为桥,不能拿 code 直接 join。
  3. _MODEL_REGISTRY(13) 是 SEEDS 物理表名的严格子集(反向差集空);SEEDS 有、MODEL_REGISTRY 无的 8 张:bms_orders, device_flow_usage, device_tag_rel, device_tags, manual_refunds, return_trackings, scrm_operators, shanmao_refunds(疑似「不进分析视图」的有意子集,待裁定)。
  4. 采集 data_type 中不在 SEEDS.code:device_profile、merchant(能入库但数据查看树无注册——缺口 or 有意不展示,待裁定)。
  5. SEEDS 非 internal 但无 UPSERT_DISPATCH 映射:scrm_operator/scrm_session/scrm_session_transfer(走 scrm_sync 专用通道,非 ingest_table)。另有 2 个 internal(cash_flow_wide/refund_wide,系统物化)。

三、根因(为什么会「五处」)

其实是 3 条入库通道 × 2 个消费子集:

  • 通道:① 通用表格 ingest(DATA_TYPE_REGISTRY+DISPATCH,18)② 投诉/手动退款专用 channel(注册在同一 REGISTRY,channel 字段区分)③ SCRM 专用同步(scrm_sync,绕开 ingest);
  • 子集:展示树 SEEDS(21,含 internal 宽表/视图 + SCRM)、分析引擎 _MODEL_REGISTRY(13,可分析表子集)。
    各自的键空间本来就不该相等——缺的是一处「某 code 参与哪些通道/是否可分析/是否 internal」的能力位声明与启动交叉校验。

四、推荐方案(两阶段,降风险)

  • 阶段 A(本单核心,低风险)—— 建一份「能力位主表」+ 启动引用完整性自检(不改任何采集/分析函数体):
    新增 app/services/registry_index.py(显式汇聚,符合验收「显式汇聚 + 单一事实定义」),以 SEEDS 为锚,为每个 code 声明:code, table_name(=model.__tablename__), ingest(data_type 或 None), channel, upsert(是否在 DISPATCH), analyzable(bool), internal(bool)。
    启动(create_app/lifespan)跑 validate_registry_integrity() fail-fast,校验:
    (a) DATA_TYPE_REGISTRY.keys == UPSERT_DISPATCH.keys(当前已等,防未来漂移);
    (b) 每个声明 ingest=table 的 code 必在 DISPATCH;
    (c) 每个 analyzable=True 的表名必在 _MODEL_REGISTRY,反之 MODEL_REGISTRY 每个表名能在主表找到;
    (d) SEEDS 每项 code/table_name/model 可解析且 name_cn 唯一(复用既有命名规范约束)。
    现状的 SCRM/internal/分析子集通过能力位显式登记为例外,使自检零误报。
  • 阶段 B(可另单)—— 让 _TABLE_TYPES/_MODEL_REGISTRY 从主表派生,彻底消除人工双写;行为零变更、数字等价,靠全量回归兜底。

五、需要人工/评审裁定的 5 点(实施前请拍板)

  1. device_profile、merchant 能入库但查看树无注册——补登记还是确认为有意不展示?
  2. _MODEL_REGISTRY 的 13 张是否确为「可分析表」的有意全集?8 张不纳入的依据写在哪?
  3. SCRM 三表通道命名(scrm)与能力位 schema 是否照此登记?
  4. 指标体系(原称 metric_registry:381)实际在哪个模块、是否纳入本单的自检范围?
  5. 阶段 B(真派生去双写)是否并入本单,还是本单只交付阶段 A(汇聚+自检)?

测试:validate 函数纯输入、构造「DISPATCH 缺映射 / MODEL_REGISTRY 多表 / name_cn 重复」三类用例断言抛错(验收可测);不改采集路径,全量 pytest + ruff 保证数字等价。

倾向:先做阶段 A(满足本单验收且零采集行为变更),待上述 5 点裁定后再开阶段 B。请评审。

## 实施前现状核查 + 收敛设计(待评审,未改码,2026-10-07) 按 S-P0-2「纯收敛、采集数字前后等价」要求,先用只读脚本实测了各注册表的键空间(不猜)。结论:**这不是「同一键集合抄五份」,不能做朴素的全键全等校验**,否则会立刻报出一堆「看似失配、实为有意」的项。 ### 一、注册表实测清单(n=键数) | 注册表 | 位置 | 键→值 | n | |---|---|---|---| | DATA_TYPE_REGISTRY | ingest_service.py:138(_TABLE_TYPES:119) | data_type→{channel, dedup_key, label} | 18 | | UPSERT_DISPATCH | upsert/_runner.py:80 | data_type→upsert 函数 | 18 | | DATA_SOURCE_SEEDS | data_source_registry.py:77 | code→{model,name_cn,category,sort_order,sync_source,internal…} | 21(+PROJECTIONS 1) | | _MODEL_REGISTRY | analysis_engine.py:35(消费 analysis_views.py:279 / analysis_engine.py:165) | **table_name→model 类** | 13 | | 指标注册表 | 工单写 metric_registry:381,但仓内**无 metric_registry.py 同名文件**;指标相关散在 analytics/today.py、api/v1/metrics.py、models/metric.py、alerts.py | — | 待实施前先定位/界定 | ### 二、客观差集(脚本实测,ENV=test) 1. `DATA_TYPE_REGISTRY`(18) 与 `UPSERT_DISPATCH`(18):**双向差集都为空**——当前完全同步(纯人工双写,暂无失配)。 2. `SEEDS.code` 与 `model.__tablename__`:**18/21 不一致**(code=逻辑名单数 `refund/wallet/ticket`,物理表名=`refunds/wallet_withdrawals/service_tickets`)。故 code ≠ table_name 是常态,跨表对照必须以 model.__tablename__ 为桥,不能拿 code 直接 join。 3. `_MODEL_REGISTRY`(13) 是 SEEDS 物理表名的**严格子集**(反向差集空);SEEDS 有、MODEL_REGISTRY 无的 8 张:`bms_orders, device_flow_usage, device_tag_rel, device_tags, manual_refunds, return_trackings, scrm_operators, shanmao_refunds`(疑似「不进分析视图」的有意子集,待裁定)。 4. 采集 data_type 中**不在 SEEDS.code**:`device_profile`、`merchant`(能入库但数据查看树无注册——缺口 or 有意不展示,待裁定)。 5. SEEDS 非 internal 但**无 UPSERT_DISPATCH 映射**:`scrm_operator/scrm_session/scrm_session_transfer`(走 scrm_sync 专用通道,非 ingest_table)。另有 2 个 internal(cash_flow_wide/refund_wide,系统物化)。 ### 三、根因(为什么会「五处」) 其实是 **3 条入库通道 × 2 个消费子集**: - 通道:① 通用表格 ingest(DATA_TYPE_REGISTRY+DISPATCH,18)② 投诉/手动退款专用 channel(注册在同一 REGISTRY,channel 字段区分)③ SCRM 专用同步(scrm_sync,绕开 ingest); - 子集:展示树 SEEDS(21,含 internal 宽表/视图 + SCRM)、分析引擎 _MODEL_REGISTRY(13,可分析表子集)。 各自的键空间本来就不该相等——缺的是一处「**某 code 参与哪些通道/是否可分析/是否 internal**」的能力位声明与启动交叉校验。 ### 四、推荐方案(两阶段,降风险) - **阶段 A(本单核心,低风险)—— 建一份「能力位主表」+ 启动引用完整性自检(不改任何采集/分析函数体)**: 新增 `app/services/registry_index.py`(显式汇聚,符合验收「显式汇聚 + 单一事实定义」),以 SEEDS 为锚,为每个 code 声明:`code, table_name(=model.__tablename__), ingest(data_type 或 None), channel, upsert(是否在 DISPATCH), analyzable(bool), internal(bool)`。 启动(create_app/lifespan)跑 `validate_registry_integrity()` fail-fast,校验: (a) DATA_TYPE_REGISTRY.keys == UPSERT_DISPATCH.keys(当前已等,防未来漂移); (b) 每个声明 `ingest=table` 的 code 必在 DISPATCH; (c) 每个 `analyzable=True` 的表名必在 _MODEL_REGISTRY,反之 MODEL_REGISTRY 每个表名能在主表找到; (d) SEEDS 每项 code/table_name/model 可解析且 name_cn 唯一(复用既有命名规范约束)。 现状的 SCRM/internal/分析子集通过能力位**显式登记为例外**,使自检零误报。 - **阶段 B(可另单)—— 让 _TABLE_TYPES/_MODEL_REGISTRY 从主表派生**,彻底消除人工双写;行为零变更、数字等价,靠全量回归兜底。 ### 五、需要人工/评审裁定的 5 点(实施前请拍板) 1. `device_profile`、`merchant` 能入库但查看树无注册——**补登记**还是确认为有意不展示? 2. _MODEL_REGISTRY 的 13 张是否确为「可分析表」的**有意**全集?8 张不纳入的依据写在哪? 3. SCRM 三表通道命名(`scrm`)与能力位 schema 是否照此登记? 4. 指标体系(原称 metric_registry:381)实际在哪个模块、是否纳入本单的自检范围? 5. 阶段 B(真派生去双写)是否并入本单,还是本单只交付阶段 A(汇聚+自检)? **测试**:validate 函数纯输入、构造「DISPATCH 缺映射 / MODEL_REGISTRY 多表 / name_cn 重复」三类用例断言抛错(验收可测);不改采集路径,全量 pytest + ruff 保证数字等价。 倾向:先做**阶段 A**(满足本单验收且零采集行为变更),待上述 5 点裁定后再开阶段 B。请评审。
Author
Owner

关闭凭证核验 (2026-10-08)

验收标准 核验方式(可复现) 结果
注册表映射收敛为单一来源(显式汇聚 + 单一事实定义),四处消费点引用同一处 新增 backend/app/services/registry_index.py:build_registry_index() 从 DATA_TYPE_REGISTRY(18)/UPSERT_DISPATCH(18)/DATA_SOURCE_SEEDS(21+1投影)/_MODEL_REGISTRY(13) 动态汇聚能力位主表(非第四份硬编码);例外语义唯一来源 CAPABILITY_NOTES(14 条登记)。commit 21f16dd ✅
启动自检:引用完整性 fail-fast,禁止静默失配 main.py create_app() 顶部 validate_registry_integrity()(纯内存校验,不依赖 DB);五类违例汇总抛 RegistryIntegrityError:①REGISTRY↔DISPATCH 键集漂移 ②采集通道缺 dispatch ③_MODEL_REGISTRY 双向漂移 ④SEEDS model 不可解析/name_cn 重名 ⑤失配无登记/冗余登记 ✅
自检可测试:构造缺映射用例验证报错 tests/test_registry_index_210.py 9 例:现状零误报 + monkeypatch 构造 DISPATCH 缺映射/多映射/_MODEL_REGISTRY 漂移/name_cn 重名/无登记失配/冗余登记六类 fail-fast,pytest tests/test_registry_index_210.py -q 9 passed ✅
pytest + ruff 全绿;不改变现有采集行为(纯收敛) 全量 pytest tests/ -q 2128 passed exit=0;ruff check app/ tests/ 全绿;仅新增校验层,四注册表本体与采集链路零改动(git show 21f16dd --stat = 3 files, +307/-0) ✅

不做项核实:未拆 today.py(S-P0-3 另批);未做多 worker。

范围说明:本单落地阶段 A(汇聚 + fail-fast 自检)。阶段 B(注册表派生去双写)与「指标体系是否纳入第五注册表」界定另开后续单挂 #199。裁定记录(2026-10-07 评论五点)已逐项落实:device_profile/merchant 登记「有意不上查看树」、_MODEL_REGISTRY 13 张为有意全集(8 张 analyzable=False 已登记)、SCRM 三表 channel='scrm' 已登记。

## 关闭凭证核验 (2026-10-08) | 验收标准 | 核验方式(可复现) | 结果 | |---|---|---| | 注册表映射收敛为单一来源(显式汇聚 + 单一事实定义),四处消费点引用同一处 | 新增 `backend/app/services/registry_index.py`:`build_registry_index()` 从 DATA_TYPE_REGISTRY(18)/UPSERT_DISPATCH(18)/DATA_SOURCE_SEEDS(21+1投影)/_MODEL_REGISTRY(13) **动态汇聚**能力位主表(非第四份硬编码);例外语义唯一来源 `CAPABILITY_NOTES`(14 条登记)。commit `21f16dd` | ✅ | | 启动自检:引用完整性 fail-fast,禁止静默失配 | `main.py create_app()` 顶部 `validate_registry_integrity()`(纯内存校验,不依赖 DB);五类违例汇总抛 `RegistryIntegrityError`:①REGISTRY↔DISPATCH 键集漂移 ②采集通道缺 dispatch ③_MODEL_REGISTRY 双向漂移 ④SEEDS model 不可解析/name_cn 重名 ⑤失配无登记/冗余登记 | ✅ | | 自检可测试:构造缺映射用例验证报错 | `tests/test_registry_index_210.py` 9 例:现状零误报 + monkeypatch 构造 DISPATCH 缺映射/多映射/_MODEL_REGISTRY 漂移/name_cn 重名/无登记失配/冗余登记六类 fail-fast,`pytest tests/test_registry_index_210.py -q` 9 passed | ✅ | | pytest + ruff 全绿;不改变现有采集行为(纯收敛) | 全量 `pytest tests/ -q` 2128 passed exit=0;`ruff check app/ tests/` 全绿;仅新增校验层,四注册表本体与采集链路零改动(`git show 21f16dd --stat` = 3 files, +307/-0) | ✅ | 不做项核实:未拆 today.py(S-P0-3 另批);未做多 worker。 **范围说明**:本单落地**阶段 A**(汇聚 + fail-fast 自检)。阶段 B(注册表派生去双写)与「指标体系是否纳入第五注册表」界定另开后续单挂 #199。裁定记录(2026-10-07 评论五点)已逐项落实:device_profile/merchant 登记「有意不上查看树」、_MODEL_REGISTRY 13 张为有意全集(8 张 analyzable=False 已登记)、SCRM 三表 channel='scrm' 已登记。
tanzk closed this issue 2026-10-08 07:17:34 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: tanzk/csctalon#210