只 grep 入口函数,SSOT 基线就是假的
我们在给一个后台管理系统做「场景引擎」重构,目标是让所有读取场景配置的地方都统一走 SceneResolver 作为唯一数据源(SSOT)。在正式迁移之前,需要出一份基线 PR,报告「还有多少文件没迁移完」。
我的第一直觉是:
rg "resolveScene\(" src/ --type ts
# 结果:0 matches零命中,太好了,说明还没有任何地方调用入口函数,基线数据是 entry_consumers=0,迁移还没开始,符合预期。然后我在 PR 里写了一行:
PR-L2-SR.0_PASS · runtime_resolve_scene_consumers=0 · migration_candidates=2
Reviewer 开口了:「你有没有独立跑一下模块导入?」
rg -ln "from .*'@/engine/scene-resolver" src --type ts \
| grep -v "src/engine/scene-resolver/" \
| grep -v "__tests__" \
| wc -l
# 结果:1313 个文件。
现象
基线 PR 声称「消费者为 0,迁移候选仅 2 个」,但 reviewer 独立一条 grep 就查出 13 个文件在导入这个模块。后续每一个迁移 PR 合并时都会「惊喜地发现」又有新文件需要处理,审计数字毫无参考价值。
根因
新引擎模块的暴露面不只有入口函数,它通常还包括三层:
| 深度 | 含义 | 典型导入 |
|------|------|---------|
| entry | 调用公共入口函数 | resolveScene(ctx) |
| helper | 导入子模块工具函数 | projectCascadePatch, loadReportColumns |
| type-only | 仅导入类型定义 | EffectiveField, SceneKind |
| seed-direct | 直接导入原始种子数据 | fieldLineageSeed(绕过引擎) |
rg "resolveScene\(" 只捕获第一层(entry)。而 helper 层说明文件已经在使用该模块的能力,只是绕过了统一入口,是真实的迁移候选。type-only 层说明 schema 已经以该模块为基准,类型 SSOT 已成立,运行时 SSOT 还未建立。
entry_consumers=0 不代表「没人用这个模块」,只代表「没人走统一入口」。而 helper 层恰好揭示了那些「知道这个模块存在、但选择了侧门入场」的文件——它们才是真正的迁移工作量。单一 grep 漏掉这两层,基线数字会比实际少一个数量级。
正解
消费者审计必须跑三次 grep,而不是一次:
MODULE="@/engine/scene-resolver"
ENTRY_FN="resolveScene"
# Layer 1:入口调用点
rg -n "${ENTRY_FN}\s*\(" src --type ts \
| grep -v "src/engine/scene-resolver/"
# Layer 2+3:所有导入该模块路径的文件(helper + type-only 一起捞)
rg -ln "from .*'${MODULE}" src --type ts \
| grep -v "src/engine/scene-resolver/" \
| grep -v "__tests__"
# 对 Layer 2+3 里的每个文件,检查导入的 symbol 是函数/值还是纯类型
# 函数/值 → helper(迁移候选)
# 仅 Type/interface → type-only(schema 已 SSOT,运行时待迁移)
# Seed-direct(完全绕过引擎)
rg -n "from .*'@/data/field-scenes.seed'" src scripts --type ts然后输出一张分类表——标记数字必须从这张表算出来,而不是反过来:
| 文件 | 深度 | 导入的 symbol | 建议 |
|------|------|--------------|------|
| src/components/fields/cascade-group-utils.ts | helper | projectCascadePatch | 迁移候选 |
| src/app/approval/WorkflowFieldPanel.tsx | type-only | EffectiveField | schema 已 SSOT,可保留 |
| src/lib/scenes/create-scene.ts | helper | resolveSceneReportIdentity | 迁移候选 |
好的基线 marker 长这样:
entry_consumers=0
submodule_helper_consumers=5 # ← 这才是迁移跑道
submodule_type_only_consumers=8
seed_direct_imports_runtime=0
migration_candidates=5
marker 数字必须从每文件分类表汇总而来,而非独立估算。自检规则:entry + helper + type-only = 总导入文件数,加和不等于整体数说明有遗漏或重复计算。后续每个迁移 PR 只需要把表里的 helper 行划掉,进度一目了然。
实测自检
TOTAL=$(rg -ln "from .*'@/engine/scene-resolver" src --type ts \
| grep -v "src/engine/scene-resolver/" \
| grep -v "__tests__" | wc -l)
# entry_count + helper_count + type_only_count 必须 == TOTAL
# 不相等 → 表里有行缺失或文件被重复计数一句话外卖
给模块做 SSOT 基线时,「入口调用数为零」从来不等于「消费者为零」——子模块 helper 导入和 type-only 导入才是隐藏的迁移工作量,必须三层全 grep、逐文件分类,基线数字才有跑道意义。