Skip to content

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
triggerAutoSaveapp 耦合🆕 F 档迁出@/core/auto-save@/core/auto-save
canvasToBase64app 耦合🆕 E 档迁出@/core/image
withLoadingIndicatorapp 耦合🆕 F 档迁出@/core/ui-loading@/core/ui-loading
logWarnapp 耦合🆕 E 档迁出@/core/logger@/core/logger
computeLibraryRefapp 耦合🆕 E 档纯化迁出@/core/path
resolveLibraryRefapp 耦合🆕 F 档迁出@/library/library-path@/library/library-path

D档实施说明

对四个纯函数执行 叶下沉

  1. src/core/ 分别创建 deep-clone.tsdebounce.tsset-key.tsformat-timestamp.ts
  2. 将函数体移入新文件,添加 JSDoc 文档
  3. utils.ts 中移除对应导出
  4. 更新所有引用调用方的 import 为新路径

对 app 耦合符号 D 档保留桶内,E/F 档逐步迁出

  • D 档时这些函数依赖 domstatefeedbacki18n 等应用层模块,暂留桶内
  • E 档将 logWarn 迁出至 @/core/loggercanvasToBase64 迁出至 @/core/image
  • F 档将 triggerAutoSave 迁出至 @/core/auto-savewithLoadingIndicator 迁出至 @/core/ui-loadingresolveLibraryRef 等迁出至 @/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.tspath + async✅ 已修复
scene/ar/ar-camera.tsimagecanvasToBase64@/core/image
scene/env/env-bridge.tsloggerlogWarn@/core/logger
scene/camera/camera.tsclamp, debounce, deepCloneclamp + (D 档)✅ 已引叶
scene/manager/model-loader.tspath + async✅ 已引叶
scene/manager/model-manager.tsclamp + async✅ 已引叶
scene/manager/thumbnail-capture.tspath + imagecanvasToBase64@/core/image
scene/env/env-persist.tslogger + asynclogWarn@/core/logger
scene/manager/thumbnail-key.tspath✅ 已引叶
scene/motion/bone-override.tsclamp + configtriggerAutoSave@/core/config
scene/motion/motion-modules/registry.tsconfigtriggerAutoSave@/core/config
scene/motion/vmd-loader.tspath + configwithLoadingIndicator@/core/config
scene/motion/vmd-layers.tsgetBaseName, clamp01path + clamp待确认
motion-algos/beat-detector.tsclamp01, swallowErrorclamp + async待确认
scene/env/env-time-of-day.ts多符号(待核)按符号分流待确认
scene/env/props.tsgetBaseNamepath待确认
core/ui-advanced-rows.tsclampPctclamp待确认
scene/render/renderer.tsclamp, clamp01, lerp, lerpArray, setKeyclamp + (setKey D 档)待确认
scene/render/lighting.tssetKey(D 档)待确认
scene/render/performance.tsformatTimestamp(D 档)待确认
core/ui-rows.tsclamp01, clampPct, swallowErrorclamp + async待确认
menus/(26 文件)tryCatchStatus, closeAllOverlays, escapeHtml, jsonStringify, CATEGORY_DIR, showErrorToast 等部分可叶化⏳ 见 E 档

注:menus/ 中多数符号(如 tryCatchStatus closeAllOverlays)定义在神桶本体中,需先拆叶模块方可迁移。swallowError/getBaseName/normPath 等可叶化符号仍混在神桶多行 import 块中——因神桶自有符号与可叶符号并列,需逐文件拆分。


决策

  1. 叶契约:新建叶须 零依赖import 仅自身或同为叶)。path.ts 自含 normPath(含缓存),并反转 fileservice 依赖(让 fileservice 改从 path.tsnormPath),消除双份定义。
  2. state/logger 耦合符号不下沉computeLibraryRef/resolveLibraryRef(依赖 libraryRoot+logWarn)与 triggerAutoSave/canvasToBase64/withLoadingIndicator/logWarn/setKey/formatTimestamp 等留桶内,属 D 档或长期留桶。
  3. math 收敛到 clamp.tsclampPct(clamp 变体)、lerp/lerpArray(纯数学)并入 clamp.ts 叶,统一数学出口。
  4. re-export 保兼容utils.tsexport { ... } from './path' | './async' | './clamp',其余调用方零改动;仅纯 / 叶子模块主动改引具体叶。
  5. 纪律写入 AGENTS.md:「纯 / 叶子模块禁止从桶(@/core/utils)导入,须引具体零依赖叶」。

