Appearance
ADR-191: 神桶 @/core/utils 去桶化(零依赖叶下沉)
状态: 已完成(2026-07-27);E 档追加 2026-07-30;F 档收尾 2026-07-31 日期: 2026-07-27(初版),2026-07-30(E 档追记),2026-07-31(F 档神桶删除) 关联: ADR-177(Web Loader 统一路径 — 测试 EXIT=124 根因)、cf264937(clamp 叶抽取地基) 来源:
virtual-skirt.test.ts「一改就炸」根因调查——纯几何模块skirt-analyzer.ts从@/core/utils桶导入clampInt,整桶 ESM 组合求值留下 pending 微任务,致 vitest fork worker 永不退(EXIT=124)。
决策者: Riku(联邦首席架构师 AI)、Jieling(人类侧首席架构师)
创建日期: 2026-07-27
背景
@/core/utils 是一处典型 god barrel:792 行,顶部 7–19 行拖入 dom / state / fileservice / status-bar / i18n / feedback / menus / logger 整套应用工具层。任何从桶导入的模块,即便只引用一个纯函数(如 clamp),都会被 ESM 组合求值强制拉起整条应用层依赖链。
直接危害(已证):
skirt-analyzer.ts(标榜纯几何)引clampInt从桶 → vitest fork worker 留 pending 微任务 →virtual-skirt.test.ts整批 EXIT=124(被强杀),表现为「一改就炸」。- 防御措施:
vitest.config.ts已加forceExit: true(管「用例全过但进程不退」),但属兜底,非根因治理。
治理目标:纯 / 叶子模块禁止从桶导入,须引用具体零依赖叶(@/core/clamp / 新增 @/core/path / @/core/async)。应用耦合符号(依赖 state/dom/feedback 的)留桶内——它们本就需要应用层,不属去桶化范围。
现状盘点(2026-07-27)
| 档 | 内容 | 状态 |
|---|---|---|
| A 档 | 抽 @/core/clamp 叶(clamp/clampInt/clamp01),14 个纯模块改引叶 | ✅ 已完成(cf264937 地基 + 14 模块本提交) |
| B 档 | 抽 @/core/path 叶(纯路径符号),迁移路径调用方 | ✅ 已完成(2026-07-27) |
| C 档 | 抽 @/core/async 叶(纯异步符号)+ clampPct/lerp/lerpArray 并入 clamp.ts,迁移调用方 | ✅ 已完成(2026-07-27) |
| D 档 | 应用耦合符号(triggerAutoSave/canvasToBase64/withLoadingIndicator/logWarn/deepClone/debounce/setKey/formatTimestamp/computeLibraryRef/resolveLibraryRef 等)单独收口或留桶 | ✅ 已完成(2026-07-27) |
D档决策(2026-07-27 已完成;2026-07-30 修订)
| 符号 | 分类 | 决策 | 文件 |
|---|---|---|---|
deepClone | 纯函数 | ✅ 下沉为叶模块 | @/core/deep-clone |
debounce | 纯函数 | ✅ 下沉为叶模块 | @/core/debounce |
setKey | 纯函数 | ✅ 下沉为叶模块 | @/core/set-key |
formatTimestamp | 纯函数 | ✅ 下沉为叶模块 | @/core/format-timestamp |
triggerAutoSave | app 耦合 | 🆕 F 档迁出至 @/core/auto-save | @/core/auto-save |
canvasToBase64 | app 耦合 | 🆕 E 档迁出 | @/core/image |
withLoadingIndicator | app 耦合 | 🆕 F 档迁出至 @/core/ui-loading | @/core/ui-loading |
logWarn | app 耦合 | 🆕 E 档迁出至 @/core/logger | @/core/logger |
computeLibraryRef | app 耦合 | 🆕 E 档纯化迁出 | @/core/path |
resolveLibraryRef | app 耦合 | 🆕 F 档迁出至 @/library/library-path | @/library/library-path |
D档实施说明
对四个纯函数执行 叶下沉:
- 在
src/core/分别创建deep-clone.ts、debounce.ts、set-key.ts、format-timestamp.ts - 将函数体移入新文件,添加 JSDoc 文档
- 在
utils.ts中移除对应导出 - 更新所有引用调用方的
import为新路径
对 app 耦合符号 D 档保留桶内,E/F 档逐步迁出:
- D 档时这些函数依赖
dom、state、feedback、i18n等应用层模块,暂留桶内 - E 档将
logWarn迁出至@/core/logger,canvasToBase64迁出至@/core/image - F 档将
triggerAutoSave迁出至@/core/auto-save,withLoadingIndicator迁出至@/core/ui-loading,resolveLibraryRef等迁出至@/library/library-path
实施后验证
- 所有 D档符号的引用已全部迁移至新叶模块或确认保留桶内
virtual-skirt.test.ts恢复正常运行,无 EXIT=124 错误npm run check:funcmap函数索引校验通过
A 档落地后,仍从桶导入的混引模块(2026-07-27 初版 21 个;2026-07-30 E 档后缩减):
| 模块 | 桶内符号 | 可下沉叶 | 状态 |
|---|---|---|---|
outfit/outfit.ts | — | path + async | ✅ 已修复 |
scene/ar/ar-camera.ts | — | image | ✅ canvasToBase64 → @/core/image |
scene/env/env-bridge.ts | — | logger | ✅ logWarn → @/core/logger |
scene/camera/camera.ts | clamp, debounce, deepClone | clamp + (D 档) | ✅ 已引叶 |
scene/manager/model-loader.ts | — | path + async | ✅ 已引叶 |
scene/manager/model-manager.ts | — | clamp + async | ✅ 已引叶 |
scene/manager/thumbnail-capture.ts | — | path + image | ✅ canvasToBase64 → @/core/image |
scene/env/env-persist.ts | — | logger + async | ✅ logWarn → @/core/logger |
scene/manager/thumbnail-key.ts | — | path | ✅ 已引叶 |
scene/motion/bone-override.ts | — | clamp + config | ✅ triggerAutoSave → @/core/config |
scene/motion/motion-modules/registry.ts | — | config | ✅ triggerAutoSave → @/core/config |
scene/motion/vmd-loader.ts | — | path + config | ✅ withLoadingIndicator → @/core/config |
scene/motion/vmd-layers.ts | getBaseName, clamp01 | path + clamp | 待确认 |
motion-algos/beat-detector.ts | clamp01, swallowError | clamp + async | 待确认 |
scene/env/env-time-of-day.ts | 多符号(待核) | 按符号分流 | 待确认 |
scene/env/props.ts | getBaseName | path | 待确认 |
core/ui-advanced-rows.ts | clampPct | clamp | 待确认 |
scene/render/renderer.ts | clamp, clamp01, lerp, lerpArray, setKey | clamp + (setKey D 档) | 待确认 |
scene/render/lighting.ts | setKey | (D 档) | 待确认 |
scene/render/performance.ts | formatTimestamp | (D 档) | 待确认 |
core/ui-rows.ts | clamp01, clampPct, swallowError | clamp + async | 待确认 |
menus/(26 文件) | tryCatchStatus, closeAllOverlays, escapeHtml, jsonStringify, CATEGORY_DIR, showErrorToast 等 | 部分可叶化 | ⏳ 见 E 档 |
注:
menus/中多数符号(如tryCatchStatuscloseAllOverlays)定义在神桶本体中,需先拆叶模块方可迁移。swallowError/getBaseName/normPath等可叶化符号仍混在神桶多行 import 块中——因神桶自有符号与可叶符号并列,需逐文件拆分。
决策
- 叶契约:新建叶须 零依赖(
import仅自身或同为叶)。path.ts自含normPath(含缓存),并反转fileservice依赖(让fileservice改从path.ts引normPath),消除双份定义。 - state/logger 耦合符号不下沉:
computeLibraryRef/resolveLibraryRef(依赖libraryRoot+logWarn)与triggerAutoSave/canvasToBase64/withLoadingIndicator/logWarn/setKey/formatTimestamp等留桶内,属 D 档或长期留桶。 - math 收敛到
clamp.ts:clampPct(clamp 变体)、lerp/lerpArray(纯数学)并入clamp.ts叶,统一数学出口。 - re-export 保兼容:
utils.ts仍export { ... } from './path' | './async' | './clamp',其余调用方零改动;仅纯 / 叶子模块主动改引具体叶。 - 纪律写入 AGENTS.md:「纯 / 叶子模块禁止从桶(
@/core/utils)导入,须引具体零依赖叶」。
验证
- 每档完成后跑
tsc --noEmit+ 受影响模块单测,确认 EXIT=0(对比 A 档前virtual-skirt.test.tsEXIT=124)。 npm run check:funcmap校验函数索引(clamp/path/async 符号迁移后)。
E 档追加(2026-07-30)
新增叶模块
| 叶模块 | 迁入符号 | 原位置 | 说明 |
|---|---|---|---|
@/core/uuid.ts | generateUuid | @/core/utils L156-162 | 纯 UUID v4 生成,零依赖 |
@/core/image.ts | canvasToBase64 | @/core/utils L116-154 | Canvas → base64 异步编码,零依赖 |
D 档决策修订
| 符号 | 原决策 | 新决策 | 原因 |
|---|---|---|---|
canvasToBase64 | 保留桶内(app 耦合) | 迁出至 @/core/image | 纯 canvas 操作,不依赖 state/dom/feedback;仅依赖 HTMLCanvasElement |
computeLibraryRef | 保留桶内(app 耦合) | 纯化迁出至 @/core/path | 改为参数化接收 libraryRoot,纯函数化后迁入零依赖叶 |
持续清理记录
本轮(2026-07-30)神桶审计清理了以下 14 个文件 的直接 @/core/utils 导入:
| 文件 | 修复内容 |
|---|---|
scene/scene.ts | swallowError → @/core/async |
scene/scene-bundle.ts | computeLibraryRef → @/core/path |
scene/scene-serialize.ts | computeLibraryRef/swallowError/generateUuid 分拆 |
scene/manager/model-id.ts | generateUuid → @/core/uuid |
scene/manager/thumbnail-capture.ts | canvasToBase64 → @/core/image |
scene/env/_bridge/env-bridge.ts | logWarn → @/core/logger |
scene/env/_bridge/env-persist.ts | logWarn → @/core/logger |
scene/ar/ar-camera.ts | canvasToBase64 → @/core/image |
scene/motion/bone-override.ts | triggerAutoSave → @/core/config |
scene/motion/motion-modules/registry.ts | triggerAutoSave → @/core/config |
scene/motion/vmd-loader.ts | withLoadingIndicator → @/core/config |
core/action-defs/motion-actions.ts | triggerAutoSave → @/core/config |
core/init.ts | fireAndForget/swallowError → @/core/async |
core/ui-resource-panel.ts | thumbDataUrl → @/core/config |
F 档收尾:删除 @/core/utils 神桶(2026-07-31)
新增叶模块与迁移符号
本轮把 E 档中标注「定义在神桶本体、需先拆叶」的符号全部抽出,最终删除 src/core/utils.ts。
| 叶模块 | 迁入符号 | 原位置 | 说明 |
|---|---|---|---|
@/core/format.ts | formatTime, formatError | @/core/utils | 格式化纯函数,零依赖 |
@/core/math-geometry.ts | dist2d, dist3d, degToRad, radToDeg | @/core/utils | 纯数学/几何辅助 |
@/core/collections.ts | ensureArray, filterKeys, Cache, allSettledFilter | @/core/utils | 集合与 Promise 工具 |
@/core/escape-html.ts | escapeHtml | @/core/utils | HTML 转义纯函数 |
@/core/json-stringify.ts | jsonStringify, jsonParse | @/core/utils | 安全 JSON 序列化/反序列化 |
@/core/ui-card.ts | cardContainer | @/core/utils | UI 卡片容器辅助;依赖 dom/i18n,属应用层叶 |
@/core/ui-loading.ts | withLoadingIndicator | @/core/utils | 加载指示器;依赖 dom/i18n |
@/core/auto-save.ts | setTriggerAutoSave, triggerAutoSave | @/core/utils | 自动保存触发器;解耦自 scene-serialize 的 impl |
@/menus/menu-stack-registry.ts | stackRegistry | @/core/utils | 菜单栈注册表;从 core 迁到 menus 域,破循环依赖 |
@/core/status-helpers.ts | tryCatchStatus | @/core/utils | 状态栏错误包装;依赖 status-bar |
@/core/toast.ts | showErrorToast | @/core/utils | 错误提示;toast 系统本已存在,归入同类 |
@/menus/menu-overlay.ts | closeAllOverlays | @/core/utils | 弹窗关闭;属于 menus 域 |
@/library/library-path.ts | CATEGORY_DIR, computeLibraryRef, resolveLibraryRef, getBrowseDir | @/core/utils / @/core/path | 图书馆路径工具;menus 用封装版读 libraryRoot,scene 用 core/path 参数化纯版 |
menus/ 26 文件导入清理
F 档前 menus/ 仍有 26 个文件直接引用神桶。拆分上述叶模块后,全部改为具体叶导入:
- 路径相关 →
@/library/library-path(CATEGORY_DIR,computeLibraryRef,getBrowseDir,resolveLibraryRef) - 自动保存 →
@/core/auto-save - 加载指示器 →
@/core/ui-loading - 卡片容器 →
@/core/ui-card - 状态包装 →
@/core/status-helpers - 错误提示 →
@/core/toast - 弹窗关闭 →
@/menus/menu-overlay - HTML 转义 →
@/core/escape-html - JSON 工具 →
@/core/json-stringify - 集合/数学 →
@/core/collections,@/core/math-geometry - 格式工具 →
@/core/format - 菜单栈注册表 →
@/menus/menu-stack-registry
聚合层调整
src/core/utils.ts已删除。src/core/config.ts不再export * from './utils',改为显式导出新增叶模块(format,math-geometry,collections,auto-save)及menu-stack-registry,保持通过@/core/config消费的代码兼容。
F 档验证
npm run check✅(tsc + eslint + docs 校验)npm run test -- --run✅ 239 文件 / 2696 用例全绿grep确认frontend/src已无@/core/utils或../core/utils导入
剩余技术债
无。@/core/utils 神桶已彻底移除,frontend/src 不再存在从该桶的导入。