Skip to content

feat(loader): support constructed values and nested calls#3067

Merged
GuoLei1990 merged 8 commits into
galacean:dev/2.0from
luzhuang:feat/runtime-type-constructor-args
Jul 16, 2026
Merged

feat(loader): support constructed values and nested calls#3067
GuoLei1990 merged 8 commits into
galacean:dev/2.0from
luzhuang:feat/runtime-type-constructor-args

Conversation

@luzhuang

@luzhuang luzhuang commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • support recursively resolved constructor arguments on runtime-v2 $type values
  • allow mutation calls to target nested method owners and apply props before calls
  • encode positive infinity as the format-level { "$number": "Infinity" } value instead of a Burst constructor-position special case
  • make sub-asset loading follow the completed main-asset promise so eager callback cleanup cannot strand concurrent queries
  • reject absent sub-asset paths and allow clean retries after a main-asset failure
  • expose ResourceManager.registerVirtualResources and VirtualResource for stable package paths backed by generated URLs

Architecture boundary

Editor resource packages remain source-v2. Builder/compiler code owns source-v2 validation and lowering to runtime-v2, including converting Editor's JSON-safe "Infinity" source value into { "$number": "Infinity" }.

Engine consumes runtime-v2 only. The reflection parser does not know Editor class / constructParams structures, particle classes, or constructor parameter positions.

For sub-assets, loader notifications remain the eager fast path. The completed main resource is the authoritative fallback, so callback lifecycle ordering cannot leave a valid query pending forever. Missing query paths and main-asset failures reject without leaving stale failure state behind.

Verification

  • pnpm vitest run tests/src/core/resource/ResourceManager.test.ts tests/src/loader/SceneFormatV2.test.ts: 72 passed
  • pnpm build: passed, including all package declaration builds
  • touched Engine files: ESLint 0 errors
  • Prettier and git diff --check: passed
  • Builder source-to-runtime tests: 45 passed
  • Builder runtime schema-v2 typecheck: passed
  • browser resource-package compiler test and production build: passed
  • real cosmic-flame package: 27 assets / 1 runtime Prefab; browser render completed with no warning or error logs

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Reflection parsing now supports nested call targets, ordered mutation execution, recursive $type constructor arguments, and positive-infinity encoding. ResourceManager adds runtime virtual-resource registration and main-promise-based sub-asset resolution, with expanded tests and documentation.

Changes

Reflection and typed value resolution

Layer / File(s) Summary
Reflection mutation execution
packages/loader/src/schema/CommonSchema.ts, packages/loader/src/resource-deserialize/.../ReflectionParser.ts, tests/src/loader/SceneFormatV2.test.ts
Adds typed value and call-target contracts, resolves nested targets, and applies properties before calls.
Typed constructor value resolution
packages/loader/src/resource-deserialize/.../ReflectionParser.ts, tests/src/loader/SceneFormatV2.test.ts, notes/loader/2026-07-15-runtime-special-number-contract.md
Supports recursive $args, typed construction, positive-infinity normalization, and invalid $args or $number rejection.

Virtual resource loading

Layer / File(s) Summary
Virtual-resource registration and sub-asset resolution
packages/core/src/asset/ResourceManager.ts, packages/core/src/index.ts, tests/src/core/resource/ResourceManager.test.ts, notes/loader/2026-07-15-resource-package-virtual-subasset-race.md
Adds runtime virtual-resource registration, exports VirtualResource, resolves sub-assets from completed main assets, and updates coverage and documentation.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SceneFormatV2Test
  participant ReflectionParser
  participant CallOrderComponent
  SceneFormatV2Test->>ReflectionParser: parse mutation block
  ReflectionParser->>CallOrderComponent: apply resolved props
  ReflectionParser->>CallOrderComponent: invoke target method
  CallOrderComponent-->>SceneFormatV2Test: expose updated state
Loading
sequenceDiagram
  participant ResourceManager
  participant MainAssetPromise
  participant GLTFLoader
  ResourceManager->>GLTFLoader: load main asset
  GLTFLoader-->>MainAssetPromise: complete main asset
  ResourceManager->>MainAssetPromise: resolve sub-asset path
  MainAssetPromise-->>ResourceManager: return sub-asset
Loading

Possibly related PRs

  • galacean/engine#3035: Updates related virtualPath option resolution and sub-asset loading behavior.
  • galacean/engine#3066: Updates the same ReflectionParser contracts, typed value resolution, and related loader tests.

Suggested labels: loader, bug

Suggested reviewers: guolei1990

Poem

I hop through props, then calls align,
Nested paths now work just fine.
Constructors bloom, bursts shine bright,
Virtual assets cross the line.
— A parser-loving rabbit 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures two major changes: constructed $type values and nested call targeting.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.95402% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.64%. Comparing base (b7bf6f7) to head (aa62327).

Files with missing lines Patch % Lines
...e-deserialize/resources/parser/ReflectionParser.ts 88.63% 5 Missing ⚠️
packages/core/src/asset/ResourceManager.ts 97.61% 1 Missing ⚠️
packages/loader/src/ProjectLoader.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           dev/2.0    #3067      +/-   ##
===========================================
+ Coverage    79.61%   79.64%   +0.03%     
===========================================
  Files          904      904              
  Lines       101208   101220      +12     
  Branches     11404    11428      +24     
===========================================
+ Hits         80579    80619      +40     
+ Misses       20445    20417      -28     
  Partials       184      184              
Flag Coverage Δ
unittests 79.64% <91.95%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`:
- Around line 165-167: Move the standalone "$args" validation immediately after
obj is created in ReflectionParser, before branches handling "$ref", "$class",
"$entity", "$component", or "$signal", so any object containing "$args" without
"$type" is rejected. Add a test covering a mixed discriminator such as "$class"
with "$args" and assert it fails.
- Around line 43-57: Update the call traversal and invocation logic in
ReflectionParser to reject the property names "constructor", "prototype", and
"__proto__" in every call.target segment and in call.method before accessing or
invoking them. Return a rejected Promise using the existing validation-error
pattern, while preserving valid nested method resolution and invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d5ce96ab-e890-4807-80ee-e87475a9bfd1

📥 Commits

Reviewing files that changed from the base of the PR and between b7bf6f7 and ceba5a3.

📒 Files selected for processing (3)
  • packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts
  • packages/loader/src/schema/CommonSchema.ts
  • tests/src/loader/SceneFormatV2.test.ts

Comment on lines +43 to +57
let target = instance;
if (call.target !== undefined) {
if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) {
return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`));
}
for (const key of call.target) target = target?.[key];
}
const method = target?.[call.method];
if (typeof method !== "function") {
return Promise.reject(new Error(`Call target does not have method "${call.method}"`));
const path = call.target?.length ? `${call.target.join(".")}.` : "";
return Promise.reject(new Error(`Call target does not have method "${path}${call.method}"`));
}