验证

  • 每档完成后跑 tsc --noEmit + 受影响模块单测,确认 EXIT=0(对比 A 档前 virtual-skirt.test.ts EXIT=124)。
  • npm run check:funcmap 校验函数索引(clamp/path/async 符号迁移后)。

E 档追加(2026-07-30)

新增叶模块

叶模块迁入符号原位置说明
@/core/uuid.tsgenerateUuid@/core/utils L156-162纯 UUID v4 生成,零依赖
@/core/image.tscanvasToBase64@/core/utils L116-154Canvas → base64 异步编码,零依赖

D 档决策修订

符号原决策新决策原因
canvasToBase64保留桶内(app 耦合)迁出@/core/image纯 canvas 操作,不依赖 state/dom/feedback;仅依赖 HTMLCanvasElement
computeLibraryRef保留桶内(app 耦合)纯化迁出@/core/path改为参数化接收 libraryRoot,纯函数化后迁入零依赖叶

持续清理记录

本轮(2026-07-30)神桶审计清理了以下 14 个文件 的直接 @/core/utils 导入:

文件修复内容
scene/scene.tsswallowError@/core/async
scene/scene-bundle.tscomputeLibraryRef@/core/path
scene/scene-serialize.tscomputeLibraryRef/swallowError/generateUuid 分拆
scene/manager/model-id.tsgenerateUuid@/core/uuid
scene/manager/thumbnail-capture.tscanvasToBase64@/core/image
scene/env/_bridge/env-bridge.tslogWarn@/core/logger
scene/env/_bridge/env-persist.tslogWarn@/core/logger
scene/ar/ar-camera.tscanvasToBase64@/core/image
scene/motion/bone-override.tstriggerAutoSave@/core/config
scene/motion/motion-modules/registry.tstriggerAutoSave@/core/config
scene/motion/vmd-loader.tswithLoadingIndicator@/core/config
core/action-defs/motion-actions.tstriggerAutoSave@/core/config
core/init.tsfireAndForget/swallowError@/core/async
core/ui-resource-panel.tsthumbDataUrl@/core/config

F 档收尾:删除 @/core/utils 神桶(2026-07-31)

新增叶模块与迁移符号

本轮把 E 档中标注「定义在神桶本体、需先拆叶」的符号全部抽出,最终删除 src/core/utils.ts

叶模块迁入符号原位置说明
@/core/format.tsformatTime, formatError@/core/utils格式化纯函数,零依赖
@/core/math-geometry.tsdist2d, dist3d, degToRad, radToDeg@/core/utils纯数学/几何辅助
@/core/collections.tsensureArray, filterKeys, Cache, allSettledFilter@/core/utils集合与 Promise 工具
@/core/escape-html.tsescapeHtml@/core/utilsHTML 转义纯函数
@/core/json-stringify.tsjsonStringify, jsonParse@/core/utils安全 JSON 序列化/反序列化
@/core/ui-card.tscardContainer@/core/utilsUI 卡片容器辅助;依赖 dom/i18n,属应用层叶
@/core/ui-loading.tswithLoadingIndicator@/core/utils加载指示器;依赖 dom/i18n
@/core/auto-save.tssetTriggerAutoSave, triggerAutoSave@/core/utils自动保存触发器;解耦自 scene-serialize 的 impl
@/menus/menu-stack-registry.tsstackRegistry@/core/utils菜单栈注册表;从 core 迁到 menus 域,破循环依赖
@/core/status-helpers.tstryCatchStatus@/core/utils状态栏错误包装;依赖 status-bar
@/core/toast.tsshowErrorToast@/core/utils错误提示;toast 系统本已存在,归入同类
@/menus/menu-overlay.tscloseAllOverlays@/core/utils弹窗关闭;属于 menus 域
@/library/library-path.tsCATEGORY_DIR, computeLibraryRef, resolveLibraryRef, getBrowseDir@/core/utils / @/core/path图书馆路径工具;menus 用封装版读 libraryRoot,scene 用 core/path 参数化纯版

F 档前 menus/ 仍有 26 个文件直接引用神桶。拆分上述叶模块后,全部改为具体叶导入:

  • 路径相关 → @/library/library-pathCATEGORY_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 -- --run239 文件 / 2696 用例全绿
  • grep 确认 frontend/src 已无 @/core/utils../core/utils 导入

剩余技术债

无。@/core/utils 神桶已彻底移除,frontend/src 不再存在从该桶的导入。