return Promise.all((call.args ?? []).map((arg) => this._resolveValue(arg)))
.then((resolvedArgs) => Promise.resolve(method.apply(instance, resolvedArgs)))
.then((resolvedArgs) => Promise.resolve(method.apply(target, resolvedArgs)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block prototype and constructor traversal before invoking nested calls.

Loader-controlled paths can traverse function properties and reach Function, for example target: ["nested", "setValue"] with method: "constructor". Combined with call.result, this permits constructing and invoking arbitrary JavaScript.

Reject constructor, prototype, and __proto__ in both target segments and method names.

Proposed guard
+        const forbiddenKeys = new Set(["constructor", "prototype", "__proto__"]);
         let target = instance;
         if (call.target !== undefined) {
           if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) {
             return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`));
           }
+          if (call.target.some((key) => forbiddenKeys.has(key)) || forbiddenKeys.has(call.method)) {
+            return Promise.reject(new Error(`Call "${call.method}" contains a forbidden target key`));
+          }
           for (const key of call.target) target = target?.[key];
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`
around lines 43 - 57, Update the call traversal and invocation logic in
ReflectionParser to reject the property names "constructor", "prototype", and
"__proto__" in every call.target segment and in call.method before accessing or
invoking them. Return a rejected Promise using the existing validation-error
pattern, while preserving valid nested method resolution and invocation.

Comment thread packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts Outdated

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ ceba5a347(首轮)— feat(loader): support constructed values and nested calls

总结

给 v2 场景 ReflectionParser 补三块运行时反序列化能力:① $type 支持递归解析的 $args 构造参数;② call.target 支持把方法调用打到嵌套属性路径的 owner 上;③ $number: "Infinity" 的 JSON-safe 正无穷哨兵;④ 把 parseMutationBlock 从「props/calls 并发无序」改成「props 先、calls 后」的确定性顺序。方向完全正确,是 #3061 那版错 base 的 d59c6392d 收敛到 dev/2.0 纯 Loader 改动后的干净版本。逐链核对无 P0/P1/P2,可合并。

逐链核对(非信 commit message / PR 描述)

  • props-before-calls 顺序是收紧而非回退:原 Promise.all([props, calls]) 的 JSDoc 明确写「without imposing ordering」(来自 #2959 284cce8e1),但那只是描述行为、未给出「必须无序」的理由。改成 props→calls 是确定性收紧:老行为并发交错,没有任何合法数据能依赖「calls 先于 props」,所以本改动严格更安全,且符合语义直觉(props 配置字段状态,calls 在其上动作,如 addBurst 需先配好 emission)。走 Git 历史透镜确认无矛盾——不是还原任何 fix:
  • 顺序测试反向可证伪should finish resolving props before executing calls 构造精巧——$ref 被 mock 成延迟 promise,await Promise.resolve() 放过一个微任务后才 resolve。新序下 call 捕获 "ready";revert 回 Promise.all 则 call 在 ref 解析前就跑、捕获 "",测试 fail。真守回归。
  • particle 恢复测试是端到端真链路Burst($args:[0.5, curve, 2, 0.1]) / ParticleCurve($args:[CurveKey,CurveKey]) / CurveKey($args:[0,1]) / ParticleCompositeCurve($args:[8]) 的构造签名逐一核对全部对齐真实构造函数,且经 call.target:["generator","emission"] + addBurst/clearBurst 走公开 API。这正是本特性的动机——editor 需要 round-trip 需构造参数 + 经方法(非纯 props)设置的粒子曲线/burst。reverse-falsifiable。
  • call.target 绑定正确method.apply(target, ...) 打到遍历后的 owner;call.target === undefinedtarget = instancetarget?.[call.method] 退化为原 instance?.[call.method],行为等价。错误消息 path 拼接在 target 校验之后,安全。
  • schema 新增类型非死代码TypeValue / SpecialNumberValueCommonSchema.ts 无 parser 消费者,但这与既有 ClassRef(同样零消费者)/Vec3Tuple/EntityRef 完全一致——该文件的职责就是声明 v2 格式的规范词汇表,不是必须被 import 才算活。按项目惯例非冗余。
  • 注释合规:更新后的 _resolveValue 优先级 JSDoc 已同步补入 $number(#4)/$type,$args?(#5) 重编号,与实际分支顺序一致;parseMutationBlock JSDoc 由「without imposing ordering」改为「Apply props before executing calls」——非 aspirational,与代码一致。CI 全绿(build×3 / codecov patch 88.37% + project 79.61% / prettier)。

已评估的 CodeRabbit 两条 finding(均非阻塞,据实校准严重度)

  • CR「Critical: prototype/constructor 遍历可达 RCE」→ 实为 P3 defensive、非新洞。核心事实:call.target:["fn"], method:"constructor" 能拿到 Function 进而 call.result 链式 RCE——但这条可达性早于本 PRparseCalls#2959 起就有 instance[call.method] + call.result → parseMutationBlock 递归,{method:"constructor", result:{calls:[{method:"constructor", result:{calls:[{method:"call", args:[...]}]}}]}} 早已可达 Function 并执行任意代码。本 PR 的 target 遍历只是便利路径,未引入新能力类。且 v2 场景数据是 editor 产出(与 glTF/scene 资产同信任级、等同 app 代码),非系统边界的不可信输入。CR 的 Critical 定级在此信任模型下过高。鉴于本 PR 恰好动到这段遍历代码,加一个 constructor/prototype/__proto__ 黑名单守卫是合理的 defense-in-depth,但属可选加固,不阻塞。

    const forbidden = new Set(["constructor", "prototype", "__proto__"]);
    if (call.target?.some((k) => forbidden.has(k)) || forbidden.has(call.method)) {
      return Promise.reject(new Error(`Call "${call.method}" contains a forbidden key`));
    }

问题

  • [P3] ReflectionParser.ts:165-167 — 独立 $args 校验位置偏后(CR Minor,据实保留为 P3)if ("$args" in obj) reject 排在 $ref/$class/$entity/$component/$signal 分支之后,故 { $class:"Foo", $args:[] } 会先命中 $class 分支、静默丢弃 $args。editor 不会产出这种混合 discriminator,属畸形输入容错的小缺口。若要收紧,把该校验挪到 obj 创建后、所有 discriminator 分支之前,并补一条 $class+$args 的混合用例。不阻塞。

  • [P3] ReflectionParser.ts — $number 仅覆盖正无穷。当前只支持 $number:"Infinity"(对应 Burst 无限循环 cycles),显式 reject NaN-Infinity 暂无字段需要故未支持——YAGNI 合理。仅记录:若将来出现需持久化 -Infinity 的字段(某些下界),此哨兵需扩展。不阻塞。

简化建议

无。代码干净,抽象克制($args 复用 _resolveValue 递归、call.target 与既有 parseCalls 无缝衔接),无过度设计。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ 84b614111(第二轮,增量)— fix(loader): decode infinite burst cycles

总结

自上轮 ceba5a347 起线性追加一个 commit(parent 就是 ceba5a347,非 force-push)。改动只做一件事:删掉上轮刚引入的通用 $number: "Infinity" 哨兵,改成在 _resolveValue$type 构造分支里对 $type === "Burst"args[2] 做位置化、字符串化的特判——"Infinity"Infinity,其余非 null 的非正整数 → reject。逻辑正确、反向证伪测试到位、CI 全绿(build×3 / codecov patch+project / e2e 4/4 / lint),可合并。但方向上把一个通用能力换成了单类硬编码特判,值得一议——记 [P2],不阻塞。

逐链核对(增量)

  • Burst.$args[2] 确是 cyclesBurst 构造签名 (time, count, cycles?, repeatInterval?)Burst.ts:51),args[2] 位置对齐。cycles === InfinityEmissionModule.ts:312 的真运行时哨兵(maxCycles = cycles===Infinity ? ceil((duration-burstTime)/repeatInterval) : cycles),JSON 无法表达 InfinityJSON.stringify(Infinity)==="null")故需 sentinel——需求真实。
  • 守卫逻辑穷举无洞"Infinity"Infinity ✓ / 2→过 ✓ / "NaN"→非"Infinity"非null且非number→throw ✓(测试覆盖)/ null|undefined!=null 短路跳过→构造函数 Math.max(cycles??1,1)=1 ✓ / 1.5/0/-Infinity→非正整数→throw ✓。
  • 反向证伪成立:测试改用 "Infinity" 串断言 bursts[0].cycles === Infinity,并新增 "NaN" 用例断言 rejects.toThrow(...);revert 特判即红。
  • Git 历史透镜无矛盾$number 是本 PR 上一 commit(ceba5a347)自己引入的,全仓无第二处引用、从未发版/被消费——删它不是还原任何 fix:,是作者重做自己刚加的机制。
  • 注释合规_resolveValue 优先级 JSDoc 从 10 条重编号回 9 条(去掉 $number),与实际分支顺序($ref3/$type4/$class5/$entity6/$component7/$signal8/plain 9)逐条一致,无残留。SpecialNumberValue interface 同步删除。

问题

  • [P2] ReflectionParser.ts:130-137 — 通用 Infinity 编码退化成 Burst 单类位置特判,是「类型枚举代替特征」+ 抽象泄漏。上轮的 $number: "Infinity" 虽然只覆盖正无穷(我上轮记为 P3-YAGNI),但它是格式层的通用 JSON-safe 数值编码——任何 prop / ctor arg 都能用。本轮把它换成 if ($type === "Burst" && args.length > 2)args[2] 的字符串特判,埋进了本该类型无关的 _resolveValue 分发器里,带来两点结构代价:

    • Infinity 本质是通用序列化问题,不是 Burst 问题。同仓 Joint.breakForce/breakTorque 默认就是 InfinityJoint.ts:27-28,对应 Unity「不可断裂关节」惯例)且是 public 可序列化属性(Joint.ts:157/171);ResourceManager.timeoutrequest.defaultTimeout 同样默认 Infinity。这些字段将来要经 v2 round-trip 时,通用 $number 本可覆盖,现在每个都得往 parser 里再加一条特判。新类型要 infinity → 必须改 parser——正是「加白名单,漏补即 bug」的信号。
    • 泛型分发器嵌入了单个引擎类的构造参数语义(硬编码「Burst 的第 3 个位置参是 cycles」)。Burst 构造顺序一旦调整,这里静默失效,且无编译期护栏。

    另外,"Infinity" 这个魔法串契约在 schema 里零文档——TypeValue.$args?: unknown[] 完全没提 Burst 的第三参接受字符串 "Infinity",producer(editor companion PR #3793)与 reader 全靠隐式约定。

    格式契约与 editor 共有、且处于 alpha 无兼容包袱——正是选对编码的最佳时机。建议保留通用 sentinel 方向(哪怕仍只实现正无穷),让「JSON-safe 特殊数值」成为格式层一等公民、与具体类解耦;Burst 侧只需在 $args 里放该 sentinel 即可,parser 不必认识 Burst。若坚持只服务 Burst,也至少把 "Infinity" 串契约写进 CommonSchema.tsTypeValue / Burst 相关注释,别让它只活在 parser 实现里。不阻塞。

已在上轮闭环、本轮不重提

  • CodeRabbit 在本 commit 重贴的两条(constructor/prototype 遍历可达 RCE = Critical;独立 $args 校验位置偏后 = Minor)我上轮均已评估:前者是早于本 PR 的既有可达性 + editor 信任模型下的可选加固(本轮 traversal 代码未变),后者我上轮已记为 P3。二者状态未变,不重复。

简化建议

若采纳上面 P2 的「恢复通用 sentinel」方向,_resolveValue 里那段 8 行 Burst 特判可整段删除,换成一条与类无关的 $number(或等价)分支,净复杂度更低且覆盖面更广——这本身就是更简的解。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ 1307f43d(第三轮,增量)— fix(asset): make virtual sub-asset loading reliable

总结

自上轮 84b614111线性追加一个 commit(parent 就是 84b614111,非 force-push)。这一 commit 换了完全不同的子系统——不再动 ReflectionParser,而是修 ResourceManagersub-asset 加载竞态 + 把 editor-only 的 initVirtualResources 重命名成 public registerVirtualResources。竞态修复本身方向正确、是根因层修复、反向证伪测试到位。但本 commit 顺手做的一个 Objectobject 类型收窄改动破坏了 tsc,CI 全红(build×3 / codecov / e2e 4/4 全挂在同一条 TS2322),必须先修才能合。记 [P0]

逐链核对(增量)

  • 竞态修复是根因层、非输出端擦屁股:eager 回调(_onSubAssetSuccess push)是 fast path,但 _loadMainAsset 完成时会 _releaseSubAssetPromiseCallback(baseURL) 整表删除;一个在这个 cleanup 边界之后才注册的 sub-asset 查询会永久 pending(notes 里 meshes[0][1] 症状)。新修法给每个 sub-promise 额外挂一条 mainPromise.then(r => resolve(_getResolveResource(r, _parseQueryPath(subPath))))——以权威主资源为兜底,解析不再依赖脆弱的 push 时序/cleanup 边界。这是对的落点:eager 是优化,main promise 是 source-of-truth。
  • eager 值 vs 兜底值一致:对 meshes[0][1],eager 侧 GLTFParserContext:194 emit 的是 item[i]ModelMesh),同一 .then:187 又把 glTFResource.meshes[index]=item 写回;兜底侧 mainResource.meshes[0][1] === item[1] === 同一 mesh。二者 resolve-once 竞争但产出等值,无分叉。
  • 兜底还顺带修了进度mainPromise.onProgress(setTaskCompleteProgress, setTaskDetailProgress) 把主资源进度转发进 sub-promise(onProgress 返回 this 已核)——旧 eager-only 路径下 pending 的 sub-promise 是不报进度的,这里一并接上了。
  • 反向证伪成立:新测 mock loader AssetPromise.resolve(mainAsset) 不 emit 任何 eager 回调load(".glb?q=materials[0]") 唯一能 resolve 的路径就是新兜底;revert 掉 mainPromise.then(...) 块则永久 pending → timeout fail。真守回归。
  • API rename 无破坏initVirtualResources 保留为委托 shim(转调 registerVirtualResources),in-engine 唯一调用点 ProjectLoader.ts:22 不受影响。旧实现里那段 if (element.dependentAssetMap) this._map[vp].dependentAssetMap = element.dependentAssetMapno-op_map[vp] 赋值后就是 element 本身,等于自赋值),删掉行为不变——作者删对了。
  • Git 历史透镜:Object[] 返回类型来自既有代码、非任何 fix: 引入,本轮改它不是还原历史 bug;但改错了(见下)。

问题

  • [P0] ResourceManager.ts:91 — load overload 返回类型从 Object[] 收窄成 object[] 破坏 tsc,全 CI 红。本 commit 把 AssetPromise<T | Object[]> 改成 AssetPromise<T | object[]>(同时 :47_assetUrlPoolObjectobject,那一处无害)。但 :97-98 的多资源分支 AssetPromise.all(promises) 产出 AssetPromise<T[]>T 是无约束泛型可为原始类型,string[] 可赋给 object[]object 排除原始类型),旧的 Object[](含装箱原始类型)则可赋。于是 tsc 报:

    src/asset/ResourceManager.ts(98,5): error TS2322:
      Type 'AssetPromise<T[]>' is not assignable to type 'AssetPromise<T | object[]>'.
    

    这条确定性错误让 build (mac/ubuntu/win) + codecov + e2e 1~4/4 全部失败(同一根因,packages/core b:types: tsc exit 2)。上一 commit 84b614111Object[] 时 CI 全绿,故是本 commit 引入的纯类型回归。PR 描述里"full Loader type build is blocked by stale workspace deps ... none point to touched files"——说明作者本地根本没跑出干净的 core tsc,把这条漏了过去。

    修法::91 返回类型改回 Object[](最小改动),或若坚持用 object 则改成 T | object[] 无法涵盖 T[],需要把签名写成能容纳数组分支的形态(如 AssetPromise<T | T[]> 配合两条 overload)。最省事就是回退 :91 这一处为 Object[]:47object 可保留。

已在前两轮闭环、本轮不重提

  • CodeRabbit 在本 tip 重贴的两条(constructor/prototype 遍历可达 = Critical;独立 $args 校验位置偏后 = Minor)我前两轮均已评估并闭环(前者=早于本 PR 的既有可达性+editor 信任模型下的可选加固、traversal 代码本轮未变;后者=我已记 P3)。状态未变,不重复。
  • 上轮 [P2](Burst "Infinity" 单类特判 vs 通用 sentinel)本 commit 未触碰 ReflectionParser,状态不变,不重提。

简化建议 / 观察(不阻塞)

  • [P3] notes/loader/2026-07-15-*.md:base dev/2.0 上无 notes/ 目录,本 PR 新引入的开发者工作笔记。同 #3039 惯例,这类 notes 不属于提交树、建议移除(不阻塞,仅记录,勿反复纠缠)。
  • [P3] 兜底路径对真不存在的 sub-path(_getResolveResource 无边界检查)会 resolve(undefined) 而非 reject。旧 eager-only 路径下这种 query 本就永久 stranded,所以谈不上回归、反而不再 hang;但静默返回 undefined 对调用方不友好,若要收紧可在 walk 到 undefined 时 reject 一个"sub-asset not found: {path}"。不阻塞。
  • PR 粒度:本 PR 已从"loader $type/$args/nested calls"扩到"ResourceManager sub-asset 竞态 + virtual resource public API"两个正交变更单元。回滚粒度变粗、注意力稀释。提一次即可,不强求拆分。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts (1)

43-57: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Block prototype and constructor traversal before invoking nested calls.

Loader-controlled paths can traverse function properties and reach Function or Object.prototype, for example via target: ["nested", "setValue"] or method: "constructor". Combined with call.result, this permits constructing and invoking arbitrary JavaScript or polluting prototypes.

Reject constructor, prototype, and __proto__ in both target segments and method names. Note that the validation for call.method must be positioned outside the call.target condition to ensure it is always evaluated.

🔒 Proposed guard
       chain = chain.then(() => {
+        const forbiddenKeys = new Set(["constructor", "prototype", "__proto__"]);
+        if (forbiddenKeys.has(call.method)) {
+          return Promise.reject(new Error(`Call method "${call.method}" is forbidden`));
+        }
         let target = instance;
         if (call.target !== undefined) {
           if (!Array.isArray(call.target) || call.target.some((key) => typeof key !== "string" || key.length === 0)) {
             return Promise.reject(new Error(`Call "${call.method}" target must be an array of non-empty strings`));
           }
+          if (call.target.some((key) => forbiddenKeys.has(key))) {
+            return Promise.reject(new Error(`Call target contains a forbidden key`));
+          }
           for (const key of call.target) target = target?.[key];
         }
         const method = target?.[call.method];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`
around lines 43 - 57, Update the call handling in ReflectionParser around target
traversal and method lookup to reject “constructor”, “prototype”, and
“__proto__” in every call.target segment and in call.method. Place method-name
validation before the call.target conditional so it always runs, while
preserving existing target array validation and invocation behavior for safe
names.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In
`@packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts`:
- Around line 43-57: Update the call handling in ReflectionParser around target
traversal and method lookup to reject “constructor”, “prototype”, and
“__proto__” in every call.target segment and in call.method. Place method-name
validation before the call.target conditional so it always runs, while
preserving existing target array validation and invocation behavior for safe
names.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 7ac69fe8-e090-4ace-b70a-6dd145c7c380

📥 Commits

Reviewing files that changed from the base of the PR and between 1307f43 and be65ff7.

📒 Files selected for processing (5)
  • notes/loader/2026-07-15-runtime-special-number-contract.md
  • packages/core/src/asset/ResourceManager.ts
  • packages/loader/src/resource-deserialize/resources/parser/ReflectionParser.ts
  • packages/loader/src/schema/CommonSchema.ts
  • tests/src/loader/SceneFormatV2.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/loader/src/schema/CommonSchema.ts
  • tests/src/loader/SceneFormatV2.test.ts
  • packages/core/src/asset/ResourceManager.ts

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ be65ff7e(第四轮,增量)— refactor(loader): make special number decoding generic — ✅ 三条历史 finding 全闭环,解除门控

总结

自上轮 1307f43d线性追加一个 commit(parent 就是 1307f43d,非 force-push)。这一 commit 精准地一次性关闭了我此前三轮遗留的三条 finding,且改法与我给的方向逐字一致:① 修掉上轮 [P0] 的 tsc 回归;② 采纳上轮 [P2],把 Burst 单类特判换回通用 $number sentinel;③ 采纳首轮 [P3],把 $args 独立校验前移到所有 discriminator 分支之前。逐链核对无新 P0/P1/P2,build×3 已转绿(上轮红的根因已消除),可合并。解除我上轮的 --request-changes 门控。

逐链核对(增量)

  • [P0 上轮] tsc 回归已修ResourceManager.ts:91 AssetPromise<T | object[]>AssetPromise<T | T[]>。多资源分支 AssetPromise.all(promises) 产出 AssetPromise<T[]> 现在可赋给 T | T[](旧 object[]object 排除原始类型故不可赋);单资源分支 _loadSingleItem 返回 AssetPromise<T> 同样可赋。两分支均 type-check,build (mac/ubuntu/win) 三平台已 pass,实证根因消除。注:T | T[]实现签名(对外由两条 overload load<T extends EngineObject> / load<T extends EngineObject[]> 决定),实现签名对调用方不可见,故无 API 破坏。

  • [P2 上轮] Burst 单类特判 → 通用 sentinel,逐字采纳我的建议:删掉 _resolveValueif ($type === "Burst" && args.length > 2) 那 8 行位置化特判,恢复为类型无关的 $number 分支(_resolveValue #4)。运行时结果等价:particle 测试 $args:[0.5, curve, {$number:"Infinity"}, 0.1] → 第 3 arg 经 _resolveValue 解出 Infinitynew Burst(...,Infinity,...)bursts[0].cycles===Infinity(测试 :444 反向证伪)。泛型分发器不再认识 Burst 的构造参数语义——正是上轮 P2 要消除的抽象泄漏。删掉的 'Burst $args[2] must be...' 校验丢失不是新问题:它是本 PR 自己上一 commit 84b614111 引入的、从未发版;且 source-v2 数值合法性由 builder/compiler 上游负责(PR 描述明载「Builder owns source-v2 validation」),runtime reader 本就不该做单类校验——这正是采纳 P2 的必然结果,与我上轮立场一致,不再提。

  • [P3 首轮] $args 独立校验前移已修if ("$args" in obj && !("$type" in obj)) reject 提到所有 discriminator 分支之前(:104)。现在 {$class, $args} 会先命中该校验而 reject(测试 :947 覆盖),不再静默丢 $args。前移到 $ref 之前也顺带收紧了 {$ref, $args} 这类畸形输入(editor 不产出,无回归)。!("$type" in obj) 正确放过合法的 {$type, $args}

  • $number 校验穷举无洞Object.keys(obj).length !== 1 || obj.$number !== "Infinity" → reject。{$number:"Infinity"}Infinity ✓ / {$number:"NaN"}→reject ✓(测试 :950)/ {$number:"Infinity", x:1}→keys=2→reject ✓ / {$number:-Infinity}(真数值)→!=="Infinity"→reject ✓。

  • [P3 round-2] sentinel 契约已文档化SpecialNumberValue { $number: "Infinity" } 以带 JSDoc 的 interface 回到 CommonSchema.ts,与同文件 ClassRef/TypeValue 的单行 /** */ 风格一致;零消费者与 ClassRef 同属该「格式词汇表」文件的既有惯例,非死代码。notes 文件亦把 rejected-alternative 逐条记录。

  • 注释合规parseProps JSDoc(:16) 与 _resolveValue 优先级 JSDoc(:86-96) 均同步补入 $number 并重编号至 10 条,与实际分支顺序逐条一致;$args-guard 作为前置校验门未编号(合理,它非解析分支)。CI lint 绿。

已在前几轮闭环、本轮不重提

  • CodeRabbit 的 constructor/prototype 遍历可达(Critical)+ 独立 $args 校验(Minor,已被本 commit 修)——前三轮均已评估闭环,traversal 代码本轮未变,状态不变。
  • $number 仅覆盖正无穷(YAGNI)——notes 文件已显式记录边界决策,非回归。

观察(不阻塞,勿反复纠缠)

  • [P3] notes/loader/2026-07-15-runtime-special-number-contract.md 仍是不属于提交树的开发者笔记(同 #3039 惯例)。上轮已记,仅一次性提示,不再重复。

无新阻塞项。此前 --request-changes 门控随本 approve 解除。

GuoLei1990

This comment was marked as outdated.

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ b8ce4dfa(第六轮,同 HEAD 架构/冗余/性能专项深扫)— 无 P0/P1,1×P2 + 6×P3,均不阻塞

总结

HEAD 未变(b8ce4dfa3),本轮不是增量审查,而是对整个 PR 做的架构、上下游冗余、性能专项深扫:ResourceManager 异步生命周期做了 eager 注册表 × mainPromise fallback 的全交错穷举(每种时序下用户 promise 恰好 settle 一次、注册表清理不变量成立、无新增 unhandled rejection、fallback 的 _parseQueryPath 重解析与被 shift() 消耗前的 paths 等价);ReflectionParser 对 $args/$number/call.target/props 顺序做了全输入形态复核。结论:无新 P0/P1,可合并状态不变。 分层判定正确:builder 管 source-v2 语义校验/lowering,runtime 只做格式边界 grammar 校验,不构成双源冗余;eager + authoritative fallback 双路径合理(eager 提供主资源完成前的早解析,fallback 保 liveness)。唯一的真冗余见下 [P2],删三行白赚。

问题

  • [P2] ResourceManager.ts:419-421 — mainPromise.catch((e) => this._onSubAssetFail(...)) 已被本 PR 的 authoritative fallback 彻底架空,唯一残余效果是写入毒化重试的 stale stash,建议直接删除这 3 行。逐链证据:

    1. handler 按注册顺序执行:_loadMainAsset 的 cleanup(load 创建时注册,最早)→ 此 catch(每个 sub 请求注册)→ fallback(:493,最晚)。主资源失败时 cleanup 总是先 _releaseSubAssetPromiseCallback 删掉整个 baseURL 注册表;
    2. 因此此 catch 里的 _onSubAssetFail 永远查不到 pending callback,只会走 else 分支写入一条永不清理{rejectedValue} stash(cleanup 已跑过,不会再删它);
    3. 后果:主资源失败后重试同一 ?q= URL,_createSubAssetPromiseCallback:473 命中 stale stash 瞬间拒绝——哪怕网络已恢复、fresh main load 已作为副作用启动且会成功(fresh load 完成后其 cleanup 才清掉 stash,之后的请求才正常);
    4. pending sub promise 的失败传播已由 fallback 的 reject(:493 第二参)正确完成,错误对象同源(同一 main rejection),删 catch 无任何行为损失;loader 主动上报的 eager _onSubAssetFail(如 glb 整体成功但单个子资源失败)是另一条独立路径,不受影响。

    据实标注:stash 毒化重试是 pre-existing(merge-base 上这段逐字节相同),且 pre-PR 此 catch 也从未成功 reject 过 pending entry——reject 先被 cleanup 删掉、promise 挂死,正是本 PR 所修 strand bug 的失败侧。故这不是本 PR 引入的回归,而是本 PR 让这条路由的存在价值归零:删除 = 纯简化 + 顺带修复既有的重试毒化,一举两得。不阻塞。

  • [P3] ReflectionParser.ts:48-53 — call.target 中间段为 undefined 时错误消息误导for (const key of call.target) target = target?.[key] 无逐段校验,中间段断裂(如 schema 版本漂移致 generator 不存在)与"方法真不存在于合法 owner"产出同一条 Call target does not have method "generator.emission.addBurst"——实际死在 generator,方法本身在 emission 上存在。失败仍是响亮 reject,仅调试误导。若收紧:逐段 walk 时报 Call target path segment "X" is unresolved

  • [P3] ReflectionParser.ts:140-142 — $type$args 时白付 Promise.all([]) + 一跳 microtask。旧代码在既有 .then 内同步 new Class();现在 constructorArgs 为空也绕道 Promise.all(空数组.map())。千级 $type 值的大场景(粒子模块/曲线/渐变/key)合计毫秒级纯浪费。加 if (constructorArgs.length === 0) 直构快路径即可,$args 路径保持不变。

  • [P3] ResourceManager.ts:493-500 — fallback 在 eager 已 settle 后仍无条件重跑解析。主资源完成时,每个已被 eager 提前 resolve 的 sub promise 的 fallback continuation 仍会执行 _parseQueryPath(regex .replace)+ _getResolveResource 遍历,再对已 settle 的 promise 调 resolve(no-op)。数百 sub-asset 的 glb 加载完成瞬间低个位 ms 纯浪费。查注册表项是否已清或 promise 是否已 settle 即可短路。

  • [P3] ReflectionParser.ts:78 — props→calls 串行化把「解析」与「应用」耦合,损失 block 内取资源并行度(bounded,仅记录)。顺序保证本身正确且必要(本轮维持首轮判定),但确定性只要求应用有序,不要求解析有序:现在 call args 里的 $ref 网络加载排在全部 props resolve 之后,props/calls 各引用一个慢资产的 block 加载延迟近似翻倍。跨组件并行仍保留(HierarchyParser:160 整体 Promise.all),故影响有界。将来若加载延迟敏感,标准解法是两阶段「全量并行解析 → 顺序应用」。现在不必动。

  • [P3] ResourceManager.ts:590 — registerVirtualResources 转公开 API 但无 unregister/clear 对偶@remarks 自己宣传的正是 object-URL 资源包场景:包卸载 + URL.revokeObjectURL 后,_virtualPathResourceMap 留 stale 映射(全 tree 无任何 delete/clear 写点),后续 load("virtual/path") 撞 revoked blob URL 报晦涩网络错而非清晰的 "not registered";反复切包时 map 单调增长。转公开的时刻正是定义完整生命周期的时刻,建议补 unregisterVirtualResources(paths) 或在 TSDoc 明示注册为会话级不可卸载。

  • [P3] ResourceManager.ts:628 — initVirtualResources shim 无 @deprecated 标记,ProjectLoader.ts:22 仍调旧名。双入口对同一操作永久共存且无退场信号。一行修复:@deprecated Use {@link registerVirtualResources} + ProjectLoader 迁移到新名。

简化建议

:419-421 三行(见 P2)——这是本轮唯一的结构性收益点,其余均为微优化。性能面已验证全部改动均在加载/解析路径,零每帧影响

无阻塞项。前四轮 approve 立场不变,保持可合并状态。

@GuoLei1990

Copy link
Copy Markdown
Member

对第六轮 review 中 [P3]「initVirtualResources shim 双入口」的补充建议:不必补 @deprecated,直接删掉 shim,只保留 registerVirtualResources 一个入口。下游我们会跟进适配。

消费者普查(engine tip 树 + editor 仓 + builder 导出模板 + 相关一方仓库)支持直接删除:

  • engine 仓内仅 2 处引用:定义本身 + ProjectLoader.ts:22(唯一调用点,本就带 @ts-ignore,一行迁移到新名)。本 PR 已把全部测试迁到 registerVirtualResources
  • editor 仓正身零调用——virtualPath 机制走 project.json 数据通道,由 engine 自己的 ProjectLoader 消费;builder 导出模板中也无调用。
  • 仓外真实调用方只有两个一方内部工具(旧 engine-spine 独立仓的 example、migration-agent 的 converter),均消费已发布版本,各自下次升级 engine 时一行改名即可,我们会跟进适配。其余命中均为 engine 源码的 vendored 镜像(文档站/知识库快照),随上游同步、非调用方。

删除依据:@internal 本无公开契约(删除不构成 breaking)+ alpha 期允许破坏(同 #3053 删 physics-lite 的先例)+ 一行纯委托、单调用点——该删而非该标记。落地两步:ProjectLoader.ts:22 改调 registerVirtualResources,然后整段删除 initVirtualResources

(可选项,与第六轮 P3 的 unregister 议题相关:单入口落定后,若资源包运行时消费者尚未落地,registerVirtualResources 可暂标 @internal @beta(对齐 getResourceByRef 惯例),待 unregister/生命周期补齐后一步转正,保留 API 重塑自由。)

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ 7f2bcbaa(第七轮,增量)— fix(asset): allow sub-asset retries after failure — 一次性关闭上轮两条遗留 finding,无新问题

总结

自上轮 b8ce4dfa线性追加一个 commit(gh api commits/7f2bcbaa 报 parent = b8ce4dfa,非 force-push;merge-base(dev/2.0)=b7bf6f7c,真实 PR delta 仍是 7 文件的 loader/ResourceManager/schema 单元)。这一 commit 精准关闭我上轮记录的两条 [P3]/[P2],且落点比我给的建议更彻底(根因层删除而非最小止血)。逐链核对无新 P0/P1/P2/P3,CI 全绿(build×3 / codecov / e2e 4/4 / lint 全 pass),保持可合并状态。ReflectionParser.ts/CommonSchema.tsb8ce4dfa 起零改动,无需复审。

逐链核对(增量)

  • [P2 上轮] 毒化重试 stash → 根因层删除(比我建议的「删 3 行 catch」更彻底)。上轮我只提议删 :419-421mainPromise.catch,作者直接把整套 rejectedValue stash 机制端掉:删 _onSubAssetFail 整个方法(-15行)、删 mainPromise.catch(e => _onSubAssetFail(...))、删 _createSubAssetPromiseCallback 里读 rejectedValue 的分支(现在只判 resolvedValue,命中走 AssetPromise.resolve(resolvedValue))、删 SubAssetPromiseCallbacks 类型里的 rejectedValue?: Error 字段。净 -26 行。移除完整性核实_onSubAssetFail 全 tip 树仅一个调用点(就是被删的那个 catch)——两个 loader(RenderTargetLoader.ts:51 / GLTFParserContext.ts:194/198/206)在 tip 上只调 _onSubAssetSuccess,零 _onSubAssetFail。故上轮我说的「loader 主动上报的 eager _onSubAssetFail 是另一条独立路径」据实修正为不成立:根本没有这条 loader 侧路径,_onSubAssetFail 是纯内部单点死码,删除无任何行为损失。

  • 失败传播完整保留:pending sub-promise 的主资源失败仍由 _createSubAssetPromiseCallback:474mainPromise.onProgress(...).then(resolve-fn, reject) 第二参 reject 正确完成。无 unhandled rejection:mainPromise 的两个消费者(_loadMainAsset:426promise.then(_, fail) + 此处 .then(_, reject))各自 handled,被删的 mainPromise.catch 是唯一 orphan 分支——删掉反而更干净。

  • retry-after-failure 生命周期逐帧核对(旧序 vs 新序)

    • 旧序毒化根因:主失败时三 handler 按注册序 fire——① _loadMainAsset 的 release handler(最早,在 _loadMainAsset 内注册)_releaseSubAssetPromiseCallback 删整表 → ② mainPromise.catch → _onSubAssetFail 查不到(表已删)走 else 重建 {rejectedValue} stash(cleanup 已跑过、永不再删)→ ③ sub-promise reject。重试同 URL 时 _createSubAssetPromiseCallback 先命中 stale rejectedValue 瞬间拒绝,哪怕 loader 已被重新调用且会成功。
    • 新序:stash 机制整体消失 → 主失败后 _releaseSubAssetPromiseCallback 删表即终结,无残留 → 重试时 loadingPromises[baseURL] 已删 → _loadMainAsset 重新调 loader(第二个 mock 成功)→ _subAssetPromiseCallbacks[baseURL] 为 undefined(已释放)→ 无 resolvedValue → fresh 注册 → mainPromise.then 解出 mainAsset.materials[0] = material。✅
  • 反向证伪成立:新测 retries a sub-asset after its main asset initially failsmockReturnValueOnce(reject).mockReturnValueOnce(resolve)——首 load await rejects.toThrow(),次 load(url) 断言 equal(material) + loaderSpy 恰好 2 次。revert 修复(恢复 rejectedValue stash + catch)后:旧序 stash 在 cleanup 之后重建,次 load 命中 stale stash 瞬拒 → equal(material) 断言 fail。loaderSpy 2 次那条不区分(旧序 loader 也会被重调),但 equal(material) 那条真守回归。测试构造精确。

  • [P3 上轮] initVirtualResources shim @deprecated + ProjectLoader 迁移已修:601@deprecated Use {@link registerVirtualResources}(多行 JSDoc、末尾句号合规);ProjectLoader.ts:22initVirtualResources(data.files) 迁到 registerVirtualResources(data.files),且删掉了那行上方冗余的 // @ts-ignore(public API 不再需要)。engine 仓内 initVirtualResources 现零调用(唯一调用点已迁移),shim 变纯委托保留给仓外一方工具。

关于我 round-6 后 follow-up 评论(@deprecated vs 整段删除)

我在上轮 review 后追加的评论建议「不必补 @deprecated,直接删掉 shim 只留 registerVirtualResources」。作者选择了保守中间路线(补 @deprecated + 迁移调用点,保留委托 shim)。这是可辩护的设计取舍、非缺陷:@deprecated 给出清晰退场信号,同时为「消费已发布版本的仓外一方工具」保留一个迁移窗口。既然我已发声、作者的选择合理,此点视为由作者取舍闭环,不再以任何变体重提。

已在前几轮闭环、本轮不重提

  • 上轮 4 条纯微优化 [P3](call.target 错误消息逐段校验 / $type$argsPromise.all 快路径 / fallback eager-settle 后短路 / props→calls 解析并行度 / registerVirtualResources unregister 对偶)——ReflectionParser/ResourceManager 相关行本轮未改这些点,状态不变、均已各记录一次,按「提一次即可」不再纠缠。
  • CodeRabbit constructor/prototype 遍历可达(Critical)+ 独立 $args(Minor)——前几轮已评估闭环,traversal 代码本轮未变。

无新阻塞项。此前 --request-changes 门控已于 round-4 approve 时解除,保持可合并状态。

@GuoLei1990 GuoLei1990 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查 @ aa623272(第八轮,增量)— refactor(asset): remove legacy virtual resource API — 删除死 shim,收敛到我 round-6 建议的终点,无新问题

总结

自上轮 7f2bcbaa线性追加一个 commit(gh api commits/aa623272 报 parent = 7f2bcbaa,非 force-push;merge-base(dev/2.0)=2f88c2fab)。这一 commit 只做一件事:删掉 initVirtualResources@internal @deprecated 委托 shimcompare 报 +0 -8,仅动 ResourceManager.ts 一个文件)。这正是我 round-6 follow-up 评论提的方向——上轮作者选了保守中间路线(补 @deprecated + 迁移调用点、保留委托 shim),我当时判为「作者取舍闭环」;本轮作者进一步收敛到更干净的终点。逐链核对无新 P0/P1/P2/P3,CI 全绿(build×3 / lint / e2e pass,codecov 进行中),保持可合并状态。

逐链核对(增量)

  • 删除完整性核实——shim 是纯死码,删除无行为损失

    • 引擎仓内 initVirtualResources零调用——在 tip aa623272 上,唯一的 in-engine 调用点 ProjectLoader.ts:21 已在上轮迁移为 engine.resourceManager.registerVirtualResources(data.files),本轮 git grep 全 tree 仅 registerVirtualResources 命中、initVirtualResources 零命中。
    • VirtualResource 类型未被孤立:删 shim 后 ResourceManager.ts 仍在 4 处使用 VirtualResource:327 _resolveLoadItemOptions 参数、:558 _virtualPathResourceMap 字段、:565 registerVirtualResources 参数、:633 interface 定义),导出面 core/src/index.ts:30export { ResourceManager, resourceLoader, type VirtualResource }。故删除不产生死导入。
    • 非破坏性变更:被删方法带 @internal 标记(round-7 diff 已核),从未是发布 API 表面——core/src/index.ts 从来只 named-export ResourceManager/VirtualResource,从不 export initVirtualResources。仓外一方工具若消费的是已发布版本,走的仍是那些历史版本里的 shim;本 PR 引入的 registerVirtualResources 本就是新 public API,删掉本 PR 自己刚加的过渡 shim 不影响任何已发布契约。
  • 收敛判定(非社交护面子,据实修正):round-7 我把「保留 @deprecated shim」记为「可辩护的设计取舍、非缺陷」,并声明「不再以任何变体重提」。作者本轮主动删到底,与我 round-6 的原始建议一致。这不是我重提——是作者自行收敛,我据实记录终点更干净即可。整个 PR 现回到「新增一个 public API + 唯一 in-engine 调用点直接用新名、零遗留 shim」的最简形态。

已在前几轮闭环、本轮不重提

  • 上轮全部 finding(毒化重试 stash 根因层删除 / @deprecated + ProjectLoader 迁移 / tsc 回归 / Burst 单类特判→通用 sentinel / $args 前移 / 缺失 sub-path reject / notes 删除)——ResourceManager 相关行本轮只删了这 8 行 shim,其余零改动,状态不变。
  • 4 条纯微优化 [P3](call.target 逐段校验 / 空 $args 快路径 / fallback eager-settle 短路 / props→calls 解析并行度)——ReflectionParser.ts/CommonSchema.tsb8ce4dfa 起零改动,均已各记录一次,不再纠缠。
  • CodeRabbit constructor/prototype 遍历可达(Critical)+ 独立 $args(Minor)——前几轮已评估闭环,traversal 代码本轮未变。

无新阻塞项。此前 --request-changes 门控已于 round-4 approve 时解除,保持可合并状态。

@GuoLei1990
GuoLei1990 merged commit 9079e7f into galacean:dev/2.0 Jul 16, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants