Skip to content

perf(cli): import core modules directly instead of the package root - #10957

Open
yiliang114 wants to merge 31 commits into
mainfrom
perf/core-subpath-imports-batch3
Open

perf(cli): import core modules directly instead of the package root#10957
yiliang114 wants to merge 31 commits into
mainfrom
perf/core-subpath-imports-batch3

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Carries the whole change: the resolver mapping, the import migration that survived a full test run, two mock moves, and the two resolution fixes the integration gate turned out to need. Supersedes #10946 and #10956, both closed.

What this PR does

  • Teaches the cli test runner to resolve individual core modules, by expressing its alias list as an ordered array with a pattern entry.
  • Moves 109 cli modules from importing the core package root to importing the modules they use — about a quarter of the 450 that do.
  • Moves two mocks onto the modules they actually stub, so the code they exercise could move with them.
  • Adds a check that the published package can still be reached by module path, and teaches the utils-layer lint rule about the new exports pattern.
  • Adds the wildcard path mapping the integration suite was missing, and a catch-all to the core package's exports.

Why it's needed

Importing from the package root evaluates core's whole export graph — a bit over six hundred modules — however little of it a file uses. The cost shows up as collection rather than assertions: the cli workspace reported 2223s collecting against 1372s running tests on the release lane, and 84 minutes on its own on the main lane with collection at roughly three times the test time.

Sharding cannot reach this. The three release shards finish within a minute of each other, so the split is balanced and adding shards only divides a fixed per-file cost — one that grows with every test file, and the suite has grown 87% in ten weeks.

Measurements and the wider plan are in #10908.

What the first full run changed

This started as four stacked PRs. CI here runs on pull requests against main and release/** only, so the three stacked ones were reporting seven or eight passing checks that were just the TUI gates and the bot jobs — no unit suite, no lint. Retargeting this one at main ran the suite for the first time and it failed: 16 files, 127 tests, against 1 file and 2 tests on a jsdom PR sharing the same base.

Every failure was a stub that stopped intercepting. Three mechanisms, only one of which the analysis behind the original batches modelled:

  • a spy planted on a namespace import — vi.spyOn(cliCore, 'getMCPServerPrompts') does nothing for a module that imported the function directly;
  • a suite asserting its debug logger was called, where the code now takes the real one;
  • a suite expecting stubbed git output and getting real output.

Six of the sixteen install their stub in a way the analysis could not identify at all. So rather than sharpen a heuristic that was wrong in a way local reading would never catch, every migrated module any failing suite reaches was restored — 146 of them — leaving the 110 here. Some of those 146 are certainly fine; separating them belongs on top of a green run.

The integration gate then failed to compile: Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js'. Only packages/cli maps these specifiers. The integration suite lists the eight named subpaths and no wildcard, and the package's exports map covers those same eight plus the dist and src trees — so anything resolving the ordinary way, that suite and any consumer of the published package alike, could not name a core module. Both gaps are closed here, and Serve A/B, which was failing for the same reason, went green with them.

Reviewer Test Plan

How to verify

Green across the unit suite, lint, the integration gate and Serve A/B — all of which were red at some point in this PR's history and each of which caught something real.

Three things worth a reviewer's judgement rather than a check:

  1. Alias ordering. The eight named subpaths must stay ahead of the pattern entry, since none of their targets can be derived from the specifier, and the package root must be matched exactly — as a string it would swallow every subpath beneath it.
  2. The exports catch-all. ./* maps a bare module path onto the build output. It exposes nothing new; ./dist/* already reaches the same files.
  3. The two mock moves. Each names the module declaring every symbol its factory stubs. A mismatch shows up as the mock silently not applying, so what confirms them is that the test still fails when the stubbed behaviour is removed.

Evidence (Before & After)

N/A — no user-visible behavior changes. The shipped CLI is a single bundle and never resolves these specifiers at runtime.

Tested on

OS Status
🍏 macOS
🪟 Windows
🐧 Linux

Via CI.

Risk & Scope

  • Main risk or tradeoff: a module carrying an initialization side effect that previously ran because the package root was evaluated. Nothing surfaced across a full run on three platforms, and it would fail a test rather than change production behavior.
  • Not validated / out of scope: the 341 files that still import the package root for a value — 146 restored from the first attempt, the rest held by mocks. A further 101 import it for types only, which costs nothing and needs no change. The bundle's size was not measured; the build already resolved these specifiers through the same path mapping, so its shape is unchanged.
  • Breaking changes / migration notes: none.

Linked Issues

Refs #10908

中文说明

本 PR 承载全部改动:解析映射、经完整测试验证后保留下来的导入迁移、两处 mock 搬迁,以及集成门禁暴露出的两处解析修复。取代 #10946#10956(均已关闭)。

这个 PR 做了什么

  • 把 cli 测试运行器的 alias 列表改成有序数组并加入通配规则,使其能解析 core 的单个模块。
  • 将 109 个 cli 模块从「导入包根」改为「导入实际用到的模块」——约占仍从包根导入的 450 个中的四分之一。
  • 将两处 mock 搬到它们真正 stub 的模块上,使被测代码得以一同迁移。
  • 新增「发布包仍可按模块路径访问」的检查,并让 utils 层 lint 规则认识新的 exports 模式。
  • 补上集成用例缺失的通配 path 映射,以及 core 包 exports 的兜底条目。

为什么需要

从包根导入会求值 core 的整个导出图(六百多个模块),无论调用方实际用到多少。代价体现在模块收集而非断言:release lane 上 cli 收集耗时 2223s、跑测试 1372s;main lane 上 cli 单独就要 84 分钟,收集约为测试时间的三倍。

分片解决不了:三个 release 分片耗时相差不到一分钟,说明切分已均衡,加分片只是摊薄一个固定的单文件成本——而它随每个新增测试文件增长,套件十周内长了 87%。

数据与整体方案见 #10908

第一次完整运行改变了什么

本改动最初是四个叠加 PR。此仓库 CI 只对 base 为 mainrelease/** 的 PR 触发,因此那三个叠加 PR 显示的七八项通过只是 TUI 门禁和机器人任务——没有单元测试,没有 lint。把本 PR 改到 main 后套件第一次真正运行,结果失败:16 个文件、127 个测试;而共享同一基线的 jsdom PR 只挂 1 个文件 2 个测试。

所有失败都是「stub 不再拦截」,涉及三种机制,而原批次背后的分析只建模了其中一种:打在命名空间导入上的 spy(vi.spyOn(cliCore, 'getMCPServerPrompts') 对直接导入该函数的模块无效);断言 debug logger 被调用、而代码拿到真实实现;期望 stub 的 git 输出、却拿到真实输出。

十六个用例中有六个,其 stub 安装方式该分析完全无法识别。与其继续打磨一个「本地读代码根本发现不了其错误」的启发式,不如回退所有被失败用例触达的已迁移模块(146 个),保留此处的 110 个。那 146 个中必然有一些本无问题,但把它们摘出来应建立在一次绿色运行之上。

随后集成门禁编译失败:Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js'。只有 packages/cli 映射了这类说明符;集成用例只列了八个具名 subpath 而无通配,包的 exports 也只覆盖那八个加 dist 与 src 两棵树——因此任何按常规方式解析的一方,无论是该用例还是已发布包的消费者,都无法指名 core 的某个模块。两处缺口在此补齐;因同一原因失败的 Serve A/B 也随之转绿。

风险与范围

  • 主要风险或权衡:某模块可能带有「原先随包根求值而发生」的初始化副作用。三平台完整运行未暴露此类问题,且它会表现为测试失败而非生产行为改变。
  • 未验证 / 超出范围:仍有 341 个文件从包根做值导入——其中 146 个是首次尝试后回退的,其余被 mock 挡着。另有 101 个仅做类型导入,零成本、无需改动。bundle 体积未测量;构建本就通过同一套 path 映射解析这些说明符,其形态不变。
  • 破坏性变更 / 迁移说明:无。

…files importing the whole package

Importing from the core package root pulls in its entire export graph — a bit
over six hundred modules — however little of it a file actually uses. In a
release run the cli workspace spent 2223s collecting modules against 1372s
running tests, and a file that imports the package root costs about 11.5s
before its first assertion where one importing a single module costs about 2s.

cli's tsconfig already maps a wildcard subpath onto core's sources, so esbuild
resolves per-module imports when it bundles. Vitest does not read tsconfig
paths, and the alias list that stands in for them named only four subpaths, so
those imports did not resolve under test at all. This adds the wildcard there.

Expressing the alias list as an ordered array is what allows a pattern entry.
The package root has to become an exact match in the process: as a string it
would also match everything beneath it and rewrite each subpath into a path
under index.ts.

Two files move to per-module imports as a first check that the mapping holds
end to end. Both were picked because nothing that depends on them replaces the
core package with a mock factory — where a test does that, the mock stops
intercepting once the code under test imports the module directly, so those
call sites need their mocks moved in the same change and are left alone here.
The previous commit was assembled from a working copy that predated main by
several weeks, so it silently reverted this file to that older state. Four
named core subpaths added since — envVarResolver, noFollowOpen,
subSessionConstants and toolWriteOrigin — disappeared with it, and the new
wildcard then claimed those specifiers and pointed them at files that do not
exist. 257 test files failed to load as a result.

All eight named subpaths are restored and kept ahead of the wildcard, with a
comment saying why that order matters and what a contributor adding a ninth
has to do. None of the eight can be derived from its specifier, so none of
them can be folded into the pattern.

The two migrated source files are rebuilt on their current contents for the
same reason; one of them had also been reverted by a line.
Importing from the core package root evaluates its whole export graph — a bit
over six hundred modules — however little of it a file uses. On the release
lane the cli workspace spends more time collecting modules than running tests,
and on the main lane it now takes 84 minutes on its own, most of it collection.

These 130 files ask for named modules instead. They were chosen by checking,
for every test whose module graph reaches them, whether that test replaces the
core package with a mock: a test that swaps the package wholesale stops
intercepting once the code under test imports a module directly, and a test
that spreads the real package and overrides a few names only matters if one of
those names is what the file imports. Files with either kind of coupling are
left for a later change that moves the mocks at the same time.

Only import statements move; every other line is byte-identical.
…nd wrap long imports

Two problems with the previous commit, both found by CI.

The symbol map resolved a re-exported name to the module that re-exports it
rather than the one that declares it, so `ProviderModelConfig` was asked of
`models/types` when it is declared in `providers/types`, and the build failed
to typecheck. A checker now confirms, for every generated specifier, that the
named module really does export that symbol — following its own re-exports —
and it reports one bad pair out of 492.

The formatting pass that was supposed to run over these files had silently
done nothing: invoked from the repository root against paths outside it,
Prettier skips the files and still reports success, so long import statements
went out unwrapped. Rerunning it properly reflows 47 files.

Two files are left with an over-long line Prettier would wrap, because that
line is over-long on the base commit too and the lint gate does not flag it;
reformatting it here would be unrelated noise. Every other line outside an
import statement stays byte-identical.
The same mechanical change as the previous commit, over the files a corrected
reading of the test suite showed were always safe to move.

The earlier pass classified a test as replacing the core package if the text
of such a call appeared anywhere in it, including inside a comment. One file
only mentions the pattern in a doc comment explaining why it deliberately
avoids it, and being counted as a blocker there ruled out 217 modules that
nothing actually blocks. Ignoring comments when detecting the call raises the
number of files movable without touching a single test from 141 to 260.

Every generated specifier is checked against the exports its named module
really has, following that module's own re-exports — 865 pairs here, none
wrong. Outside import statements every line is byte-identical.
Where a test replaces the whole core package with a factory, the code under
test cannot move to per-module imports on its own: the mock would stop
intercepting and the real implementation would load instead, quietly changing
what the test exercises while leaving it green. The mock has to move in the
same commit.

These three are the cases where that is unambiguous — every name the factory
stubs is declared in one module, and the code under test imports exactly those
names. Each pair moves together onto that module.

The pattern generalises: about sixty tests each hold back one or two modules
this way, and roughly ninety more modules are held by several tests at once
and need them changed together. Establishing the shape on the clean cases
first keeps the ambiguous ones honest.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@yiliang114
yiliang114 changed the base branch from perf/core-subpath-imports-batch2 to main September 3, 2026 17:20
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Retargeted to main so this actually gets tested.

The repository's CI runs on pull_request only for main and release/**, so the three PRs stacked under this one (#10946, #10956, and this one before the change) were only picking up the TUI gates and the bot jobs — no unit suite, no lint. The whole argument for these changes is that a wrong import fails loudly in CI, which does not hold if CI never runs.

With main as the base this shows the cumulative tree — the resolver mapping from #10917, both import batches, and the three mock moves — and gets the full run. The diff shrinks on its own as the PRs below land.

Reviewing is still easier one layer at a time:

scope files
#10917 resolver mapping + two files as a smoke test 3
#10946 first import batch 130
#10956 second import batch 114
this three mock moves 6
中文说明

改 base 到 main,否则这些改动根本没被测到。

仓库的 CI 在 pull_request 上只对 mainrelease/** 触发,所以叠在下面的 #10946#10956(以及改之前的本 PR)只跑到了 TUI 门禁和机器人任务——没有单元测试,没有 lint。这批改动的全部论据是「写错的导入会在 CI 里大声失败」,而 CI 压根没跑的话,这个论据不成立。

base 改成 main 后,本 PR 展示的是累积状态(#10917 的解析映射、两批导入迁移、三处 mock 搬迁),并能拿到完整的 CI 运行。下面的 PR 合并后,这里的 diff 会自动缩小。

审阅仍建议逐层来看,各层范围见上表。

@yiliang114 yiliang114 closed this Sep 3, 2026
@yiliang114 yiliang114 reopened this Sep 3, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review was cancelled before a review could be posted. Nothing failed and nothing is retried automatically: the run was cancelled — by an operator, an upstream event, or the job exceeding its execution time limit. If you still want a review of this PR, request one with @qwen-code /review. See workflow logs.

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR — and for publishing the measurement that undercuts your own headline. That comment is the most useful thing on this thread.

Template looks good ✓ — every required heading is present, including Evidence (Before & After) and the Chinese section.

Problem: real and measured, not theoretical. Collection dominating assertion time (2223s collecting vs 1372s running on the release lane, ~84 minutes on the main lane), the suite up 87% in ten weeks, and sharding ruled out with an actual argument — the three release shards finish within a minute of each other, so the split is already balanced and more shards only divide a fixed per-file cost. That is an observed problem with a tracked plan (#10908).

But the problem this PR solves is smaller than the problem it is titled for. Your own measurement two days after opening: 109 modules moved, 24 of 1003 test files got cheaper, ~228s of CPU ≈ a minute of wall across four workers against an 84-minute lane — under 2%, inside run-to-run spread, your words being "Do not expect this PR to move the number." Nothing in the CI evidence on this head contradicts that, and nothing confirms a win either. So: problem exists ✓, this slice's contribution is not observable.

Direction: in scope — this is test-infrastructure cost, and the mechanism you land (resolver mapping, an exports entry with a check standing on it, the lint rule taught the new shape) is the part that has to exist before any later batch can. Two things push it to a maintainer rather than a gate, and I want to name both rather than wave them through:

  • packages/core/package.json is the published package's public contract. I checked the surface claim and it holds: ./*./dist/src/* reaches no file that ./dist/* did not already reach, exact keys still beat patterns, and ./dist/* / ./src/* have longer prefixes so nothing is shadowed. What I cannot settle from the diff is the design consequence — after this, every internal core module spells like a supported subpath (.../config/storage.js) instead of an obviously-internal one (.../dist/src/config/storage.js), so a future reorganisation of packages/core/src/ becomes a breaking change for whoever discovered the clean spelling. That is a judgement about how much of core you want to freeze, not a correctness question.
  • Stage 1e matched packages/cli/src/serve/sandbox.ts, a path this repo's revert history flags as high-risk. I read the change: it is import-only, and all five symbols land on plausible modules. tsc --build runs inside CI's npm ci, so a wrong module would have failed the ubuntu Test job — it did not. Flagging it so a human knows where to look, not because I think it is broken.

Size: 129 files, 1853 changed lines (+1347 / −506). Breakdown, because the shape matters more than the total:

slice files lines
the mechanical sweep (packages/cli/src/**, non-test) 109 669
new machinery (vitest aliases, exports entry, check script, lint rule, tsconfig, dev loader, CI step) 10 615
tests (3 new sync/gate tests + 3 touched suites + dev/lint rule tests) 10 569

So ~1184 lines of machinery and tests carry a 669-line sweep averaging 6 lines a file. Stage 0: packages/core/src/** is untouched — only core's package.json, one line — and you have admin on this repo, so the two-tier core gate does not apply to you and the title is perf, not refactor, so Tier 1 would not have fired regardless. The 1000+ production-line advisory does apply (1284 lines): informational, and I would not split this one, because the mechanism and the sweep do have to agree on a resolver.

Approach: the honest question is whether the sweep belongs in this PR at all. Your own history answers it — the first full run broke 16 files and 127 tests, and you restored 146 of the 255 migrated modules rather than sharpen a heuristic that local reading could not validate. You then wrote that selecting batches by "which modules are safe to move" is the wrong unit, and that "which test graphs can be made entirely clean" is the right one. If that is true, this PR is a batch chosen by the unit you have since rejected, and it is the batch that produced the breakage.

Cut to 20% and the mechanism still stands on its own: the ordered alias array with the wildcard, the ./* exports entry, check:core-subpath-exports and its CI step, the lint rule learning the new shape, and the sync tests. That is the part later batches depend on and the part nothing else exercises. The 109-module sweep is what can be re-sequenced into graph-complete batches, each measured, each paying for itself instead of 24 files out of 1003. Worth saying plainly: I am not sure the sweep earns its 669 lines plus the 146 it dragged back.

Two smaller notes, neither a blocker — the body says "two mock moves" and the diff has three, and Tested on claims macOS ✅ / Windows ✅ "Via CI" while both of those Test jobs are skipped on the head I reviewed (detail in the Stage 2 comment).

Risk: one Stage 1e match (packages/cli/src/serve/sandbox.ts, import-only), so I ran the full Stage 2 pass with no enrichments skipped and required real CI evidence before considering approval. No elevated risk beyond that.

⏸️ Escalating the direction question to a maintainer rather than deciding it here — Stage 3 carries the reasoning and the mention.

中文说明

感谢这个 PR——也感谢你公开了那个削弱自己标题的测量数据。那条评论是整个 thread 里最有价值的东西。

模板完整 ✓ —— 所有必需标题都在,包括 Evidence (Before & After) 和中文部分。

**问题:**真实且经过测量,不是理论性的。收集时间压过断言时间(release lane 上收集 2223s vs 运行 1372s,main lane 约 84 分钟),测试套件十周内增长 87%,并且用实际论证排除了分片方案——三个 release 分片彼此在一分钟内完成,说明切分本身是均衡的,增加分片只是把一个固定的「每文件成本」再除一次。这是一个有观测证据、有跟踪计划(#10908)的问题。

但本 PR 实际解决的问题,比它标题所声称的要小。你自己在开 PR 两天后的测量:迁移了 109 个模块,1003 个测试文件里只有 24 个变便宜,约 228s CPU ≈ 四个 worker 下一分钟墙钟,对着 84 分钟的 lane——不到 2%,落在运行间波动范围内,你的原话是「不要指望本 PR 让数字变好看」。当前 head 的 CI 证据既没有反驳这一点,也没有证明任何收益。所以:问题存在 ✓,但这一片的贡献不可观测。

**方向:**在范围内——这是测试基础设施成本,而你落地的机制(解析映射、有检查兜底的 exports 条目、认识新形态的 lint 规则)正是后续任何批次都必须先存在的部分。有两点让它应该交给 maintainer 而不是由 gate 决定,我想明确点出来而不是含糊放过:

  • packages/core/package.json 是已发布包的公共契约。我核对了「表面未扩大」这个说法,它成立:./*./dist/src/* 没有触达任何 ./dist/* 原本触达不到的文件,精确键仍然优先于模式键,且 ./dist/* / ./src/* 前缀更长,因此没有任何条目被遮蔽。我无法从 diff 判断的是设计后果——此改动之后,core 的每个内部模块都长得像受支持的子路径(.../config/storage.js),而不再像显然内部的写法(.../dist/src/config/storage.js),于是将来重新组织 packages/core/src/ 对发现这个干净写法的人来说就变成破坏性变更。这是关于「你想冻结 core 多少」的判断,不是正确性问题。
  • Stage 1e 命中 packages/cli/src/serve/sandbox.ts,这是本仓库回滚历史标记为高风险的路径。我读了改动:纯导入重写,五个符号都落在合理的模块上。CI 的 npm ci 里会跑 tsc --build,所以模块指错会让 ubuntu Test job 失败——它没有失败。点出来是让人类知道该看哪里,不是我认为它坏了。

**规模:**129 个文件,1853 行改动(+1347 / −506)。拆分如下,因为形状比总数更重要:

部分 文件数 行数
机械式迁移(packages/cli/src/**,非测试) 109 669
新增机制(vitest alias、exports 条目、检查脚本、lint 规则、tsconfig、dev loader、CI 步骤) 10 615
测试(3 个新增 sync/gate 测试 + 3 个被改动的套件 + dev/lint 规则测试) 10 569

即约 1184 行机制与测试,承载一次平均每文件 6 行、共 669 行的迁移。Stage 0:packages/core/src/** 完全没动——只动了 core 的 package.json 一行——而你在这个仓库有 admin 权限,因此两层核心门禁对你不适用;标题是 perf 而非 refactor,Tier 1 本来也不会触发。1000+ 生产行的大 PR 提示确实适用(1284 行):仅供参考,而且这一个我不建议拆分,因为机制与迁移必须在同一个解析方案上达成一致。

方案:真正该问的是这次迁移是否应该留在这个 PR 里。你自己的历史已经回答了——第一次完整运行弄坏了 16 个文件、127 个测试,你选择把 255 个已迁移模块中的 146 个还原,而不是去打磨一个本地阅读无法验证的启发式。你随后写道,以「哪些模块可以安全迁移」来选批次是错误的单位,正确的单位是「哪些测试图能被彻底清干净」。如果这是对的,那本 PR 就是一个用你已经否定的单位选出来的批次,而且正是产生了那次破坏的批次。

砍到 20%,机制本身依然站得住:带通配的有序 alias 数组、./* exports 条目、check:core-subpath-exports 及其 CI 步骤、学会新形态的 lint 规则,以及各 sync 测试。这部分是后续批次所依赖的、也是别处完全不会触达的。而 109 个模块的迁移,恰恰是可以按「整张图清干净」重新编排的部分——每批都能被测量、都能自己付清成本,而不是 1003 个文件里只便宜 24 个。直说:我不确定这次迁移对得起它的 669 行,外加它拖回来的那 146 个。

两个更小的点,都不是阻塞项——正文写「两处 mock 搬迁」而 diff 里有三处;Tested on 声称 macOS ✅ / Windows ✅「Via CI」,但在我审查的这个 head 上这两个 Test job 都是 skipped(细节见 Stage 2 评论)。

**风险:**一个 Stage 1e 命中(packages/cli/src/serve/sandbox.ts,纯导入),因此我跑了完整的 Stage 2、没有跳过任何 enrichment,并且在考虑批准前要求真实的 CI 证据。除此之外没有额外风险。

⏸️ 方向问题上交 maintainer,而不是在这里下结论——推理与 @mention 在 Stage 3。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Code review

No Critical findings. I went looking for the two ways this class of change breaks — a resolver that mis-orders, and an import that names a module which does not declare the symbol — and neither is present. What follows is what I verified, then the two things I would actually change.

Verified correct, not assumed:

  • Alias ordering. The named subpaths sit ahead of the wildcard, and the package root is matched by RegExp rather than string. That last part is the subtle one and the code gets it right: @rollup/plugin-alias treats a string find as matching when the importee equals it or starts with it followed by /, so '@qwen-code/qwen-code-core' as a string would have swallowed every subpath and rewritten it into a path under index.ts. Your comment says exactly this. The wildcard and the exact root cannot collide either, since the wildcard requires a / after core.
  • resolveExportTarget matches Node's real algorithm. Longest literal prefix wins, ties broken by the longer full key, keys with zero or two * skipped, captured segment substituted into the target. I checked the tie-break specifically because it is the kind of thing that looks right and is off by one comparison: best.pattern.length >= pattern.length keeps the earlier key on a tie, which is what Node does. Without this the lint rule would go blind the moment ./* landed, so it is load-bearing rather than decorative.
  • ./* shadows nothing. Exact keys always beat patterns, and ./dist/* / ./src/* carry longer prefixes than ./*, so all three pre-existing behaviours survive. The surface claim holds too — ./dist/* already reached every file ./* now reaches, so the reachable set is unchanged and only the spelling is new.
  • The new index.js barrel entry belongs there. ./* maps @qwen-code/qwen-code-core/index.js onto dist/src/index.js, and packages/core/src/index.ts is a barrel — so without adding that specifier to CORE_BARREL_SPECIFIERS, the ./* entry would have opened a second door to the exact thing the rule exists to block. Good catch, and easy to miss.
  • All 109 migrations are symbol-correct, including the type-only ones. This is the finding I most expected to make and could not. npm ci in the Test job runs preparenpm run build → per-package tsc --build, and QWEN_SKIP_PREPARE is never set in ci.yml, so typechecking does run in CI even though npm run typecheck appears nowhere in the workflow. Test (ubuntu-latest, Node 22.x) is green, which means SandboxConfig really is in config/config.js, resolveBundleDir really is in utils/bundlePaths.js, and the AgentStatus-from-the-wrong-module class of error you hit during the merge conflict is excluded across the whole sweep. Vitest alone would not have caught the type-only ones — esbuild erases them.

Suggestion — the new check is more load-bearing than your description says. Risk & Scope rests on "The shipped CLI is a single bundle and never resolves these specifiers at runtime", and that is only true of the esbuild standalone. bin.qwen points at packages/cli/dist/index.js, compiled from packages/cli/index.ts, which imports ./src/cli.js — so the published npm entrypoint runs tsc output that keeps @qwen-code/qwen-code-core/config/storage.js verbatim and lets Node resolve it against core's exports map at startup. @qwen-code/qwen-code-core is not in esbuild's external list, so the bundle genuinely does inline core; the npm package genuinely does not. Worth correcting in the body, because right now the description argues against the strongest justification for both the ./* entry and the CI step: without them the installed CLI dies with ERR_MODULE_NOT_FOUND on its first subpath import while every in-repo suite stays green, which is precisely what the check's own fileoverview says.

Suggestion — gating asymmetry. You built three gates and left two maps ungated, and said so yourself in the code:

hand-synced map gate
integration-tests/tsconfig.json named paths integration-tsconfig-core-paths-sync.test.js
skill-review-harness loader named map text-capture-core-loader-sync.test.js
published resolution of every specifier in cli/acp-bridge/sdk src check:core-subpath-exports + CI step ✓
packages/cli/vitest.config.ts named aliases none — "kept in sync by hand"
packages/cli/tsconfig.json paths ↔ the vitest alias array none — "kept in sync by hand"

The ungated one is the most-exercised map in the repo: every cli test run goes through it. To be fair about the failure mode, it is loud rather than silent — a new named key whose target stem differs from the key falls through the wildcard to packages/core/src/<key>, that file does not exist, Vite fails resolution, and Test (ubuntu) goes red. So this is not a blocker and I am not calling it a correctness hole. But the two gated siblings already contain the exact derivation needed (read core's exports map, skip keys whose stem mirrors the key, require an explicit entry for the rest), and the harness one is gated for a loader that your own comment notes no CI test executes. The map that runs thousands of times a day is the one with no gate. That is the inconsistency a future contributor will copy.

Note — three mock moves, not two. StandaloneSessionPicker.test.tsxutils/gitUtils.js, useGitBranchName.test.tsutils/gitDirect.js, useMemoryMonitor.test.tsutils/debugLogger.js. The added mock wiring test in the first of those is the right shape: it asserts vi.isMockFunction(getGitBranch) and that the fixture actually returns, so moving the mock back to the package root fails it. That is the failure class that broke 16 files on the first run, and it is now pinned where it happened. Its scope caveat is honest too — it covers the suite's own import, not StandaloneSessionPicker.tsx, and says so instead of implying more.

Files changed (13 of 129 shown)
File What changed
packages/cli/vitest.config.ts The resolver: object alias map becomes an ordered array so a RegExp wildcard can exist. Named core subpaths first, then the wildcard, then the package root matched exactly. 339 lines, mostly the pre-existing acp-bridge and sdk entries being re-spelled into the new shape.
packages/core/package.json One line: the ./ to ./dist/src/ catch-all on the published exports map.
scripts/check-core-subpath-exports.mjs New 124-line gate. Collects every core subpath specifier in cli, acp-bridge and sdk source, runs Node's real resolver against the built package, and rejects anything that lands outside the published dist.
eslint-rules/no-core-utils-upward-import.js Adds resolveExportTarget, a reimplementation of Node's exports pattern matching, so the rule still sees deep specifiers now that the catch-all resolves them.
integration-tests/terminal-capture/skill-review-harness/text-capture.tsx The manual harness's ESM loader hook learns subpath specifiers, with a hardcoded named map for the nine keys that do not mirror their file.
integration-tests/tsconfig.json Adds the core wildcard plus three named entries whose specifier does not name their file, and rewrites the comment explaining why those entries must not be deleted.
.github/workflows/ci.yml New step that builds core and runs the exports check, gated on the full CI profile.
scripts/dev.js The dev loader redirects subpath specifiers at core source, but only when the source file exists — otherwise a mismatch silently mixes live root with stale dist.
eslint-rules/no-core-root-barrel-import.js Adds index.js to the barrel specifier set, since the catch-all now reaches the src barrel through it.
package.json Wires the new script.
packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx Mock moves to utils/gitUtils.js, plus the new test pinning that the alias and the mock agree.
packages/cli/src/ui/hooks/useGitBranchName.test.ts Mock moves to utils/gitDirect.js alongside the hook it stubs.
…and 117 more files 109 cli source files carrying the same import rewrite (669 lines, ~6 each), 3 new gate/sync tests, 5 existing script tests updated, one more mock move in useMemoryMonitor.test.ts.

Testing

This was an unattended CI run (workflow_dispatch), so per the skill's rules I did not build or execute anything from this PR — no npm, no vitest, no checkout. Everything below is the PR's own CI, read through the API for the exact commit in the footer. What I carried: real check names, real conclusions, and step-level conclusions for the two jobs that matter here.

The signal is strong on Linux and thin everywhere else. Test (ubuntu-latest, Node 22.x) covers more than it looks: its Install dependencies step is npm ci, which runs preparenpm run buildtsc --build, so a green job is also a green typecheck of all 109 migrations. Its Run tests and generate reports step is test:ci:workspaces followed by test:scripts, so the three new sync tests ran there and passed. On Lint & Static (ubuntu-latest, Node 22.x) I checked the step list rather than trusting the job conclusion — Check core subpath exports resolve = success, meaning the new gate actually executed on this head and was not skipped by its ci_profile == 'full' condition.

Check Conclusion
Test (ubuntu-latest, Node 22.x) success
Test (macos-latest, Node 22.x) skipped
Test (windows-latest, Node 22.x) skipped
Lint & Static (ubuntu-latest, Node 22.x) success
Integration Tests (no-AK, No Sandbox) success
Integration Tests (CLI, No Sandbox) skipped
Serve A/B (ubuntu-latest, Node 22.x) success
TUI parity snapshots (ink vs opentui) success
OpenTUI no-flicker gate success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
Real daemon E2E / Java 11 success
ubuntu-latest / Java 11 success
ubuntu-latest / Java 17 success
ubuntu-latest / Java 21 success
macos-latest / Java 21 success
windows-latest / Java 21 success

The gaps, stated plainly:

  • Not verified: macOS and Windows on this head. Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) are both skipped. The body's Tested on table claims macOS ✅ and Windows ✅ "Via CI"; on cdabd7dad neither ran. Those claims may well have been true of an earlier head on the release lane, but they are not evidence for this commit.
  • Not verified: the CLI integration suite. Integration Tests (CLI, No Sandbox) is skipped. This matters more than it usually would, because the PR description says the integration gate is what surfaced the missing wildcard and the exports gap in the first place. Integration Tests (no-AK, No Sandbox) did run and is green, and npm run typecheck:integration lives in that job, so the tsconfig.json wildcard is covered — but the suite that originally caught the bug is not the one that ran.
  • Not verified: any performance improvement. No check on this head measures collection time, and a green suite cannot show a speedup. This is the PR's central claim and CI does not touch it.

Nothing red to attribute, so no log excerpt to quote — the only non-success conclusions on this commit are skipped, and those are the two gaps above rather than failures.

Sandboxed verification would settle the one claim CI cannot: @qwen-code /verify — an A/B of cli collection time with and without this diff on the same host. You have write access, so this is a normal run rather than a sponsored one. It is worth naming because your own measurement says the answer is "under 2%, inside the spread", and an independent A/B is the only thing that either confirms that or contradicts it; the perf claim currently rests entirely on a number you published in a comment, which is better evidence than most PRs offer but is still the author's measurement rather than a gate's. /tmux is not the right lane here — nothing user-visible moves, and the serve/sandbox.ts touch is import-only.

中文说明

代码审查

没有 Critical 问题。我专门去找这类改动的两种典型崩坏方式——解析器顺序排错、以及导入指向一个并未声明该符号的模块——两种都不存在。下面是我核实过的内容,然后是我真正会改的两点。

已核实为正确(不是想当然):

  • **alias 顺序。**具名子路径排在通配之前,包根用 RegExp 而非字符串匹配。最后这一点很微妙,而代码是对的:@rollup/plugin-alias 对字符串 find 的匹配规则是「相等,或以其为前缀且后接 /」,所以写成字符串 '@qwen-code/qwen-code-core' 会吞掉所有子路径并把它们改写到 index.ts 下面。你的注释准确说明了这一点。通配与精确根之间也不会冲突,因为通配要求 core 后面有 /
  • **resolveExportTarget 与 Node 的真实算法一致。**字面前缀最长者优先,平手时完整键更长者优先,跳过含零个或两个 * 的键,捕获段替换进目标。我特别核对了平手判定,因为这是那种看起来对、实际差一个比较的地方:best.pattern.length >= pattern.length 在平手时保留先出现的键,与 Node 行为相同。没有它,catch-all 一落地 lint 规则就会失明,所以它是承重的,不是装饰。
  • **./* 没有遮蔽任何条目。**精确键永远优先于模式键,且 ./dist/* / ./src/* 的前缀都比 ./* 长,所以三个既有行为都保留。「表面未扩大」也成立——./dist/* 本来就能触达 ./* 现在触达的每一个文件,因此可达集合未变,只是多了新写法。
  • 新增的 index.js barrel 条目是必要的。./*@qwen-code/qwen-code-core/index.js 映射到 dist/src/index.js,而 packages/core/src/index.ts 正是一个 barrel——所以如果不把这个 specifier 加进 CORE_BARREL_SPECIFIERS./* 就等于给这条规则本要封锁的东西开了第二扇门。这个点抓得好,而且很容易漏。
  • **全部 109 处迁移的符号都正确,包括纯类型导入。**这是我最预期能找到、却没能找到的问题。Test job 里的 npm ci 会跑 preparenpm run build → 各包的 tsc --build,而 ci.yml 从未设置 QWEN_SKIP_PREPARE,所以尽管工作流里根本没有 npm run typecheck,CI 实际上是做了类型检查的。Test (ubuntu-latest, Node 22.x) 为绿,意味着 SandboxConfig 确实在 config/config.jsresolveBundleDir 确实在 utils/bundlePaths.js,并且你在合并冲突中遇到的「AgentStatus 来自错误模块」那一类错误,在整个迁移范围内都被排除了。单靠 vitest 抓不到纯类型的那些——esbuild 会把它们擦掉。

建议——新检查比你的描述所说的更承重。Risk & Scope 建立在「shipped CLI 是单一 bundle,运行时从不解析这些 specifier」之上,而这只对 esbuild 的独立产物成立。bin.qwen 指向 packages/cli/dist/index.js,由 packages/cli/index.ts 编译而来,它 import ./src/cli.js——所以已发布到 npm 的入口跑的是 tsc 产物,原样保留 @qwen-code/qwen-code-core/config/storage.js,并在启动时交给 Node 按 core 的 exports 映射解析。@qwen-code/qwen-code-core 不在 esbuild 的 external 列表里,所以 bundle 确实内联了 core;而 npm 包确实没有。建议修正正文,因为目前这段描述恰好反对./* 条目与 CI 步骤最有力的理由:没有它们,安装后的 CLI 会在第一个子路径导入处以 ERR_MODULE_NOT_FOUND 死掉,而仓库内所有套件全绿——这正是该检查自己的 fileoverview 所写的内容。

**建议——门禁不对称。**你建了三道门禁,却留下两张映射表没有门禁,而且在代码里自己说明了:

手工同步的映射 门禁
integration-tests/tsconfig.json 具名 paths integration-tsconfig-core-paths-sync.test.js
skill-review-harness loader 的 named 映射 text-capture-core-loader-sync.test.js
cli/acp-bridge/sdk 源码中每个 specifier 的发布态解析 check:core-subpath-exports + CI 步骤 ✓
packages/cli/vitest.config.ts 具名 alias 无 —— "kept in sync by hand"
packages/cli/tsconfig.json paths ↔ vitest alias 数组 无 —— "kept in sync by hand"

没有门禁的那一张,是仓库里被使用最频繁的映射:每一次 cli 测试运行都要经过它。关于失败模式,公平地说它是响的而不是静默的——一个新的具名键若其目标 stem 与键名不同,会穿过通配落到 packages/core/src/<key>,该文件不存在,Vite 解析失败,Test (ubuntu) 变红。所以这不是阻塞项,我也不认为它是正确性漏洞。但两个已有门禁的同级实现里,恰好就包含了所需的推导逻辑(读 core 的 exports 映射、跳过 stem 与键名一致的键、其余要求显式条目),而 harness 那一张之所以有门禁,是为了一个你自己注释里写明没有任何 CI 测试会执行的 loader。每天跑上千次的那张表反而没有门禁。这种不一致是后来的贡献者会照抄的。

注——三处 mock 搬迁,不是两处。StandaloneSessionPicker.test.tsxutils/gitUtils.jsuseGitBranchName.test.tsutils/gitDirect.jsuseMemoryMonitor.test.tsutils/debugLogger.js。第一处新增的 mock wiring 测试形态是对的:它断言 vi.isMockFunction(getGitBranch) 且 fixture 确实生效,所以把 mock 移回包根会让它失败。那正是第一次运行时弄坏 16 个文件的失败类型,现在在它发生过的地方被钉住了。它的作用域声明也是诚实的——覆盖的是套件自身的 import,而不是 StandaloneSessionPicker.tsx,并且明确说了,没有暗示更多。

测试

这是一次无人值守的 CI 运行(workflow_dispatch),因此按照 skill 的规则,我没有构建或执行本 PR 的任何东西——没有 npm、没有 vitest、没有 checkout。以下全部是本 PR 自己的 CI,通过 API 针对 footer 中那个确切的 commit 读取。我携带的证据:真实的检查名、真实的结论,以及此处最关键的两个 job 的 step 级结论。

信号在 Linux 上很强,在其他平台上很薄。Test (ubuntu-latest, Node 22.x) 覆盖的范围比看上去大:它的 Install dependencies 步骤就是 npm ci,会跑 preparenpm run buildtsc --build,所以这个 job 为绿同时意味着全部 109 处迁移的类型检查为绿。它的 Run tests and generate reports 步骤是 test:ci:workspaces 后接 test:scripts,所以三个新增 sync 测试在那里跑过并通过了。在 Lint & Static (ubuntu-latest, Node 22.x) 上,我核对了 step 列表而不是只看 job 结论——Check core subpath exports resolve = success,说明新门禁在这个 head 上确实执行了,没有被它的 ci_profile == 'full' 条件跳过。

上方表格为本次审查 commit 的 CI 结论;三个缺口如下:

  • 未验证:本 head 上的 macOS 与 Windows。Test (macos-latest, Node 22.x)Test (windows-latest, Node 22.x) 都是 skipped。正文的 Tested on 表格声称 macOS ✅ 与 Windows ✅「Via CI」;在 cdabd7dad 上两者都没跑。这些说法对 release lane 上更早的 head 可能确实成立,但不能作为本 commit 的证据。
  • 未验证:CLI 集成套件。Integration Tests (CLI, No Sandbox)skipped。这一点比平常更要紧,因为 PR 描述说,正是集成门禁最先暴露出缺失的通配与 exports 缺口。Integration Tests (no-AK, No Sandbox) 确实跑了并且为绿,而 npm run typecheck:integration 就在那个 job 里,所以 tsconfig.json 的通配是有覆盖的——但当初抓到这个 bug 的那个套件,不是这次跑的那个。
  • **未验证:任何性能提升。**本 head 上没有任何检查测量收集时间,而全绿的套件无法证明加速。这是本 PR 的核心主张,CI 完全没有触及。

没有红色需要归因,所以没有日志摘录可引——本 commit 上唯一的非 success 结论就是 skipped,也就是上面那两个缺口,而不是失败。

沙箱验证可以判定 CI 判不了的那个主张:@qwen-code /verify —— 在同一台主机上、带与不带本 diff 的 cli 收集时间 A/B。你有写权限,所以这是普通运行而非 sponsored 运行。之所以值得点名,是因为你自己的测量给出的答案是「不到 2%,落在波动范围内」,而独立的 A/B 是唯一能证实或推翻它的东西;目前这个性能主张完全依赖你在评论里公布的那个数字——这比多数 PR 提供的证据更好,但仍然是作者的测量,而不是门禁的测量。/tmux 在这里不是合适的通道——没有用户可见的变化,而 serve/sandbox.ts 的改动是纯导入。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the engineering is clean and I could not break it, but a perf PR whose own author measured the win at under 2% and then concluded the batching unit was wrong is a maintainer's call, not a gate's.

What I would have proposed. Before reading the diff, from the title and the "Why it's needed" section alone: confirm with one measurement that the barrel evaluation is actually the collection cost, then land only the mechanism — the resolver mapping, the exports entry, a check that pins it, the lint rule taught the new shape — with zero module migrations, and migrate afterwards by whole test graphs, one graph per PR, measuring each. Same mechanism, none of the 669-line sweep, and no revert cycle.

Your history is the evidence for that ordering, which is why I keep coming back to it. The sweep is what broke 16 files and 127 tests. The mechanism is not implicated in a single one of those failures — every one was a stub that stopped intercepting, i.e. a consequence of which modules moved, never of how they resolve. You then wrote that "which modules are safe to move" is the wrong unit and "which test graphs can be made entirely clean" is the right one. This PR is a batch selected by the unit you have since rejected, and it is the batch that produced the breakage. Your own analysis is a better argument for splitting than anything I could construct.

What I would thank you for. The gates are the genuinely valuable part and they are well built. check-core-subpath-exports closes a hole I confirmed is real and worse than the description claims: bin.qwen runs tsc output, not the esbuild bundle, so the npm-installed CLI resolves these specifiers through core's exports at startup and would die with ERR_MODULE_NOT_FOUND while every in-repo suite stayed green. That is a nasty failure shape — silent in CI, fatal in production — and it is now caught. The two sync tests derive their expectations from core's exports map instead of hardcoding a list, so they cannot drift into asserting nothing. The mock wiring test pins the exact failure class that cost you the first attempt. And the review surface was honest throughout: you published a measurement that argues against your own PR, cut 146 modules back rather than defend them, and named the two ungated maps in a code comment before I could.

Reservations, in the order they'd stop me.

  1. The benefit is unobservable and the cost is permanent. ~1184 lines of machinery and tests carrying a 669-line sweep, for 24 of 1003 test files getting cheaper — your number, which I have no basis to dispute. Nothing in CI on this head measures collection time, so the PR's titular claim rests entirely on that comment.
  2. The published exports catch-all is a design commitment, not a one-liner. I verified it widens no reachable file — ./dist/* already got there — and shadows nothing. What I cannot verify is whether the project wants every internal core module to spell like a supported subpath from now on, which makes a future packages/core/src/ reorganisation a breaking change for whoever found the clean spelling. That is a contract decision and it is being made in a perf PR's diff, one line, without being the PR's subject.
  3. Gating asymmetry. Three maps got gates; the two that did not are the ones every cli test run touches. Failure is loud rather than silent, so this is not a blocker — but the harness loader that did get a 92-line gate is, in your own comment, one that no CI test executes.
  4. This PR is past the point where widening it costs more than it returns. Six-plus bot review rounds across 6626ccb5, c41c2d43, b3d54437, ccbc6ac6, 18aa484b, 47128e70. AGENTS.md says that after roughly five, land Critical fixes and defer the rest. Both of my Suggestions are deferrable.
  5. The evidence claims outrun this head. macOS ✅ / Windows ✅ "Via CI" while both Test jobs are skipped on cdabd7dad, and the CLI integration suite — the one that originally caught the missing wildcard — is skipped too.

Why I am not requesting changes. I found no correctness defect, no security issue, and no regression. Every load-bearing claim in the code that I could check independently checked out, including the two I most expected to break: alias precedence under @rollup/plugin-alias's string-matching rule, and the tie-break in the Node exports-pattern reimplementation. Requesting changes would be manufacturing a blocker out of a cost-benefit disagreement, which is not what that verdict is for.

Why I am not approving either. The question this PR leaves open is not "is it correct" — it is. It is "should a sub-2% perf slice, carrying a widening of the published core exports map and roughly 1200 lines of permanent machinery, land now, when its author's own measurement concludes the remaining work should be re-sequenced by test graph rather than by module?" I cannot answer that from the diff, the tests, or the description, and the description is the thing arguing against it. Approving would mean I ran out of reasons to say no.

⏸️ Deferring to @chiga0 — needs a human call on this one. Specifically, three things I could not resolve:

  • whether the ./* catch-all on packages/core/package.json is an acceptable permanent widening of the published contract, given it exposes no new file but does make every internal core module path a supported-looking subpath;
  • whether the 109-module sweep should land in this PR or be re-cut into graph-complete batches per the re-sequencing you described, keeping the mechanism here;
  • whether the ungated packages/cli/vitest.config.ts alias map needs a gate before merge or can be a follow-up.

You reviewed this at ccbc6ac6 and found no blocking issues, so you have the context I lack — the head is now cdabd7dad, and CI on it is green on Linux with macOS, Windows and the CLI integration suite skipped. I have assigned the PR to you so it lands in your filter rather than only in a mention.

中文说明

Confidence: 3/5 —— 工程实现是干净的,我没能把它弄坏;但一个 perf PR,其作者自己测出的收益不到 2%,随后又判定分批单位选错了,这应该由 maintainer 决定,而不是由 gate 决定。

**我原本会怎么做。**在读 diff 之前,仅凭标题和「Why it's needed」:先用一次测量确认 barrel 求值确实是收集成本的来源,然后落地机制——解析映射、exports 条目、一个把它钉住的检查、学会新形态的 lint 规则——一个模块都不迁移;之后再按「整张测试图」为单位迁移,一个 PR 一张图,每批都测量。机制相同,没有那 669 行迁移,也没有还原循环。

你的历史正是这个顺序的证据,这也是我反复回到这一点的原因。弄坏 16 个文件、127 个测试的是迁移,不是机制——机制没有牵涉其中任何一次失败,每一次都是 stub 停止拦截,也就是「迁移了哪些模块」的后果,从来不是「它们如何解析」的后果。你随后写道「哪些模块可以安全迁移」是错误的单位,「哪些测试图能被彻底清干净」才是正确的单位。本 PR 就是一个用你此后已否定的单位选出的批次,而且正是产生了那次破坏的批次。你自己的分析,比我能构造出的任何论证都更有力地支持拆分。

**我要感谢的部分。**门禁是真正有价值的部分,而且建得很好。check-core-subpath-exports 堵上了一个我确认为真实、且比描述所说更严重的洞:bin.qwen 跑的是 tsc 产物而非 esbuild bundle,所以 npm 安装后的 CLI 在启动时就会通过 core 的 exports 解析这些 specifier,会以 ERR_MODULE_NOT_FOUND 死掉,而仓库内所有套件全绿。这是一种很难缠的失败形态——在 CI 里静默、在生产里致命——现在它被抓住了。两个 sync 测试从 core 的 exports 映射推导预期,而不是硬编码列表,所以不会退化成断言空物。mock wiring 测试钉住的正是让你第一次尝试付出代价的那个失败类型。而且整个审查过程是诚实的:你公布了一个反对自己 PR 的测量数据,把 146 个模块砍回来而不是为它们辩护,并在我之前就在代码注释里点名了那两张没有门禁的映射表。

保留意见,按会让我停下来的顺序。

  1. **收益不可观测,成本是永久的。**约 1184 行机制与测试,承载 669 行迁移,换来 1003 个测试文件中 24 个变便宜——这是你的数字,我没有依据反驳。本 head 上没有任何 CI 检查测量收集时间,所以这个 PR 的标题主张完全依赖那条评论。
  2. **发布态 exports 的 catch-all 是设计承诺,不是一行代码。**我核实了它没有扩大任何可达文件——./dist/* 本来就能到达——也没有遮蔽任何条目。我无法核实的是:项目是否愿意从此让 core 的每个内部模块都长得像受支持的子路径,这会让将来对 packages/core/src/ 的重新组织,对发现这个干净写法的人来说变成破坏性变更。这是一个契约决定,而它正在一个 perf PR 的 diff 里、以一行代码的形式做出,并且不是这个 PR 的主题。
  3. **门禁不对称。**三张映射拿到了门禁;没拿到的两张,恰恰是每次 cli 测试运行都要经过的。失败是响的而不是静默的,所以这不是阻塞项——但那个确实拿到 92 行门禁的 harness loader,按你自己的注释说,没有任何 CI 测试会执行它。
  4. **这个 PR 已经越过了「继续扩大它的代价大于回报」的临界点。**跨 6626ccb5c41c2d43b3d54437ccbc6ac618aa484b47128e70 共六轮以上机器人审查。AGENTS.md 说,大约五轮之后就只落地 Critical 修复、其余推迟。我这两条建议都是可以推迟的。
  5. **证据声称超过了本 head 的实际覆盖。**macOS ✅ / Windows ✅「Via CI」,而两个 Test job 在 cdabd7dad 上都是 skipped;最初抓到缺失通配的那个 CLI 集成套件,同样是 skipped。

**为什么我不 request changes。**我没有发现正确性缺陷、安全问题或回归。代码里每一个我能独立核实的承重主张都核实通过了,包括我最预期会坏的两个:@rollup/plugin-alias 字符串匹配规则下的 alias 优先级,以及 Node exports 模式重实现里的平手判定。request changes 意味着我要把一个成本收益分歧包装成阻塞项,那不是这个裁决的用途。

**为什么我也不 approve。**这个 PR 留下的问题不是「它正确吗」——它正确。而是「一个不到 2% 的性能切片,携带对已发布 core exports 映射的扩大和约 1200 行永久机制,是否应该现在落地——而它作者自己的测量已经得出结论:剩余工作应当按测试图重新编排,而不是按模块」。我无法从 diff、测试或描述中回答这个问题,而描述本身正在反对它。approve 就意味着我只是找不出说不的理由了。

⏸️ 上交 @chiga0 —— 这件事需要人来定。具体是我无法解决的三点:

  • packages/core/package.json 上的 ./* catch-all,作为对已发布契约的永久扩大是否可接受——它没有暴露新文件,但确实让 core 每个内部模块路径都成了看起来受支持的子路径;
  • 那 109 个模块的迁移应该留在本 PR,还是按你描述的重新编排切成「整张图干净」的批次、机制部分留在这里;
  • 没有门禁的 packages/cli/vitest.config.ts alias 映射,是合并前必须补门禁,还是可以作为后续项。

你在 ccbc6ac6 上审查过这个 PR 并且没有发现阻塞问题,所以你掌握我缺少的上下文——当前 head 是 cdabd7dad,其 CI 在 Linux 上为绿,macOS、Windows 与 CLI 集成套件为 skipped。我已把这个 PR 指派给你,让它进入你的 Assigned 过滤器,而不只是停留在一个 mention 里。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 · re-run with @qwen-code /triage

Retargeting this PR at main got the unit suite to run for the first time, and
it failed: 16 files, 127 tests, none of them failing on the jsdom PR that
shares the same base. So they are this stack's doing.

Every one is the same shape — a stub the suite installs no longer intercepts
once the code under test names a module instead of the package root. The
static analysis that picked these files models three ways of installing such a
stub and misses at least two more: a spy planted on a namespace import of the
package, and whatever six of the sixteen suites do, which it cannot parse at
all. Sharpening the heuristic further is not the answer; it was already wrong
in a way no amount of local reading would have caught.

So this restores every migrated module that any failing suite reaches, 146 of
them, and keeps the 110 that nothing failing depends on. That is blunt — some
of the 146 are certainly fine — but it is the version that can be shown to
pass, and picking the survivors apart is work for a run that is green to begin
with.
@yiliang114

Copy link
Copy Markdown
Collaborator Author

The full run found real breakage, and I have cut the change back to what passes.

What the run said. 16 files, 127 tests. The jsdom PR (#10958), which branches from the same base and shares none of these commits, failed 1 file and 2 tests — a .tsx neither PR touches. So the 16 belong to this stack.

Why. Every failure is the same shape: a stub the suite installs stops intercepting once the code under test names a module instead of the package root. Three examples, three different mechanisms:

  • McpPromptLoader.test.ts plants a spy on a namespace import — vi.spyOn(cliCore, 'getMCPServerPrompts'). Patching the package's namespace object does nothing for a module that imported the function directly.
  • startup-prefetch.test.ts asserts its debug logger was called; the code now takes the real one.
  • workspace-git-diff.test.ts gets real git output where it expected stubbed rows.

Why the analysis missed them. It models vi.mock(<package>, factory) and distinguishes a factory that replaces the package from one that spreads it. It does not model a spy on a namespace import, and for six of the sixteen suites it cannot identify how the stub is installed at all. That is a modelling gap, not a tuning problem, and no amount of reading the code locally would have closed it — only the suite would, which is exactly what was not running.

What I did. Restored every migrated module that any failing suite reaches — 146 files — and kept the 110 that nothing failing depends on. Deliberately blunt: some of the 146 are certainly fine, but separating them belongs on top of a green run rather than in place of one.

The two batches below still read as originally written. Once this is green, they should be squashed to match what actually lands, or this can carry the whole change and they can close.

中文说明

完整运行抓到了真实的破坏,我已把改动收缩到能通过的范围。

运行结果:16 个文件、127 个测试失败。而同样基于该基线、与本栈无任何共同提交的 jsdom PR(#10958)只挂了 1 个文件 2 个测试,且是两个 PR 都没碰过的 .tsx。所以这 16 个是本栈造成的。

原因:所有失败都是同一形态——被测代码从「包根」改为「具名模块」后,用例安装的 stub 不再拦截。三个例子对应三种不同机制:McpPromptLoader.test.tsvi.spyOn(cliCore, 'getMCPServerPrompts') 打在命名空间导入上,而直接导入该函数的模块不受影响;startup-prefetch.test.ts 断言其 debug logger 被调用,而代码现在拿到的是真实实现;workspace-git-diff.test.ts 期望 stub 数据,却拿到真实 git 输出。

分析为何漏掉:它建模了 vi.mock(<包>, 工厂) 并区分「整体替换」与「展开真实包」两种工厂,但没有建模「打在命名空间导入上的 spy」,而且十六个用例里有六个它完全识别不出 stub 是怎么装上去的。这是建模缺口而非参数问题,靠本地读代码补不上——只有测试套件能发现,而它恰恰没在跑。

处理:回退所有被失败用例触达的已迁移模块(146 个),保留没有任何失败用例依赖的 110 个。这刀切得钝——其中必然有一些本来没问题——但把它们摘出来应该建立在一次绿色运行之上,而不是取而代之。

下面两个 PR 的描述仍是最初写法。本 PR 转绿后,它们应压缩成实际落地的内容,或者由本 PR 承载全部改动、它们关闭。

…e by path

The integration gate failed to compile against the migrated files:

  error TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js'
  error TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/editor.js'

Only packages/cli maps these specifiers, through a wildcard in its own
tsconfig. The integration suite lists the eight named subpaths and no
wildcard, and the package's exports map has entries for those same eight plus
the dist and src trees — so anything resolving the normal way, this suite and
any consumer of the published package alike, cannot name a core module.

Both gaps close here: the wildcard is added to the integration suite's path
mappings, and a catch-all maps a bare module path onto the build output. The
catch-all exposes nothing new; `./dist/*` already reaches the same files.

This is the part of the change with consequences beyond the test run. The
shipped CLI is a single bundle and never resolves these specifiers at runtime,
but the package is published, and until now a migrated import was only
resolvable from inside the one workspace that happens to map it.
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head e82b671, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 12 scenario(s).

Qwen Code · serve A/B

@yiliang114 yiliang114 changed the title test(cli): move three barrel mocks onto the modules they actually stub perf(cli): import core modules directly instead of the package root Sep 3, 2026
yiliang114 and others added 2 commits September 4, 2026 22:49
The exports check validated resolved targets against the repo tree, but
core's exports map also carries "./src/*": "./src/*" while package.json
publishes only dist, vendor and scripts/postinstall.js. A specifier
routed through that entry resolved to a real in-repo packages/core/src
file, passed the bare existsSync and exited 0 even though the published
artifact ships nothing — the installed CLI would die at startup with
ERR_MODULE_NOT_FOUND while the gate stayed green. Require the resolved
target to lie inside packages/core/dist/, where every legitimate
runtime specifier lands.

Add scripts/tests/check-core-subpath-exports.test.js: a fixture-tree
suite (per the scripts/tests convention) that runs a copy of the real
script in a temp workspace. It pins the dep-only scan from a18ffeb
(goalWire named by no cli source resolves; removing its exports entry
exits 1 — the mutation witness requested in review, made hermetic) and
pins this guard (a ./src/*-routed specifier is reported as not
published; removing the guard turns that test red).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtn1pqbuir
Keep the subpath import style while absorbing main's output-style
dialog changes: route OutputStyleDefinition/BUILT_IN_OUTPUT_STYLES
through core/output-styles.js, drop the now-unused
BUILT_IN_OUTPUT_STYLES import from dialogs-modes, and register the
new conversationsRuntimeMarker alias ahead of the wildcard.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtn4km2niw
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head cb9ee87. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

terminal-turn-error-copy-narrow-dark before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability (junction symlinks, realpath spellings, exec-bit skipIf) verified by reading only.

Not reviewed: build-and-test — packages/cli and packages/core unit suites timed out under runner load (infrastructure); packages/web-shell failed only in files this PR does not touch (base A/B unavailable — path-rule attribution); packages/vscode-ide-companion and packages/webui suites never ran (continuation cap).

Not reviewed: reverse audit — stopped before round 5 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:

  • scripts/tests/no-core-utils-upward-import.test.js:251 — [review] D5-1 'keeps exact export keys ahead of the wildcard' cannot discriminate its mutation (goalWire reports 1 under either ordering); re-specify it with a key whose orderings dive…
  • scripts/tests/dev.test.js:164 — [review] D5-2 dev-loader fall-through stub is argument-blind — a nextResolve(sub, context) mutation ships green; echo the specifier in the stub and assert it
  • eslint-rules/no-core-root-barrel-import.js:35 — [review] D5-3 CORE_BARREL_SPECIFIERS misses the /dist/src/index.js barrel spelling reachable via the ./dist/* exports entry — add it plus a rejects-table case
  • scripts/check-core-subpath-exports.mjs:33 — [review] D5-4 8 of the 9 named exports entries are never probe-imported; a repoint to another existing dist file passes the gate — extend EXPORT_PROBES

Convergence: round 5 posted 5 inline comment(s), 5 of them reported for the first time; the previous round posted 2 (2 new). Findings keep coming back to the same files: scripts/check-core-subpath-exports.mjs (findings in round 4; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability (junction symlinks, realpath spellings, exec-bit skipIf) verified by reading only.

未审查(原文为英文):build-and-test — packages/cli and packages/core unit suites timed out under runner load (infrastructure); packages/web-shell failed only in files this PR does not touch (base A/B unavailable — path-rule attribution); packages/vscode-ide-companion and packages/webui suites never ran (continuation cap).

未审查:反向审计——评审时间预算不足,未能开始第 5 轮。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 5 轮发布了 5 条行内评论,其中 5 条是首次提出;上一轮发布了 2 条(其中 2 条首次提出)。发现反复回到同一批文件:scripts/check-core-subpath-exports.mjs(第 4 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread scripts/check-core-subpath-exports.mjs
Comment thread scripts/tests/check-core-subpath-exports.test.js
Comment thread scripts/tests/check-core-subpath-exports.test.js
Comment thread scripts/check-core-subpath-exports.mjs Outdated
yiliang114 and others added 2 commits September 5, 2026 06:27
…r probe stages

The round-4 dist-containment guard rejected
@qwen-code/qwen-code-core/package.json, which core's exports map
deliberately publishes ("./package.json": "./package.json") and npm ships
regardless of "files". Allow that one target; ./src/* targets stay
rejected (existing test remains red without the guard).

Also extend the fixture suite to the two failure stages it did not
witness: the named-export probe stage (target exists but lacks the probed
export) and the resolve-failure branch (exports map without the ./*
catch-all). Mutant-verified: removing the probe import/export-name check
or the resolve-catch failed++ turns the matching new test red and nothing
else.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl
…er map

The loader's named map listed 8 of the 9 named entries in core's exports
map: conversationsRuntimeMarker (source at utils/conversations-runtime-marker.ts,
a path the specifier does not mirror) was missing. The specifier is
reachable in the harness graph today via shared-env-keys.ts, so resolution
fell through to the exports map — ERR_MODULE_NOT_FOUND mid-capture on a
fresh clone, or a stale dist silently mixed into a source-backed capture
otherwise. Add the entry, matching packages/core/package.json and the cli
vitest alias.

Also add a sync guard (scripts/tests) asserting every named key of core's
exports map has a loader entry pointing at an existing core source file,
so the next omitted entry goes red in CI.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtngpuscjl
@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 48 passed · 2 failed · 50 total

Flakiness gate: ⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:48 通过 · 2 失败 · 50 总计

抖动门:⚠️ timeout — only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

PR 10957 — deep verification (follow-up round)

Verdict: findings (non-blocking) — 50 scripted assertions, 48 pass / 2 fail.
Verified head 47128e7007c6250d3963a90359b054870604a8f9 (git rev-parse HEAD^2), base 74fe3a659dde2859f152d6c860e04cfddca86d05 (HEAD^1).

Nothing regressed. The two failed assertions are my census model's calibration limits, not defects in the PR (named in Findings §5 and in Not covered); the central claim was re-measured with 32 real vitest runs whose oracle is noise-free. Since the last round the PR grew six commits, all of them in the exports gate, its new test file, and the skill-review harness loader — and that delta is where this round's evidence is: the loader change is load-bearing (base 3/4 harness targets load and SkillReviewDialog dies with ERR_MODULE_NOT_FOUND; head 4/4 with zero fall-through), previous finding 2 is fixed, and one new survivor (M13) is a real coverage gap with a demonstrated wrong-module load.

中文摘要

结论:findings(非阻塞),50 条脚本断言 48 通过 / 2 失败。验证 head 47128e7007c6,base 74fe3a659d

A/B 结论:上一轮的 16 个单元在新 head/base 上全部重跑(32 次 vitest 真实运行)。机制形态与上轮一致——4/16 个单元从「加载 barrel」翻转为「不加载」(585→16、585→15、585→16、587→18 个模块),其余 12 个单元模块数逐字节相同(662=662、718=718、641=641…);无任一单元在 head 加载更多模块;32 次运行中跨树污染为 0。唯一变红的单元(SkillReviewDialog)在两臂完全相同(641/641 模块、1 failed/32 passed),空载重跑仍红,属容器内既有失败,非本 PR 引入。详见 Central claim + A/B 表与 01-ab-cells-base-vs-head.png

上一轮发现的处置:F2(门禁普查止于 packages/cli/src已修复——门禁现同时扫描 acp-bridge 与 sdk-typescript,说明符 92 → 96,三个「仅依赖包命名」的条目(goalWire/subSessionConstants/transcriptRecords)已纳入,M7 变异被新测试捕获。F1(实际收益约 22 个测试文件)依然成立——本轮普查(部分校准 30/32)得 601 → 580,即 1011 个中减少 21 个(2.1%),与上轮已校准的 -22/1007(2.2%)一致。F3(./* 把未知子路径从解析期错误变成文件缺失)依然成立。更正 C1(CI 注释与脚本文件头声称「qwen 启动即挂」)未被采纳,仍然成立:重测发布 bundle dist/cli.js+dist/chunks/ 中该说明符出现 0 次,未打包 tsc 产物 packages/cli/dist/src/ 中出现 268 次

本轮新发现

  1. 新增的 text-capture-core-loader-sync.test.js 只校验 loader named 映射的键覆盖目标文件存在,不校验映射是否正确。变异 M13 把 goalWire 指向 utils/transcript-records.ts(文件存在、模块错误),该测试仍 2/2 全绿,而 loader 实际把 goalWire 解析到了错误模块。同文件正向对照 M14(指向不存在的文件)干净变红,证明这是真实覆盖缺口而非 harness 失效。
  2. loader 改动本身是 load-bearing 的(这是上一轮无法触达的分支):14 个说明符的 resolve 扫描中 base 仅 1 个被重定向、13 个穿透;head 13 个全部重定向到 TypeScript 源码、伪造说明符正确穿透。并复现了注释所述的第二种失效:当 dist 存在时,base 把 envVarResolver 路由进编译产物 dist/src/utils/envVarResolver.js
  3. 3 个与本 PR 目的无关的纯格式化改动(web-shell ToolGroup.tsx/ToolGroup.test.tsxexport-html-from-chatrecord-jsonl.js)。实测 base 版本不满足仓库锁定的 prettier 3.6.1、head 版本满足,故属规范化而非破坏,但会让 perf(cli) 的评审面对意外的 web-shell diff。
  4. dist 容纳守卫的判据(「必须在 dist/ 下」)窄于它所引用的 manifest("files": ["dist","vendor","scripts/postinstall.js"]):将来任何 ./vendor/* 导出会被 npm 正常发布,却被该门禁拒绝。今日 96 个真实说明符全部通过(0 误报)。

未覆盖:cli 全量单元套件(84 分钟 × 2 臂)、仓库级 lint/typecheck、integration-tests/tsconfig.json 通配符(仅静态审阅,未编译)、按 commit 归因(浅克隆:本地 rev-list HEAD^1..HEAD^2 只返回 1,快照列 21 个)、上轮 M3–M6 未重跑、普查模型仅部分校准(30/32)故其套件级数字只能作为估计。

Previous-round findings — status at the new head

Last round verified head ccbc6ac6d78a, base 9c320cb0cc32. Neither commit is reachable in this depth-2 checkout (git cat-file -t ccbc6ac6d78acould not get object info), so nothing was diffed against the old report: every measurement below was re-run at the new head, including the 16 A/B cells, the base-arm setup, the exports probe and the mutation matrix. The delta is six commits (a18ffebb88e6, 03886a48062a, 647e97f6e54b, 18aa484b4da2, c10be7f20426, 47128e7007c6) plus a base that advanced from 9c320cb0cc to 74fe3a659d.

# previous finding severity status at 47128e7007c6
C1 Correction: ci.yml comment and the gate's file header claim a broken exports map "breaks qwen at startup", but the shipped CLI is an esbuild bundle that inlines core correction stands, text unchanged. Re-measured: grep -roh "@qwen-code/qwen-code-core/" dist/cli.js dist/chunks/0; packages/cli/dist/src/268 (was 264). Real consumers of the unbundled tree are still serve-ab.yml:294,307 and scripts/tui-parity/fixtures/scenarios/*.json
F1 Delivered saving is ~22 test files (≈4–5% of the lanes the description quotes), not the suite-wide collection cost Suggestion stands. 4/16 cells flip, exactly as last round. Census (partially calibrated, see §5) gives 601 → 580 reaching the barrel of 1011 cli test files = −21 (2.1%), against last round's calibrated −22/1007 (2.2%)
F2 The exports gate's census stops at packages/cli/src; 4 specifiers named only by acp-bridge/sdk-typescript were ungated Nice to have fixed. scannedSources now lists all three dirs; the gate resolves 96 specifiers (was 92) and exits 0. The three dep-only ones — goalWire, subSessionConstants, transcriptRecords — are collected. Mutation M7 (drop both dep dirs) is killed by the new test
F3 ./* turns an unknown subpath from a resolve-time error into a URL for a missing file, making the gate's existsSync line load-bearing but unpinned Nice to have stands. import.meta.resolve('…/does-not-exist.js') still returns …/node_modules/@qwen-code/qwen-code-core/dist/src/does-not-exist.js with exists=false. The new 245-line test file has no case for it (asserted: it contains neither does-not-exist nor resolved target does not exist)
M1–M6 Mutation matrix, 6/6 killed M1 re-measured here as M11 and killed (deleting ./* fails 88 of 96 specifiers). M3–M6 were not re-run — see Not covered

Scope

Central claim (perf): importing individual core modules instead of the package root removes core's ~578-module barrel from a cli test file's module graph.

Secondary claims: (a) the vitest alias ordering and core's ./* catch-all resolve every deep specifier the migrated sources name; (b) the guards added around all of this are load-bearing.

This round's emphasis is the delta: the exports gate's two new guards and their new fixture test, and the skill-review-harness loader's new subpath handling — the one branch the previous round listed as unreachable from CI.

Central claim + A/B

Control: HEAD^1 in a scratch worktree at tmp/base-tree. The PR leaves package-lock.json untouched (git diff --name-only HEAD^1..HEAD -- package-lock.json → empty), so third-party dependencies are identical across arms. Internal workspace links were re-pointed, not assumed: node_modules was mirrored entry-by-entry (1772 symlinks) with every @qwen-code/* link re-rooted at the base tree, and vite dep caches (.vite, .vite-temp) were deliberately not shared. 8/8 setup assertions passed, including realpath(base/node_modules/@qwen-code/qwen-code-core) == tmp/base-tree/packages/core and realpath(…/qwen-code) == tmp/base-tree/packages/cli. Only the twelve dist/ outputs the cli globalSetup demands — from packages this PR does not touch (git diff --name-only HEAD^1..HEAD over acp-bridge, web-templates, channels, sdk-typescript, audio-capture0 files) — were linked from head.

Oracle: a counting vite plugin wrapping each arm's own vitest.config.ts, recording every module id vite transforms; one fresh vitest process per cell. Module count carries the verdict because wall time on this shared runner does not (ExtensionsList 41.6s → 79.3s at 679/679 identical modules). Witness: 01-ab-cells-base-vs-head.png.

Cells are the same 16 the previous round measured (all still colocated with a module this PR migrated), so the two rounds are directly comparable.

cell base mods head mods core barrel base→head base wall head wall tests
agent-view/supervisor-store 585 16 loaded → absent 36.5s 18.9s 17 = 17
remoteInput/RemoteInputWatcher 585 15 loaded → absent 42.8s 21.2s 10 = 10
services/tips/tipHistory 585 16 loaded → absent 38.9s 21.8s 11 = 11
ui/utils/customBanner 587 18 loaded → absent 36.6s 20.4s 40 = 40
ui/components/EffortDialog 662 662 loaded → loaded 30.6s 38.4s 5 = 5
ui/components/InputPrompt 718 718 loaded → loaded 146.3s 135.7s 215 = 215
ui/components/SkillReviewDialog 641 641 loaded → loaded 49.6s 47.7s 1 failed / 32 passed on BOTH arms
ui/components/approvalModeVisuals 608 608 loaded → loaded 31.9s 30.0s 3 = 3
ui/components/hooks/HandlerListBody 587 587 loaded → loaded 36.1s 36.9s 11 = 11
ui/components/messages/AskUserQuestionDialog 660 660 loaded → loaded 37.8s 40.8s 34 = 34
ui/components/views/ExtensionsList 679 679 loaded → loaded 41.6s 79.3s 9 = 9
ui/hooks/useAtCompletion 641 641 loaded → loaded 45.9s 49.8s 36 = 36
ui/hooks/useMemoryMonitor 8 8 absent → absent 15.8s 20.0s 8 = 8
ui/opentui/dialogs-memory-status 589 589 loaded → loaded 29.4s 32.1s 5 = 5
ui/opentui/live-turn 588 588 loaded → loaded 32.3s 35.1s 12 = 12
ui/utils/tool-display-map 585 585 loaded → loaded 31.3s 28.1s 2 = 2
totals 9308 7031 4 of 16 flip 683.4s 656.2s 30/32 green

Both shapes from last round reproduce exactly:

  • Where it fires, it fires completely — 585 → 16 transformed modules (−97%), core modules 578 → 9, wall −17.6s.
  • Where it cannot fire, nothing changes at all — 12 of 16 cells are identical in module count. One surviving barrel importer anywhere in the closure pulls all 578 core modules back.
  • No cell loaded more modules at head; no cross-tree module id appeared in any of the 32 runs (contam=0 throughout).

The one red cell is an A/A control, not a regression. SkillReviewDialog.test.tsx > auto-refreshes the preview when the staged file changes on disk fails with expected '╭────…' to contain 'WATCHED_BODY_MARKER' after a 5.2s frame wait — identically on both arms (641/641 modules, 1 failed | 32 passed), and it fails again on an otherwise idle runner (logs/rerun-idle-head.log), so it is deterministic in this container rather than load-sensitive flake. The test file is not one this PR changes; the module beside it is. It is a pre-existing environmental failure and is counted as such, not as a PR defect.

Closure census — partially calibrated this round

The static walk (closure-census.mjs, alias rules parsed from each arm's own vitest.config.ts rather than transcribed) agrees with the measured barrel flag on 30 of 32 cell/arm pairs. One cell disagrees on both arms: SkillReviewDialog.test.tsx measures barrel loaded while the model predicts not reached, so the model under-predicts reach there. Two model bugs were found and fixed before this number was used at all — a dropped path.resolve replacement that silently reported zero barrel reach on the head arm alone, and per-file instead of graph-wide vi.mock suppression (fixing the latter is what moved 28/32 → 30/32 and reconciled useMemoryMonitor, whose base test mocks the core root with a bare factory).

base head Δ
cli test files reaching the core barrel 601 / 1011 580 / 1011 −21 (2.1%)
cli test files barrel-free 410 431 +21
cli production modules importing the barrel by value 370 289 −81

Labelled an estimate, not a measurement: with one cell disagreeing in the under-predicting direction the −21 could be a few files larger, and the −81 production cross-check does not close against the 110 migrated files (that assertion is one of the two fails in assertions.json). Its convergence with last round's independently calibrated −22/1007 is the reason to trust the order of magnitude; the 32 real vitest runs above are the reason the verdict does not rest on it.

Corrections

C1 still stands, verbatim, at the new head. .github/workflows/ci.yml (comment above Check core subpath exports resolve) says a change to the exports map or dist layout "leaves every suite green and breaks qwen at startup", and scripts/check-core-subpath-exports.mjs's file header repeats it ("every suite stays green while qwen dies on its first core subpath import"), as does its failure message. Measured against this round's freshly built tree:

  • grep -roh "@qwen-code/qwen-code-core/" dist/cli.js dist/chunks/0 occurrences; grep -rl over all of dist/ → no file at all. esbuild.config.js bundles with packages: 'bundle' and core is not in its external list, so the bundle inlines core.
  • grep -roh … packages/cli/dist/src/268 occurrences. The unbundled tsc tree keeps the specifiers verbatim and is executed by .github/workflows/serve-ab.yml:294,307 (node packages/cli/dist/index.js) and scripts/tui-parity/fixtures/scenarios/*.json.

The PR body's own Evidence section — "The shipped CLI is a single bundle and never resolves these specifiers at runtime" — remains the correct statement, and the CI comment still contradicts it. This is a correction to the description and the comment, not a request to change the code: the gate is genuinely load-bearing, but for CI lanes and non-bundled runs, and a maintainer reading "breaks qwen at startup" will misjudge a failure's blast radius.

Findings

1. The new loader-sync test pins the map's presence, not its correctness — Suggestion

scripts/tests/text-capture-core-loader-sync.test.js asserts (a) every named key of core's exports map appears in the harness loader's inline named map, and (b) every target file exists. It never asserts that a name points at the module the exports map names. So a wrong-but-existing mapping passes.

Measured (mutation M13, logs/mutations.out, witness 02-mutation-matrix-7-of-8-killed.png): repoint ['goalWire', 'goals/goal-wire.ts'] at ['goalWire', 'utils/transcript-records.ts'].

  • the sync test stays green, 2/2 passed;
  • the loader, extracted byte-verbatim and driven through a real resolve(), now returns packages/core/src/utils/transcript-records.ts for @qwen-code/qwen-code-core/goalWire — a different module, silently.

The survivor is real, not a dead harness: the positive control M14 in the same file (point goalWire at a file that does not exist) turns exactly one test red with AssertionError: loader entry goalWire -> packages/core/src/goals/does-not-exist.ts does not exist. M12 — deleting the conversationsRuntimeMarker row the head commit added — is also killed cleanly, so the guard the last commit shipped does work; it just cannot see a mis-mapping.

Blast radius is bounded and worth stating plainly: this loader is exercised only by npm run test:terminal-bench, never by CI, so a mis-mapping produces a wrong capture, not a wrong shipped artifact. Classification: coverage gap (the behaviour is right today; nothing asserts it stays right).

Suggested fix (not applied — measured only in a scratch copy)

Derive the expected target from the exports map instead of only checking existence — the map already carries it:

it('points every entry at the module the exports map names', () => {
  const exportsMap = JSON.parse(
    readFileSync(join(coreDir, 'package.json'), 'utf8'),
  ).exports;
  for (const [name, target] of named) {
    const importTarget = exportsMap[`./${name}`]?.import;
    expect(importTarget, `exports map has no import target for ./${name}`).toBeTruthy();
    // "./dist/src/goals/goal-wire.js" -> "goals/goal-wire.ts"
    expect(target).toBe(
      importTarget.replace(/^\.\//, '').replace(/^dist\/src\//, '').replace(/\.js$/, '.ts'),
    );
  }
});

Verified against the current map: all nine named entries satisfy it (goals/goal-wire.ts, utils/transcript-records.ts, …), so it is behaviour-preserving on head, and M13 goes red under it. The suite is green both with and without the existing two tests catching M13 — this is the unpinned axis, and the fixture above is the one that would go red.

Reproduce: node mutations.mjs (rows M12–M14).

2. The harness loader is now load-bearing, and this round proves it — pass, reported because last round could not reach it

The previous round listed this branch as untestable from CI. Constructing the configuration that reaches it was cheap, and the result is the cleanest A/B of the round (logs/loader-probe.out, logs/stale-dist-probe.out, witness 03-loader-ab-base-vs-head.png).

The loader body was extracted byte-verbatim from each arm's text-capture.tsx and evaluated as the template literal it is — an earlier version of my harness wrote the raw text out instead, which turns /\\.js$/ into a different regex and fabricated two fall-throughs that do not exist. A validity control now asserts the evaluated head loader carries the corrected /\.js$/, and it passes.

probe base arm head arm
resolve sweep, 14 specifiers (root, 9 named, 3 path-style, 1 bogus) 1 redirected, 13 fall through 13 redirected to packages/core/src/**.ts, bogus one correctly falls through
harness import graph, 4 targets (KeypressContext, ConfigContext, SettingsContext, SkillReviewDialog) 3/4SkillReviewDialog dies ERR_MODULE_NOT_FOUND: …/node_modules/@qwen-code/qwen-code-core/dist/src/utils/envVarResolver.js 4/4, zero core specifiers reaching nextResolve
with a dist present (both halves of the documented failure) envVarResolverCOMPILED dist/src/utils/envVarResolver.js; utils/debugLogger.js, config/storage.jsERR_PACKAGE_PATH_NOT_EXPORTED all three → TypeScript SOURCE

That third row reproduces both failure modes the new code comment claims, which is worth recording because the second one (silent load from compiled output) is invisible to any behavioural assertion. It also independently corroborates the PR body's report that the integration gate failed on Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js' — at base that specifier is not exported at all.

The real harness was also run end to end: tsx integration-tests/terminal-capture/skill-review-harness/text-capture.tsx after-preview exits 0 and renders the full dialog frame. Incidentally, after (without a suffix) matches neither mode === 'all' nor mode.startsWith('after-'), so it renders nothing and exits 0 — a pre-existing trap in the harness's mode naming, not something this PR introduced.

3. Three unrelated files carry pure formatting churn — Nice to have

packages/web-shell/client/components/messages/ToolGroup.tsx, its test, and integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js change only whitespace: union members onto leading-pipe lines, and && continuations re-indented. The PR body never mentions web-shell.

Measured, and it is benign rather than damaging: the base versions fail the repo's pinned formatter (prettier@3.6.1, resolved from ^3.5.3 with package-lock.json untouched, so both arms agree) and the head versions pass — prettier --check reports [warn] on all three base files and All matched files use Prettier code style! on all three head files. So this is normalization of drift main already had.

The cost is review-surface, not correctness: a perf(cli) PR whose diff touches web-shell's largest message component will conflict with any concurrent web-shell work, and the change is invisible in the description. Worth either dropping from this PR or naming in it.

4. The dist-containment guard is narrower than the manifest it cites — Nice to have

The new guard rejects any resolved target that is not under packages/core/dist/ (plus an explicit allow-clause for core's own package.json). Its comment justifies this from core's "files", which is ["dist", "vendor", "scripts/postinstall.js"]. The predicate implements dist only, so a future ./vendor/* export would be published by npm and rejected by this gate.

Measured as correct today: all 96 real specifiers the gate collects are accepted — 0 containment-rejected, 0 existsSync-rejected, 0 resolve-rejected. And no real specifier routes through ./src/*, so the guard is redundant defence in-repo: it fires only on the injected …/src/utils/errors.ts probe, and in CI it is pinned solely by its own fixture (M8 kills it). Correct as it stands; the mismatch is between the comment's reasoning and the code's predicate.

Two properties of the guard that hold and are worth recording: existsSync runs before containment, which matters because import.meta.resolve realpaths targets that exist (…/packages/core/dist/…) but cannot realpath ones that do not, leaving them under node_modules/… — reversed order would report every typo as "not published"; and .. segments are rejected outright (ERR_INVALID_MODULE_SPECIFIER, 2/2), so ./* cannot escape dist/, which is what the reviewer test plan's claim 2 asserts.

5. The two failed assertions are my census model, not the PR

assertions.json reports 48/2. Both fails are calibration limits of the supplementary static walk, stated here so the counts are not read as PR defects:

  • the census model agrees with the measured barrel flag on every cell, both arms — 2 disagreements, both SkillReviewDialog.test.tsx, measured loaded vs predicted not reached, in the same direction on both arms. The model under-predicts; the 32 real vitest runs are unaffected.
  • census cross-check: Δ production barrel importers == 110 migrated files — measured Δ 81 against 110 migrated. Last round's model closed this cross-check exactly; mine does not, which is evidence about my edge extraction (it does not follow unaliased bare packages into workspace source) and not about the migration.

Per the verdict contract a nonzero fail rules out merge-ready; the verdict is findings on the strength of §1, §3 and §4, and these two fails are declared as harness limitations rather than silently dropped.

Mutation matrix

Witness 02-mutation-matrix-7-of-8-killed.png. Unmutated baselines ran first and are green — gate exit 0 with All 96 core subpath specifiers resolve, gate test 6/6, sync test 2/2 — so every kill is attributable to its mutation. git status --porcelain -uno is empty after all restores, and the base worktree and both scratch vitest configs were removed.

# guard reverted mutation oracle result
M7 gate scans acp-bridge + sdk-typescript (the F2 fix) drop both dep dirs from scannedSources gate test killed — 2 failed | 4 passed, expected '✓ …/config/st…' to contain '✓ …/goalWire'
M8 gate rejects targets outside the published dist/ disable the containment branch gate test killed — 1 failed, fails when a specifier routes through the unpublished ./src/* entry
M9 gate allows the published ./package.json remove && resolved !== corePackageJson gate test killed — 1 failed, expected 1 to be +0
M10 gate imports a probed target and checks the named export disable the export-name check gate test killed — 1 failed, fails when a probe target exists but lacks the probed export
M11 core exports ./* catch-all (last round's M1) delete "./*": "./dist/src/*" the gate itself killed — exit 1, 88 of 96 specifiers fail
M12 loader named map covers every exports entry delete the conversationsRuntimeMarker row sync test killedloader named map is missing exports entry ./conversationsRuntimeMarker
M13 loader named map points each name at the right module repoint goalWire at utils/transcript-records.ts (exists, wrong) sync test SURVIVED — 2/2 green, and the loader really resolves goalWire to the wrong module. Coverage gap (§1)
M14 positive control, same file as M13 point goalWire at a nonexistent file sync test killedloader entry goalWire -> …/goals/does-not-exist.ts does not exist

7/8 killed. M13's survivor is classified a coverage gap rather than dead code or redundant defence: the mapping is live, the loader uses it, and driving the mutated loader through a real resolve() shows a wrong module being loaded — so the missing assertion has an observable consequence.

Not covered

  • The whole cli unit suite on either arm. ~84 minutes per arm does not fit the budget, so the PR's headline "green across the unit suite" is still not independently reproduced. What ran instead: 32 vitest invocations (16 cells × 2 arms) plus the gate's own test files. The remaining ~995 cli test files are unmeasured.
  • Last round's M3–M6 (mock move onto utils/gitUtils.js, named subpaths ahead of the wildcard alias, dev.js subpath→source redirect, resolveExportTarget pattern matching) were not re-run. None of the six delta commits touches scripts/dev.js, eslint-rules/, or the two mock moves, and packages/cli/vitest.config.ts is unchanged between the two heads for those entries — but that is an argument from the diff, not a measurement, so those four rows carry last round's status and no new one.
  • npm run lint and npm run typecheck, repo-wide. The changed eslint rules were not run across the tree; only the gate and its fixtures were exercised. A new false positive elsewhere would not show up here.
  • integration-tests/tsconfig.json's wildcard. Read, not compiled — same gap as last round. The claim that TypeScript's longest-prefix paths matching keeps the named entries authoritative is plausible and unbacked by a tsc run.
  • The rest of ci.yml. Only the Check core subpath exports resolve step was replayed: its run: block was extracted with a YAML parser (not retyped) and executed under the job's own shell contract (defaults.run.shell: bashbash -e), giving REPLAY_EXIT=0 after a real npm run build --workspace=packages/core and a 96/96 gate. The replay is uncalibrated — no artifact this step has previously emitted is retrievable from inside a token-free container, and there is no previous-report.md content produced by this step to compare against. The step's if: gate (ci_profile == 'full') was read, not evaluated.
  • Per-commit attribution. git rev-parse --is-shallow-repositorytrue; git rev-list HEAD^1..HEAD^2 returns 1 while $QWEN_VERIFY_CONTEXT lists 21 commits. Only the aggregate HEAD^1..HEAD diff was verified, and the six delta commits are identified from the snapshot's commit list, not from reachable objects. The previous round's head ccbc6ac6d78a is not present locally, so no round-to-round diff was possible.
  • The PR's own CI numbers (2223s collecting, 84 minutes, the 16-file/127-test first full run, the 146 restored modules). Lane history this container cannot see; the census above is an independent estimate of the same mechanism, not a check of those figures.
  • Runtime side-effect risk — the description's named main risk. Not probed: no flipped cell changed test count or behaviour, and the shipped bundle evaluates the same modules, but no systematic audit of the 578 barrel modules was attempted.
  • The bundle's size. The description says it was not measured and should be unchanged. Not measured here either.

Methodology

Environment: the CI verify container, node:22-bookworm, Node v22.23.2, working tree at refs/pull/10957/merge (depth 2), npm ci + npm run build already complete. Control arm: git worktree add tmp/base-tree HEAD^1, wired by setup-base.mjs — 1772 entry-by-entry symlinks with all @qwen-code/* links re-rooted at the base tree, no shared vite cache, and 8 contamination assertions; removed with git worktree remove --force after the cells were captured. Harnesses are in this directory and rerunnable: setup-base.mjs, make-count-config.mjs + ab-driver.mjs (the 32 vitest cells through a counting plugin, with cross-tree contamination detection), closure-census.mjs (static barrel-reachability walk, alias rules parsed from each arm's own config), validate-census.mjs (calibrates the walk against the measured cells and emits the A/B assertions), loader-probe.mjs (extracts and evaluates each arm's loader verbatim, then classifies every core specifier as redirected or fallen-through, plus a 14-specifier resolve sweep with an extraction validity control), stale-dist-probe.mjs (where each core subpath really lands with a dist present), exports-probe.mjs (real Node resolver, ESM and CJS, over the exports map plus the gate's own 96-specifier census), mutations.mjs (the matrix). Raw per-cell stdout/stderr and JSON are in logs/ (base-*.log, head-*.log, ab-results.json, census-{base,head}.json, loader-probe-*.json, loader-sweep.json, stale-dist-probe.{out,json}, exports-probe.{out,json}, mutations.{out,json}, ci-step-replay.log, rerun-idle-head.log, assertions-detail.json). Images 01–03 were captured live as those harnesses printed, via node scripts/verify-capture.mjs. Wall times were measured on a shared runner with other harnesses in this round running concurrently, equally on both arms and with each cell's two arms adjacent in time; the module counts, which carry the verdict, are unaffected by load.

Flakiness gate log

rounds=5 files=10 skipped=0
file packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/StandaloneSessionPicker.test.tsx
file packages/cli/src/ui/hooks/slashCommandProcessor.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/slashCommandProcessor.test.ts
file packages/cli/src/ui/hooks/useGitBranchName.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useGitBranchName.test.ts
file packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useMemoryMonitor.test.ts
file packages/web-shell/client/components/messages/ToolGroup.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/messages/ToolGroup.test.tsx
file scripts/tests/check-core-subpath-exports.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/check-core-subpath-exports.test.js
file scripts/tests/dev.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/dev.test.js
file scripts/tests/no-core-root-barrel-import.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/no-core-root-barrel-import.test.js
file scripts/tests/no-core-utils-upward-import.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/no-core-utils-upward-import.test.js
file scripts/tests/text-capture-core-loader-sync.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/text-capture-core-loader-sync.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: PPP
  packages/cli/src/ui/hooks/slashCommandProcessor.test.ts: PPP
  packages/cli/src/ui/hooks/useGitBranchName.test.ts: PPP
  packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: PPP
  packages/web-shell/client/components/messages/ToolGroup.test.tsx: PPP
  scripts/tests/check-core-subpath-exports.test.js: PPP
  scripts/tests/dev.test.js: PPP
  scripts/tests/no-core-root-barrel-import.test.js: PPP
  scripts/tests/no-core-utils-upward-import.test.js: PPP
  scripts/tests/text-capture-core-loader-sync.test.js: PPP

verdict: timeout
summary: only 3 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/slashCommandProcessor.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 1 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 1 · scripts/tests/dev.test.js: P (exit 0)
round 1 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 1 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 1 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 2 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/slashCommandProcessor.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 2 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 2 · scripts/tests/dev.test.js: P (exit 0)
round 2 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 2 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 2 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 3 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/slashCommandProcessor.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/messages/ToolGroup.test.tsx: P (exit 0)
round 3 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 3 · scripts/tests/dev.test.js: P (exit 0)
round 3 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 3 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 3 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)

Evidence images

01-ab-cells-base-vs-head

02-mutation-matrix-7-of-8-killed

03-loader-ab-base-vs-head

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

4 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • PR description mock-move count (three landed, description says two; 110 vs 109) — already reported (issue-level triage comments 5529582370 and 5529583464 at the current head)
  • Dev-loader named-subpath fall-through to stale dist (scripts/dev.js:95) — already reported (issue-level triage comment 5529582959, non-blocking follow-up)
  • Gate source scan covers 3 of the cli's runtime file: workspace dependencies (scripts/check-core-subpath-exports.mjs:47) — already reported as R5-2 (comment 3937838491); author declined to fix at this head
  • integration-tests files outside every npm workspace (integration-tests/tsconfig.json:63) — already reported as R1-6 (comment 3931108394), still open

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": none — all checks above completed. (Note: the tail of the ToolGroup.tsx diff section past diff line 2701 belongs to the next chunk and was left to its agent.).

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:

  • scripts/check-core-subpath-exports.mjs:47 — [review] R5-2: still stands (author declined to fix at this head) — the gate's source scan hardcodes 3 of the cli's runtime file: workspace dependencies

Convergence: round 6 posted 8 inline comment(s), 8 of them reported for the first time; the previous round posted 5 (5 new). Findings keep coming back to the same files: integration-tests/terminal-capture/skill-review-harness/text-capture.tsx (findings in round 5; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

本轮确认的 4 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)"none — all checks above completed. (Note: the tail of the ToolGroup.tsx diff section past diff line 2701 belongs to the next chunk and was left to its agent.)

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 6 轮发布了 8 条行内评论,其中 8 条是首次提出;上一轮发布了 5 条(其中 5 条首次提出)。发现反复回到同一批文件:integration-tests/terminal-capture/skill-review-harness/text-capture.tsx(第 5 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread integration-tests/tsconfig.json
Comment thread scripts/tests/text-capture-core-loader-sync.test.js
Comment thread scripts/tests/text-capture-core-loader-sync.test.js
Comment thread packages/cli/vitest.config.ts
Comment thread packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx
Comment thread scripts/tests/no-core-utils-upward-import.test.js Outdated
Comment thread eslint-rules/no-core-utils-upward-import.js
yiliang114 and others added 3 commits September 5, 2026 11:48
… in tsconfig paths

The ninth named exports entry was missing from the paths block, so
`@qwen-code/qwen-code-core/conversationsRuntimeMarker` (imported by
packages/cli/src/config/shared-env-keys.ts) fell through the wildcard to
a nonexistent substitution and resolved against packages/core/dist — or
failed with TS2307 on an unbuilt tree — the exact stale-dist mode this
block's comment warns about. Add a sync gate asserting every named
exports key whose target stem differs from the key has a paths entry
pointing at the matching source file.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtntktgsk8
…ore exports

The harness loader sync gate only checked key coverage (exports key ->
loader entry) and file existence, so two drifts passed it green:
a retargeted exports entry left the loader serving the old module (the
named map short-circuits ahead of the stem probe), and a stale
loader-only entry short-circuits captures on a module graph the shipped
package can no longer resolve. Assert each loader entry equals the
exports import target mapped into source space, and every loader entry
has a matching exports key. Mutating either direction now fails the
suite.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtntktgsk8
The rule verdicts by directory layer and never checks file existence, so
the goalWire fixture reported one violation whether the exact exports
key or the ./* wildcard resolved it — removing the exact-key branch of
resolveExportTarget left the test green. Switch the fixture to
transcriptRecords, whose exact resolution lands inside utils/ (allowed)
while the wildcard resolution lands outside (violation), so dropping the
exact-key branch now flips the assertion from 0 to 1.

Co-authored-by: Qwen-Coder <qwen-coder@alibaba-cloud.com>
Patrol-Run: qwen-pr-closeout/jmtntktgsk8

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

⚠️ Downgraded from Approve to Comment: CI failing: Real daemon E2E / Java 11. Partially reviewed — gaps disclosed.

Not reviewed: build-and-test — the four changed files sit outside every npm workspace; the scoped harness built and tested zero suites (their CI lanes are test:scripts and typecheck:integration).

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:

  • scripts/tests/integration-tsconfig-core-paths-sync.test.js:50 (+1 locations) — [review] D7-1 the new paths sync gate pins only the named-exports-key tier; the load-bearing wildcard entry (integration-tests/tsconfig.json:66) is asserted by n…
  • scripts/tests/integration-tsconfig-core-paths-sync.test.js:37 — [review] D7-2 the two new sync gates paste the same core-exports interpretation verbatim (namedKeys filter + dist/src stem derivation); extract a shared helper per the workflow…
中文说明

⚠️ 已从批准降级为评论:CI failing: Real daemon E2E / Java 11。 仅完成部分审查,审查缺口已披露。

未审查(原文为英文):build-and-test — the four changed files sit outside every npm workspace; the scoped harness built and tested zero suites (their CI lanes are test:scripts and typecheck:integration).

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.23.0)

yiliang114 and others added 3 commits September 5, 2026 16:56
packages/cli/vitest.config.ts named the cli tsconfig `paths` block as the
trigger for adding a named core subpath alias, but only three of the nine
entries (noFollowOpen, subSessionConstants, transcriptRecords) appear there;
the source of truth is the named keys of the `exports` map in
packages/core/package.json. The comment now says so, and records that this
map — unlike the skill-review-harness loader's, which
scripts/tests/text-capture-core-loader-sync.test.js checks against core's
exports — has no gate.

StandaloneSessionPicker.test.tsx's wiring test claimed to pin the wrapper's
import, but StandaloneSessionPicker.tsx is not in that suite's module graph:
the tests render SessionPicker, which takes `currentBranch` as a prop, so the
stub's only consumer is the test file's own import. Renamed the test and
corrected both comments to state what it pins and what it does not cover.

Comment and test-name only; no behavior change.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto4amd1ku
One conflict, in
integration-tests/concurrent-runner/export-html-from-chatrecord-jsonl.js.
main removed the legacy-exported-JSONL render path (it now throws and asks
for source ChatRecord JSONL); this branch still carried
buildLegacySessionData plus the let/else shape around it. This branch's only
change to that file was a prettier reflow of the buildProductSessionData
call, which main's version already contains, so taking main's side drops
nothing this PR wanted and keeps main's safety decision.

Verified: no conflict markers left, node --check clean, no remaining
buildLegacySessionData reference anywhere in the tree, packages/webui absent
from the merged index (main retired it in #9812), and
"tsc -p integration-tests/tsconfig.json" reports no error in the resolved
file. The tsc errors that do appear are unbuilt-workspace phantoms
(@qwen-code/acp-bridge/* subpaths, generated/git-commit.js, generated
web-templates modules) which need npm run build rather than merge repair;
the full typecheck lane runs on push.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmto8kxx1l2
One overlapping file: packages/cli/src/ui/components/agent-view/AgentComposer.tsx.
main (#10315) removed the isTerminalStatus() call site, so the merged file keeps
this branch's subpath import split (AgentStatus from agents/runtime/agent-types.js,
ApprovalMode/APPROVAL_MODES from config/approval-mode.js) and drops the now-unused
isTerminalStatus import. package.json and ci.yml auto-merged with only this branch's
own additions (check:core-subpath-exports script and its CI step).

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-closeout/jmtof0f82lb
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Merge conflict cleared508b0026c..6c9d2bf21 (merge of origin/main @ e09a45c52, non-force).

main moved since the last merge and left this PR DIRTY, which blocks it outright. Exactly one file overlapped between the two sides:

  • packages/cli/src/ui/components/agent-view/AgentComposer.tsxfix(cli): deliver Agent View queued follow-ups from the provider #10315 removed the isTerminalStatus() call site while this branch was rewriting the import block to core subpaths. Taking main's side verbatim would have left AgentStatus being imported from config/approval-mode.js (the wrong module), so the resolution keeps the subpath split and drops only the now-unused isTerminalStatus:
    • AgentStatus@qwen-code/qwen-code-core/agents/runtime/agent-types.js (export enum AgentStatus, agent-types.ts:140)
    • ApprovalMode / APPROVAL_MODES@qwen-code/qwen-code-core/config/approval-mode.js

The other two files both sides touched (package.json, .github/workflows/ci.yml) auto-merged carrying only this branch's own additions (check:core-subpath-exports and its CI step) — confirmed by diffing the merge result against main.

Verification: no conflict markers repo-wide; npx eslint on the resolved file exits 0; a symbol-level sweep of every file this branch and main both touched found no core import lost by the auto-merge (0 missing). The packages/cli suite and typecheck were not run locally — this machine cannot build the packages/audio-capture native module (node-gyp rebuild fails), and both the cli vitest globalSetup gate and tsc --noEmit require that build. CI is the authoritative check for this merge.

CI attribution for the previous head 508b0026c: Test (ubuntu-latest, Node 22.x) failed on runner disk exhaustion rather than on this diff — run 33962652891 uploaded a disk-pressure-run-33962652891-attempt-1 artifact and the log carries ENOSPC: no space left on device; web-shell E2E Smoke was cancelled downstream of it. The two test names that did report FAIL (MessageList.dom.test.tsx, export-transcript-document.test.ts) are outside this branch's 113 changed cli files. Leaving that on the maintainer side — nothing to fix in this PR.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

3 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • D8-23 StandaloneSessionPicker.test.tsx added mock-wiring block — already reported as R6-6 (comment 3939170878)
  • D8-24 scripts/dev.js named-subpath fall-through to stale dist — already reported (issue-level triage comment 5529582959, non-blocking follow-up)
  • D8-25 no-core-root-barrel-import.js CORE_BARREL_SPECIFIERS /dist/src/index.js spelling — already reported as D5-3 (round-5 deferral), tracked under open thread R1-7 (comment 3931108340)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

Not reviewed: finding D8-29 — the verifier never ruled on it: the reverse-audit loop stopped on the review time budget before a further verification round could be built, so it is reported terminal-only at low confidence.

Not explored to full depth (tool budget reached): chunk 5: none — but for transparency, two checks I deliberately did not run because they are outside my chunk's authorship and were settled by precedent instead: npm ru…; "agent reverse-audit (round 1)": bundle *evaluation* (module-init-order / TDZ after the reorder) was not completed — my ad-hoc esbuild invocation cannot reproduce the repo's banner + per-file….

Not reviewed: reverse audit — stopped before round 5 by the review time budget.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:

  • .github/workflows/ci.yml:1215 — [review] New gate cold-rebuilds packages/core a second time in the…
  • packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx:24 — [probe] Both factual claims in the added scope note are measurably…
  • scripts/check-core-subpath-exports.mjs:18 — [probe] Gate's fileoverview and failure text claim a published-CLI…
  • scripts/tests/check-core-subpath-exports.test.js:120 — [probe] Fixture never exercises the glob's tsx or recursion arms,…
  • scripts/check-core-subpath-exports.mjs:49 — [probe] A renamed scan root silently degrades the gate to its four…
  • scripts/tests/check-core-subpath-exports.test.js:123 — [probe] Untokenized harvest counts test-file mock strings and…
  • eslint-rules/no-core-utils-upward-import.js:70 — [probe] Condition tier reads only import ; a default -only key…
  • scripts/tests/text-capture-core-loader-sync.test.js:59 — [probe] Sync gate never pins the loader's base URL, whose trailing…
  • scripts/check-core-subpath-exports.mjs:75 — [probe] Containment prefix is not canonicalized, so a…
  • packages/cli/src/ui/utils/modelsBySource.ts:11 — [probe] The converted modules' own colocated tests still…
  • packages/cli/vitest.config.ts:31 — [probe] The reachability clause this diff adds is false for one of…
  • scripts/check-core-subpath-exports.mjs:89 — [probe] existsSync passes for a directory, so a directory…
  • scripts/tests/check-core-subpath-exports.test.js:82 — [probe] No fixture reaches the !existsSync branch, so its accurate…
  • scripts/tests/dev.test.js:158 — [probe] The data: URL import escapes the node:fs mock, so…
  • scripts/tests/integration-tsconfig-core-paths-sync.test.js:51 (+3 locations) — [probe] CLASS: both new sync gates assume every named exports key…
  • scripts/tests/integration-tsconfig-core-paths-sync.test.js:40 (+1 locations) — [probe] CLASS: the tsconfig sync gate's assertions are incomplete…
  • integration-tests/terminal-capture/skill-review-harness/text-capture.tsx:227 — [probe] The comment this diff adds names a command that cannot…
  • scripts/check-core-subpath-exports.mjs:31 — [probe] Hardcoded probe contract reddens a required gate on a…
  • scripts/check-core-subpath-exports.mjs:96 — [probe] Containment allows dist/ only while its own text recites…
  • scripts/tests/no-core-utils-upward-import.test.js:32 — [probe] The tie-break test is written in the key order a…
  • …and 1 more (see the run report)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 3 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) unit lanes were skipped in CI; OS-specific portability of the new scripts tests verified by reading only.

未审查(原文为英文):finding D8-29 — the verifier never ruled on it: the reverse-audit loop stopped on the review time budget before a further verification round could be built, so it is reported terminal-only at low confidence.

未探索到全部深度(达到工具调用预算):chunk 5:none — but for transparency, two checks I deliberately did not run because they are outside my chunk's authorship and were settled by precedent instead: npm ru…"agent reverse-audit (round 1)"bundle *evaluation* (module-init-order / TDZ after the reorder) was not completed — my ad-hoc esbuild invocation cannot reproduce the repo's banner + per-file…

未审查:反向审计——评审时间预算不足,未能开始第 5 轮。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 21 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.23.0)

Comment thread .github/workflows/ci.yml
yiliang114 and others added 2 commits September 6, 2026 04:12
Catch the branch up with main so CI validates the current tree against the
current gates instead of a 27-commit-old checkout.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtosl3ualt
`Check core subpath exports resolve` was added to the lint_and_static job
without being added to the pin that asserts that job's full-gated payload by
name and order, so the guard failed with 19 received against 18 expected.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Patrol-Run: qwen-pr-conflict/jmtosl3ualt

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

12 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • integration-tsconfig-core-paths-sync.test.js gate key-filter coverage — root key :35-38, wildcard key :37, reverse direction :41-42 — already reported (round-8 CLASS deferrals at :40 and :51)
  • .github/workflows/ci.yml:1215 duplicate cold core rebuild in the new gate step — already reported (round-8 deferral, same line)
  • scripts/dev.js:96 named-subpath fall-through to stale dist — already reported (round-8 duplicate drop D8-24; issue-level triage comment 5529582959)
  • StandaloneSessionPicker.test.tsx:94 mock-wiring block has no consumer in the suite graph — already reported as R6-6 (comment 3939170878)
  • check-core-subpath-exports.mjs:18 fileoverview and CI-comment blast-radius claim — already reported (round-8 deferral, same line)
  • check-core-subpath-exports.mjs:48 untokenized specifier harvest — already reported (round-8 deferral)
  • check-core-subpath-exports.mjs:89-95 unreached !existsSync branch — already reported (round-8 deferral at check-core-subpath-exports.test.js:82)
  • check-core-subpath-exports.test.js:120-123 fixture is .ts-only so the harvest glob's .tsx arm is unpinned — already reported (round-8 deferral, same line)
  • dev.test.js:172-174 data: URL loader import escapes the node:fs mock — already reported (round-8 deferral at dev.test.js:158)
  • text-capture.tsx:260 stem-probe fallback executed by no test — already reported and settled as R4-2 (comment 3933497731)
  • text-capture.tsx:227-228 added comment names a command that does not exercise the loader — already reported (round-8 deferral at :227)
  • no-core-root-barrel-import.js CORE_BARREL_SPECIFIERS omits the dist/src/index.js spelling — already reported as D5-3, tracked under open thread R1-7 (comment 3931108340)

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) is skipped in CI at this commit and its suite did not run locally, while this PR changes integration-tests/tsconfig.json and integration-tests/terminal-capture/skill-review-harness/text-capture.tsx; only npm run typecheck:integration was executed here (clean).

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) are merge_group/schedule/workflow_dispatch-gated and report skipped, so the new scripts tests were never executed on those hosts; their portability was verified by reading plus targeted POSIX probes (symlinked TMPDIR runs, pathToFileURL separator semantics, fs.globSync brace expansion, junction analysis, win32 exclude-list checks) rather than by a run on either lane.

Not reviewed: build-and-test — the test-efficacy probe established no mutation coverage for this diff: harnessValidated is null because no probe file was green in the unmutated baseline (skippedForBaseline: 2), and 111 hunks were never reverse-applied (cap of 6), so no claim is made that the PR's tests fail when its code is broken.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 4)": tsc -p integration-tests/tsconfig.json was not executed, so the tsconfig comment's claim that nodenext substitutes .js for .ts on a *paths-substituted* ta…; "agent reverse-audit (round 4)": the packages/cli vitest suite was not run, so the mock-bypass layer above is established by static cross-referencing of mock factories against post-migration …; "agent reverse-audit (round 3)": Windows-lane confirmation that fileURLToPath(import.meta.resolve(…)) and path.join(root, 'packages', 'core', 'dist') + path.sep agree on drive-letter casing…; "agent 8b": I did not mutation-test each of the 22 root-mocking suites individually (only the three relocated-mock suites); their clearance rests on the static symbol-inter…; "agent 8b": the static closure follows only relative specifiers inside packages/cli/src ; modules reached through other workspace packages ( acp-bridge , sdk-typescript ,…, and 1 more.

Not linted (tool limitation, not a blocker): .github/workflows/ci.yml — actionlint embedded-shell source mapping is not yet supported.

Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:

  • packages/cli/vitest.config.ts:82 — [probe] Wildcard alias capture also swallows the non-src core spellings the exports map blesses (package.json, src/*, dist/*)
  • packages/cli/vitest.config.ts:89 — [probe] RegExp alias form is invisible to the build guard's text-scraping alias set (real function: 35 -> 34, losing the core root)
  • scripts/check-core-subpath-exports.mjs:37 — [probe] CLASS: the gate's checked set cannot reach any exports entry no harvested specifier names — measured on the root "." entry and on "./dist/*"
  • scripts/tests/integration-tsconfig-core-paths-sync.test.js:50 — [probe] Sync gate's mirroring exemption skips exactly the class the tsconfig wildcard cannot serve (all 9 named keys are extensionless)
中文说明

仅完成部分审查,审查缺口已披露。

本轮确认的 12 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

未审查(原文为英文):build-and-test — Integration Tests (CLI, No Sandbox) is skipped in CI at this commit and its suite did not run locally, while this PR changes integration-tests/tsconfig.json and integration-tests/terminal-capture/skill-review-harness/text-capture.tsx; only npm run typecheck:integration was executed here (clean).

未审查(原文为英文):build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) are merge_group/schedule/workflow_dispatch-gated and report skipped, so the new scripts tests were never executed on those hosts; their portability was verified by reading plus targeted POSIX probes (symlinked TMPDIR runs, pathToFileURL separator semantics, fs.globSync brace expansion, junction analysis, win32 exclude-list checks) rather than by a run on either lane.

未审查(原文为英文):build-and-test — the test-efficacy probe established no mutation coverage for this diff: harnessValidated is null because no probe file was green in the unmutated baseline (skippedForBaseline: 2), and 111 hunks were never reverse-applied (cap of 6), so no claim is made that the PR's tests fail when its code is broken.

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 4)"tsc -p integration-tests/tsconfig.json was not executed, so the tsconfig comment's claim that nodenext substitutes .js for .ts on a *paths-substituted* ta…"agent reverse-audit (round 4)"the packages/cli vitest suite was not run, so the mock-bypass layer above is established by static cross-referencing of mock factories against post-migration …"agent reverse-audit (round 3)"Windows-lane confirmation that fileURLToPath(import.meta.resolve(…)) and path.join(root, 'packages', 'core', 'dist') + path.sep agree on drive-letter casing…"agent 8b"I did not mutation-test each of the 22 root-mocking suites individually (only the three relocated-mock suites); their clearance rests on the static symbol-inter…"agent 8b"the static closure follows only relative specifiers inside packages/cli/src ; modules reached through other workspace packages ( acp-bridge , sdk-typescript ,…,另有 1 条。

未检查(工具限制,非阻断):.github/workflows/ci.yml——actionlint 对 workflow 内嵌 shell 的源映射尚未支持。

收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.23.0)

@yiliang114

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 206 passed · 3 failed · 209 total

Flakiness gate: ✅ 10 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:206 通过 · 3 失败 · 209 总计

抖动门:✅ 10 changed test file(s) x 5 identical rounds, no divergence

Verification report (report.md, truncated)

# PR 10957 — deep verification (follow-up round 3)

**Verdict: `findings`** (non-blocking) — 209 scripted assertions, **206 pass / 3 fail**.
Verified head `cdabd7dad32ba0ee7fe4219da1e2b86aafa67069` (`git rev-parse HEAD^2`), base `e133150ed18d891e4c7694cdbae650b95c358289` (`HEAD^1`).

Nothing regressed and the central claim re-measured clean: **4 of 16 A/B cells flip from loading core's 579-module barrel to not loading it** (586 → 16 transformed modules, vitest `collect` 6.04s → 0.16s), the other 12 are byte-identical in module count, no cell loads more at head, and cross-tree contamination was 0 in all 32 runs. The 3 failed assertions are the three mutation survivors (M1, M2, M14) — real coverage gaps in guards this PR added, named in *Findings*.

The delta since the last round is 8 commits (`30ddedb1ec76`, `b235256cc008`, `fedf05311600`, `cb9ee870e9a6`, three `main` merges, `cdabd7dad32b`). It closes the previous round's sharpest finding outright and lets two gaps that were listed as *Not covered* be measured for the first time:

- **Previous finding §1 (the M13 survivor) is FIXED.** Commit `b235256cc008` added the assertion last round suggested; the identical mutation is now killed.
- **The integration tsconfig wildcard is load-bearing, proven by compilation** — an isolated base tree reproduces the exact `TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js'` the PR body reports, and head compiles the same file with 0 errors.
- **The two changed eslint rules introduce zero new false positives** across all 1348 files of `packages/core/src`, and both changes are load-bearing.
- **Last round's red cell is green on both arms now** (`SkillReviewDialog` 0 failed / 33 passed at base *and* head, against 1 failed / 32 passed on both last round).

<details>
<summary>中文摘要 — 判定:`findings`(非阻塞)· 209 条断言 206 通过 / 3 失败</summary>

验证 head `cdabd7dad32b`,base `e133150ed18d`。

**A/B 结论**:上一轮的 16 个单元在新 head/base 上全部重跑(32 次真实 vitest 运行)。16 个中 **4 个从「加载 core 的 579 模块 barrel」翻转为「不加载」**(586→16 个 transform 模块,vitest `collect` 6.04s→0.16s),其余 12 个模块数**逐字节相同**;无任一单元在 head 加载更多模块;32 次运行跨树污染均为 0;每个单元两侧测试计数完全一致。本轮新增了描述所声称机制的直接证据:节省**全部落在 `collect`**(翻转单元合计 24.7s→0.6s),而 `tests` 时间不变(0.8s→0.9s);barrel 存活的 12 个单元 `collect` 毫无改善。详见 *Central claim + A/B* 表与 `01-ab-cells-base-vs-head.png`。

**上一轮发现的处置**:§1(loader sync 测试只校验键覆盖、不校验映射正确性,M13 存活)**已修复** —— commit `b235256cc008` 加入了上一轮建议的断言,同一变异现被干净杀死(M4),反向键集变异 M7 亦被杀死。§3(三个无关文件的纯格式化改动)**已从 diff 中消失**(合并 main 后基线已含同样格式化)。C1(CI 注释与门禁文件头声称「`qwen` 启动即挂」)**仍然成立**,三处文本均未改动:发布 bundle `dist/cli.js`+`dist/chunks/` 中该说明符出现 **0 次**、整个 `dist/` 下 **0 个文件**,而未打包 tsc 产物 `packages/cli/dist/src/` 中出现 **269 次**。F1(实际收益远小于描述引用的整条 lane 收集成本)**仍然成立**,但描述自身的数字本轮得到精确验证:解析器级普查得生产文件根 barrel 值导入 **460 → 351(Δ −109,与「109 个模块」逐字吻合,新增 0)**,上一轮未闭合的交叉校验本轮**闭合**。F3(`./*` 把未知子路径从解析期错误变成缺失文件)**仍然成立**,245 行门禁测试中 0 处覆盖该分支。§4(dist 容纳守卫窄于其引用的 manifest)**仍然成立**。

**本轮新发现**:
1. 新增的 `integration-tsconfig-core-paths-sync.test.js` **从不断言通配条目本身存在**。删除 `"@qwen-code/qwen-code-core/*"`(M1)或把它指向不存在的目录(M2),该测试仍 1/1 全绿;而同文件正向对照 M3(删除 `noFollowOpen` 具名条目)干净变红。通配条目恰恰是本轮证明为 load-bearing 的那一条。
2. `resolveExportTarget` 的**同长度前缀 tie-break 子句未被其同名测试钉住**(M14 存活)。实测该子句真实有效(把 fixture 声明顺序反转后,变异体会解析到错误目标),但在 core 现有 exports 下**不可达**(`./*`=2、`./dist/*`=7、`./src/*`=6,无同长度前缀对)。已测量修复:补一条反序用例后 head 仍全绿、M14 变红、其余计数不变。
3. **cli vitest alias 映射是同一份九条映射的三个副本中唯一无门禁的一个** —— 而它是 CI 每次运行真正使用的那一个。其注释自陈「this map has no gate and is kept in sync by hand」。实测:删除 `goalWire` 条目(M17)或把通配项移到具名项之前(M18),`scripts/tests/unit-vitest-configs.test.ts` 仍 26/26 全绿;而一个导入该说明符的临时 cli 测试无法解析。本 PR 为另外两个**较少被执行**的副本都加了门禁(手动运行的 harness loader:3 个测试;integration tsconfig:1 个测试)。这正是评审测试计划第 1 条要求人工判断的排序。

**未覆盖**:cli 全量单元套件(约 84 分钟 × 2 臂)、仓库级 `npm run lint` / `npm run typecheck`(仅对 `packages/core/src` 跑了真实 eslint)、套件级「多少测试文件不再触达 barrel」的闭包普查(本轮未重跑,沿用上一轮 −21/1011 的估计)、按 commit 归因(浅克隆:本地 `rev-list HEAD^1..HEAD^2` 只返回 1,快照列 29 个)、`scripts/dev.js` 与两处 mock 搬迁的变异(未重跑)、F1/M1-M2 的建议修复未全部实测。

</details>

## Previous-round findings — status at the new head

Last round verified head `47128e7007c6`, base `74fe3a659d`. Neither is reachable in this depth-2 checkout (`git cat-file -t 47128e7007c6…` → *could not get object info*), so nothing was diffed against the old report: **every measurement below was re-run at the new head**, including all 16 A/B cells on both arms, the base-arm setup, the exports sweep, the mutation matrix and the census. No input closure qualified for the carry-forward shortcut — both the head and the base moved.

| # | previous finding | severity | status at `cdabd7dad32b` |
| --- | --- | --- | --- |
| §1 | The loader-sync test pins the map's *presence*, not its *correctness* (mutation M13 survived: `goalWire` → `utils/transcript-records.ts` stayed green while the loader really resolved the wrong module) | Suggestion | **FIXED.** Commit `b235256cc008` added `points every entry at the exports target in source space` plus a reverse key-set test — the fix last round suggested, and more. Re-ran the identical mutation as **M4: killed** (`1 failed / 2 passed`, red test is the new one). **M5** control (nonexistent target) killed, **M6** (drop `conversationsRuntimeMarker`) killed, **M7** (stale loader-only entry, the new reverse direction) killed. Baseline 3/3 green |
| §2 | The harness loader is load-bearing (pass, reported because the round before could not reach it) | — | **not re-run.** No delta commit touches the loader's `named` map beyond the entries M4–M7 exercise; the loader body is unchanged since `47128e7007c6` except for comments. Carried as last round's status, labelled an argument from the diff |
| §3 | Three unrelated files carry pure formatting churn (web-shell `ToolGroup.tsx` + test, `export-html-from-chatrecord-jsonl.js`) | Nice to have | **gone from the diff.** `git diff --name-only HEAD^1..HEAD \| grep -E 'web-shell\|export-html'` → **no match**. Three `main` merges (`508b0026c0b1`, `6c9d2bf21124`, `da0498b12db9`) advanced the base past that formatting, so it is no longer this PR's churn |
| §4 | The dist-containment guard is narrower than the manifest it cites (`"files"` also publishes `vendor` and `scripts/postinstall.js`) | Nice to have | **stands.** Re-measured: `"files": ["dist","vendor","scripts/postinstall.js"]`, and the gate's predicate mentions neither `vendor` nor `scripts/postinstall.js`. Still correct today — all **96** real specifiers accepted, 0 containment-rejected, 0 existsSync-rejected, 0 resolve-rejected |
| §5 | Two failed assertions were the round's own census model, not the PR | — | **superseded — and the model is gone.** This round replaced the graph model with TypeScript's own parser; the cross-check that failed last round (Δ81 against 110 migrated) now **closes exactly** at Δ109 = 109 migrated, 0 added |
| C1 | Correction: `ci.yml` and the gate header claim a broken exports map "breaks `qwen` at startup", but the shipped CLI is an esbuild bundle that inlines core | correction | **stands, text unchanged at all three sites.** `grep -roh "@qwen-code/qwen-code-core/" dist/cli.js dist/chunks/` → **0**; `grep -rl` over all of `dist/` → **0 files**; `packages/cli/dist/src/` → **269** (was 268). Real consumers of the unbundled tree: `serve-ab.yml:294,307`, `scripts/check-serve-fast-path-bundle.js` (8 paths), `scripts/tui-parity/accept-noflicker.sh:15` |
| F1 | Delivered saving is ~22 test files (≈2% of the suite), not the suite-wide collection cost the description quotes | Suggestion | **stands**, and the description's own counts now check out — see *Closure census* below |
| F2 | The exports gate's census stopped at `packages/cli/src` | Nice to have | **fixed, re-verified.** Gate resolves **96** specifiers, exit 0. **M8** (drop both dep dirs) killed: `2 failed / 4 passed` |
| F3 | `./*` turns an unknown subpath from a resolve-time error into a URL for a missing file, leaving the gate's `existsSync` line load-bearing but unpinned | Nice to have | **stands.** `import.meta.resolve('…/does-not-exist.js')` → `node_modules/@qwen-code/qwen-code-core/dist/src/does-not-exist.js`, `exists=false`. The now-245-line gate test contains **0** occurrences of `does-not-exist` or `resolved target does not exist`; the only place that string exists is the gate's own error message. Base control: the same specifier **throws** `ERR_PACKAGE_PATH_NOT_EXPORTED`, so this shape is head-only |
| M1–M6 | Mutation matrix, 6/6 killed; M3–M6 not re-run last round | — | M1 re-run here as **M12** and killed (deleting `./*` fails **88 of 96** specifiers, gate exit 1). **M3–M6 still not re-run** — see *Not covered* |
| red cell | `SkillReviewDialog` failed 1 / passed 32 **identically on both arms** | — | **green on both arms now**: 0 failed / 33 passed at base and head, 642/642 modules. The container-specific failure last round called an A/A control no longer reproduces |

## Scope

**Central claim (perf):** importing individual core modules instead of the package root removes core's 579-module barrel from a cli test file's module graph, and the cost it removes is *collection*, not assertions.

**Secondary claims:** (a) the vitest alias ordering, core's `./*` catch-all and the integration tsconfig wildcard resolve every deep specifier the migrated sources name; (b) the guards added around all of this are load-bearing.

**This round's emphasis** is the delta: the new `integration-tsconfig-core-paths-sync.test.js`, the two new loader-sync tests that answer last round's §1, the eslint rule's `resolveExportTarget` pattern matching, and the CI lane pin added by the head commit.

## Central claim + A/B

Control: `HEAD^1` in a scratch worktree at `tmp/base-tree`, removed after the cells were captured. The PR leaves `package-lock.json` untouched, so third-party dependencies are identical across arms. **Internal workspace links were re-pointed, not assumed**: root `node_modules` was mirrored entry-by-entry (1134 symlinks) with all 24 `@qwen-code/*` links re-rooted at the base tree, and vite dep caches (`.vite`, `.vite-temp`, `.cache`) were deliberately *not* shared. **26/26 setup assertions passed**, including `realpath(base/node_modules/@qwen-code/qwen-code-core) == tmp/base-tree/packages/core` (same for `qwen-code`, `acp-bridge`, `web-templates`), per-arm markers proving the change under test is present on one side and absent on the other, and `git diff --name-only HEAD^1..HEAD -- packages/core/src` → **empty**. Only the twelve `dist/` outputs the cli `globalSetup` demands were linked from head — from packages this PR touches **0 files** in (`acp-bridge`, `web-templates`, `channels/*` all asserted).

Oracle: a counting vite plugin wrapping **each arm's own** `vitest.config.ts`, recording every module id vite transforms; one fresh vitest process per cell, 32 runs total. Module count carries the verdict; wall time on this shared runner does not. Witness: `01-ab-cells-base-vs-head.png`. Cells are the **same 16** the previous round measured, so the rounds are directly comparable.

| cell | base mods | head mods | core base→head | core barrel | `collect` base→head | tests base = head |
| --- | --- | --- | --- | --- | --- | --- |
| agent-view/supervisor-store | 586 | **16** | 579→**9** | loaded → **absent** | 6.04s → **0.16s** | 17 = 17 |
| remoteInput/RemoteInputWatcher | 586 | **15** | 579→**8** | loaded → **absent** | 5.98s → **0.15s** | 10 = 10 |
| services/tips/tipHistory | 586 | **16** | 579→**9** | loaded → **absent** | 6.07s → **0.16s** | 11 = 11 |
| ui/utils/customBanner | 588 | **18** | 579→**9** | loaded → **absent** | 6.58s → **0.17s** | 40 = 40 |
| ui/components/EffortDialog | 663 | 663 | 579→579 | loaded → loaded | 6.85s → 7.15s | 5 = 5 |
| ui/components/InputPrompt | 719 | 719 | 579→579 | loaded → loaded | 8.06s → 7.32s | 215 = 215 |
| ui/components/SkillReviewDialog | 642 | 642 | 579→579 | loaded → loaded | 7.53s → 9.24s | **33 = 33 (green both arms)** |
| ui/components/approvalModeVisuals | 609 | 609 | 579→579 | loaded → loaded | 9.04s → 8.16s | 3 = 3 |
| ui/components/hooks/HandlerListBody | 588 | 588 | 579→579 | loaded → loaded | 7.60s → 7.74s | 11 = 11 |
| ui/components/messages/AskUserQuestionDialog | 661 | 661 | 579→579 | loaded → loaded | 7.92s → 8.16s | 34 = 34 |
| ui/components/views/ExtensionsList | 680 | 680 | 579→579 | loaded → loaded | 7.60s → 7.98s | 9 = 9 |
| ui/hooks/useAtCompletion | 642 | 642 | 579→579 | loaded → loaded | 7.71s → 7.72s | 36 = 36 |
| ui/hooks/useMemoryMonitor | 8 | 8 | 0→0 | absent → absent | 0.24s → 0.19s | 8 = 8 |
| ui/opentui/dialogs-memory-status | 590 | 590 | 579→579 | loaded → loaded | 7.02s → 7.08s | 5 = 5 |
| ui/opentui/live-turn | 589 | 589 | 579→579 | loaded → loaded | 7.36s → 7.01s | 12 = 12 |
| ui/utils/tool-display-map | 586 | 586 | 579→579 | loaded → loaded | 6.79s → 6.62s | 2 = 2 |
| **totals** | **9323** | **7042** | 8685→6404 | 4 of 16 flip | 108.4s → 85.0s | 32/32 green, tallies identical |

Both shapes from the previous two rounds reproduce exactly, and this round adds the mechanism evidence the description actually claims:

- **Where it fires, it fires completely** — 586 → 16 transformed modules (−97%), core modules 579 → 9.
- **Where it cannot fire, nothing changes at all** — 12 of 16 cells identical in module count. One surviving barrel importer anywhere in the closure pulls all 579 core modules back.
- **The saving is collection, not assertions** — on the four flipped cells vitest's own `collect` goes **24.7s → 0.6s** while its `tests` time goes **0.8s → 0.9s** (unchanged). On the twelve unflipped cells `collect` does not improve at all (every pair within 3s, most within 0.4s, one 1.7s *worse* at head). This is the description's mechanism claim, asserted directly rather than inferred from wall time.
- No cell loaded more modules at head; `contam=0` in all 32 runs.

### Closure census — exact this round, no graph model

Last round's estimate came from a static reachability walk that disagreed with 2 of 32 measured cells and failed its own cross-check. This round replaced it with **TypeScript's own parser** over every file, classifying each root-specifier import the way the compiler does (`import type`, `isTypeOnly` clauses, and `export … from` all handled), so there is no alias model left to be wrong.

| | base | head | Δ | body's number |
| --- | --- | --- | --- | --- |
| cli production files **value**-importing the core root | 460 | **351** | **−109** | "Moves 109 cli modules" ✓ / "the 341 files that still import the package root for a value" (−10) |
| cli production files importing it **type-only exclusively** | 98 | **98** | 0 | "A further 101 import it for types only" (−3) |
| files that stopped value-importing the root | — | **109** | — | — |
| files that *started* value-importing the root | — | **0** | — | — |
| cli production files using a core **subpath** | 7 | **116** | +109 | — |
| distinct core subpath specifiers in cli sources | 5 | **93** | +88 | — |
| cli files naming the barrel *through* a subpath (`…/index.js`) | 0 | **0** | 0 | the new `./*` creates this door; nobody walks through it |

**The cross-check that failed last round now closes exactly**: Δ value-importers (−109) equals the count of files that stopped importing the root (109), with 0 files added. Last round measured Δ81 against 110 migrated and correctly labelled that as evidence about its own edge extraction. The two small offsets against the body's prose (351 vs 341, 98 vs 101) are consistent in direction and size on both arms, so they read as a counting-boundary difference (my census excludes `*.test.ts`, `__tests__/`, `fixtures/`; the body does not say which set it used) rather than a wrong claim — noted, not a finding.

**F1 therefore stands on firmer numbers than before.** The mechanism is real and large *per file* (≈6.0s of `collect` removed wherever it fires), but it fires on a minority of the suite: the description's own accounting leaves 351 production files still on the barrel, and last round's calibrated closure census put the suite-level effect at −21 of 1011 cli test files. Multiplying the two measured per-file numbers by that carried-forward file count gives an order of 21 × ~6s ≈ **~126s** against the 2223s of collection the description quotes. That last step is **derived, not measured** — the file count is carried forward and this round did not re-run a closure walk (see *Not covered*).

## Corrections

**C1 still stands, verbatim, at the new head — all three sites.** `.github/workflows/ci.yml`'s comment above `Check core subpath exports resolve` says a change to the exports map or dist layout "leaves every suite green and **breaks `qwen` at startup**"; `scripts/check-core-subpath-exports.mjs`'s file header says "every suite stays green while **`qwen` dies on its first core subpath import**"; and its failure message says "this **breaks `qwen` at startup** while every in-repo suite stays green". Measured against this round's freshly built tree:

- `grep -roh "@qwen-code/qwen-code-core/" dist/cli.js dist/chunks/` → **0 occurrences**; `grep -rl` over all of `dist/` → **0 files**. `esbuild.config.js` bundles with `packages: 'bundle'` and core is not in its `external` list, so the bundle inlines core.
- `grep -roh … packages/cli/dist/src/` → **269 occurrences**. The unbundled tsc tree keeps the specifiers verbatim, and real CI does execute it: `.github/workflows/serve-ab.yml:294,307` (`node packages/cli/dist/index.js`), `scripts/check-serve-fast-path-bundle.js` (8 explicit `packages/cli/dist/src/**` paths), `scripts/tui-parity/accept-noflicker.sh:15`.

The PR body's own Evidence section — "The shipped CLI is a single bundle and never resolves these specifiers at runtime" — remains the correct statement, and the CI comment still contradicts it. **This is a correction to the description and the comment, not a request to change the code**: the gate is genuinely load-bearing, but for CI lanes and non-bundled runs. A maintainer reading "breaks `qwen` at startup" will misjudge a failure's blast radius. Control: the loader comment this PR *added* in commit `03886a48062a` does **not** repeat the startup claim — so the correction was applied in one new comment and not in the two older ones.

One measurement note on my own first pass: the exports probe initially reported a FAIL on the gate-header row because its regex matched raw file text, where a jsdoc `*` continuation sits between "`qwen` dies" and "on its first core subpath import". Stripping comment markers before matching (`c1-text-check.mjs`) gives **4/4** — three sites present, one control absent. The broken row is excluded from `assertions.json` and the corrected rows are counted; both are listed in `logs/assertions-detail.json` under `excluded`.

## Findings

### 1. The new tsconfig-sync test never asserts that the wildcard exists — and the wildcard is the load-bearing entry — Suggestion

`scripts/tests/integration-tsconfig-core-paths-sync.test.js` (new this round, 77 lines) iterates the **named** keys of core's exports map and requires an explicit `paths` entry for each one whose target stem differs from its key. It never looks at the `@qwen-code/qwen-code-core/*` wildcard — the entry that carries every path-style specifier cli sources actually emit (`config/storage.js`, `utils/debugLogger.js`, …), which is 88 of the 93 distinct subpaths now in use.

Measured (witness `02-mutation-matrix-15-of-18-killed.png`):

| # | mutation | oracle | result |
| --- | --- | --- | --- |
| **M1** | delete `"@qwen-code/qwen-code-core/*": ["../packages/core/src/*"]` from `integration-tests/tsconfig.json` | the new sync test | **SURVIVED** — 0 failed / 1 passed |
| **M2** | repoint it at `../packages/core/src-wrong/*` (a directory that does not exist) | the new sync test | **SURVIVED** — 0 failed / 1 passed |
| **M3** | *positive control, same file*: delete the `noFollowOpen` named entry | the new sync test | **killed** — `has a paths entry for every named exports key whose file it does not mirror` |

M3 is what makes M1/M2 a real coverage gap rather than a dead harness: the same test file, run by the same command, does go red when a *named* entry is removed. The test's own header comment even names the wildcard's role ("Its `@qwen-code/qwen-code-core/*` wildcard substitutes `../packages/core/src/<subpath>`, which covers every specifier whose source file mirrors its name") — it just never asserts it.

That the wildcard is load-bearing is not in question this round; §2 below proves it by compilation. Deleting it puts `integration-tests` back into the state where the PR body reports the gate failing.

Classification: **coverage gap** — the entry is correct today, nothing asserts it stays present or keeps pointing at the source tree.

<details>
<summary>Suggested fix (NOT measured — budget; the shape is the same as the M14 fix below, which was)</summary>

```js
it('keeps the wildcard that carries every path-style specifier', () => {
  const wildcard = paths['@qwen-code/qwen-code-core/*'];
  expect(wildcard, 'the wildcard entry is what resolves config/storage.js and friends').toEqual([
    '../packages/core/src/*',
  ]);
  expect(existsSync(join(root, 'integration-tests', '../packages/core/src')),
    'the wildcard points at a directory that does not exist').toBe(true);
});
```

This kills M1 (entry gone) and M2 (wrong target directory) and is behaviour-preserving on head by construction — but unlike the M14 fix below it was **not** applied and run, so treat it as a sketch.

</details>

Reproduce: `node mutations.mjs` (rows M1–M3).

### 2. The integration tsconfig wildcard is load-bearing, proven by compilation this round — pass, reported because two rounds listed it as unreachable

Both previous rounds put this in *Not covered* as "read, not compiled". Compiling it was cheap; the first attempt was wrong in a way worth recording, and correcting it produced the round's cleanest A/B.

**The naive base arm lies.** `tmp/base-tree` is nested inside the head repo, so when base's `paths` had no wildcard, TypeScript walked *up* out of the base tree and found the head tree's built `packages/core/dist`. Every path-style specifier "resolved" — to `/__w/qwen-code/qwen-code/packages/core/dist/src/**.d.ts`, i.e. head's compiled declarations. `tsc -p integration-tests/tsconfig.json --noEmit` on that arm reported 5 errors and **none of them were core-resolution errors**: 3 were `Cannot find module '@larksuiteoapi/node-sdk'` and `./generated/insightTemplate.js` (a per-package `node_modules` I had not mirrored and gitignored generated files). Presenting that as "base fails, head passes" would have been a fabricated A/B, so it is reported here as what it is: **an uninformative cell, caused by my setup**.

**The corrected arm.** A second base worktree at `/tmp/pr10957-base-isolated` — *outside* the repo, so there is no parent `node_modules` to leak into — with the base exports map (no `./*`) and **no built core dist**, which is the fresh-clone state the PR body describes. Same one-file probe on both arms (witness `03-integration-tsconfig-ab-base-vs-head.png`, `logs/isolated-base-probe.out`):

| arm | `utils/debugLogger.js` | `utils/errors.js` | `noFollowOpen` | head-wide `tsc` errors |
| --- | --- | --- | --- | --- |
| isolated base (no core dist) | **TS2307 Cannot find module** | **TS2307 Cannot find module** | resolves (explicit `paths` entry) | exit 2, 50× TS2307 project-wide (environmental: unbuilt tree) |
| head | **0 errors** | **0 errors** | resolves | **exit 0, 0 errors, 31.8s** |

The reproduced line is byte-for-byte the one the PR body quotes: `error TS2307: Cannot find module '@qwen-code/qwen-code-core/utils/debugLogger.js' or its corresponding type declarations.` Note what it also shows: `noFollowOpen` does **not** error at base, because its explicit `paths` entry already existed — so the wildcard is precisely what closes the path-style gap and nothing else. 10/10 assertions.

**And the reviewer test plan's claim, answered for the tsconfig side.** `integration-tests/tsconfig.json` declares the wildcard at index 7 of 50, with **three** named entries *after* it (`goalWire`, `memoryScopes`, `userPromptSubmitContext`). Driven through `ts.resolveModuleName` with the options parsed from each arm's real config including its `extends` chain (TypeScript 5.8.3), all three still resolve to their own source files — `goals/goal-wire.ts`, `memory/scopes.ts`, `hooks/user-prompt-submit-context.ts` — not to the `src/goalWire.ts`-shaped paths the wildcard would invent. TypeScript's `paths` matching is longest-prefix, not declaration order, so the ordering that *is* load-bearing in the vitest alias array (Finding 3) is not load-bearing here. All 9 named + 5 path-style specifiers resolve to `packages/core/src/**.ts` at head; a bogus subpath is `UNRESOLVED` on both arms. 18/18 assertions.

### 3. The cli vitest alias map is the only ungated copy of the same nine-entry map — and it is the one CI runs — Suggestion

The same nine named core subpaths are mapped to source in three places. This PR added a gate for two of them:

| copy | exercised by | gate |
| --- | --- | --- |
| `integration-tests/tsconfig.json` `paths` | `tsc -p integration-tests` | `integration-tsconfig-core-paths-sync.test.js` (new this round; see Finding 1 for what it misses) |
| skill-review-harness loader `named` map | `npm run test:terminal-bench` — **manual only, never CI** | `text-capture-core-loader-sync.test.js`, **3 tests** |
| **`packages/cli/vitest.config.ts` alias array** | **every cli unit-test run in CI** | **none** |

The config's own comment says so: *"Unlike the skill-review-harness loader's equivalent named map, which `scripts/tests/text-capture-core-loader-sync.test.js` checks against core's exports, this map has no gate and is kept in sync by hand."* The comment is accurate — `scripts/tests/unit-vitest-configs.test.ts` does `import cliConfig from '../../packages/cli/vitest.config.js'`, but asserts only on the unhandled-error exemption, timeouts, the thread cap and coverage; it never touches `resolve.alias`.

Measured, with two oracles per mutation so "no gate" and "no consequence" are separated:

| # | mutation | scripts suite | scratch cli test importing `goalWire` + `noFollowOpen` |
| --- | --- | --- | --- |
| **M17** | delete the `goalWire` exact alias entry | **SURVIVED** — 0 failed / 26 passed | **killed** — fails to resolve |
| **M18** | move the wildcard entry ahead of all nine named entries | **SURVIVED** — 0 failed / 26 passed | **killed** — fails to resolve |
| — | *baseline, unmutated config* | 0 failed / 26 passed | **exit 0** (so M17/M18's red is attributable) |

M18 is exactly the property the reviewer test plan asks a human to judge ("The eight named subpaths must stay ahead of the pattern entry … and the package root must be matched exactly"). It is real — reordering breaks resolution — and nothing machine-checks it. The blast radius is bounded and worth stating plainly: a wrong or missing entry breaks **test collection loudly** (a module fails to resolve), it does not silently load the wrong module the way the harness-loader map could. So this is a maintenance-ergonomics gap, not a correctness hazard.

Classification: **coverage gap**, self-declared by the author in the comment. The asymmetry is the finding: the copy that runs on every CI unit-test invocation is the one with no gate, while the copy that no CI job runs has three tests.

Reproduce: `node mutations.mjs` (rows M17–M18; the scratch probe file is created and deleted by the harness, `git status --porcelain -uno` empty afterwards).

### 4. The tie-break fixture does not pin the clause it is named after — Nice to have

`eslint-rules/no-core-utils-upward-import.js` gained `resolveExportTarget`, which implements Node's exports-pattern ordering. Its tie-break clause — among patterns whose literal prefixes are the same length, prefer the longer full key — has a test named `uses Node pattern ordering when literal prefixes tie`. Deleting the clause (**M14**) leaves all **13** tests green.

Why, measured (`04-m14-tiebreak-survivor-and-measured-fix.png`, 5/5 assertions):

| fixture declaration order | head | M14 (clause removed) |
| --- | --- | --- |
| `'./tools/*'` first, `'./tools/*.js'` second — **the shipped fixture** | `./dist/src/tools/x.js` ✓ | `./dist/src/tools/x.js` ✓ **(mutant invisible)** |
| `'./tools/*.js'` first, `'./tools/*'` second | `./dist/src/tools/x.js` ✓ | **`./dist/src/utils/x.js` ✗ WRONG** |

The clause is real; the fixture just uses the one order in which removing it changes nothing, because the correct winner is reached by the default "replace" path. And with core's actual exports map the clause is **unreachable**: the three pattern keys have literal-prefix lengths `./*`=2, `./dist/*`=7, `./src/*`=6 — **0 tying pairs**. So this is *redundant defence* against a hypothetical future exports map, guarded by a test that cannot see it.

That makes it a Nice-to-have rather than a defect — and it is worth noting the author fixed this exact defect class one fixture over: commit `fedf05311600` is titled *"make the exact-key-precedence fixture verdict-sensitive"* and its comment spells out the reasoning ("a fixture like `goalWire` … would stay green on the mutant"). The tie-break fixture has the same problem that commit set out to remove.

<details>
<summary>Suggested fix — applied in a scratch copy and measured (6/6 assertions)</summary>

Add the reversed declaration order as a second case in the same `it`:

```js
    // Declaration order must not decide the winner: with the longer pattern
    // first, only the full-key tie-break keeps the right target.
    expect(
      resolveExportTarget('./tools/x.js', {
        './tools/*.js': './dist/src/tools/*.js',
        './tools/*': './dist/src/utils/*',
      }),
    ).toBe('./dist/src/tools/x.js');
```

| cell | result |
| --- | --- |
| shipped fixture + head rule | exit 0, 0 failed / 13 passed |
| shipped fixture + M14 rule | exit 0, 0 failed / 13 passed — **the survivor** |
| **patched** fixture + head rule | exit 0, **0 failed / 13 passed** — behaviour-preserving |
| **patched** fixture + M14 rule | exit 1, **1 failed / 12 passed** — `× uses Node pattern ordering when literal prefixes tie` |

The killed assertion is the tie-break test itself, not a bystander; no other test changes outcome; both files restored byte-identically (`git status --porcelain -uno` empty). This is the unpinned-axis signal: the suite could not tell head from head-minus-clause, and now can.

</details>

Reproduce: `node m14-probe.mjs` then `node m14-fix-probe.mjs`.

### 5. The two changed eslint rules add no false positives anywhere in the tree — pass, closes a previous *Not covered*

Last round: *"The changed eslint rules were not run across the tree; only the gate and its fixtures were exercised. A new false positive elsewhere would not show up here."* Both changed rules scope themselves to `packages/core/src/` (`isCoreProductionFile` / `isUtilsProductionFile` anchor on that marker), and setup asserted that directory is **byte-identical across arms** — so this is a clean A/B of the rule and the exports map it reads, over the same 1348 files (witness `05-eslint-rules-zero-false-positives.png`, 11/11 assertions):

- **0 reports** from either rule at head, **0** at base. Real repo eslint over the same tree: **0 messages of any kind across 1350 files** — the validity control that makes the census believable.
- Both changes are load-bearing. A planted `import { Storage } from '@qwen-code/qwen-code-core/config/storage.js'` inside `packages/core/src/utils/` is reported by the **head** rule and **not** by the base rule — at base that specifier was unresolvable through the exports map, so the rule could not verdict it. A planted `import … from '@qwen-code/qwen-code-core/index.js'` in `packages/core/src/core/` is reported by head and not base: the new `./*` catch-all makes `<pkg>/index.js` a new way to name the barrel, and the specifier the PR added to `CORE_BARREL_SPECIFIERS` is what catches it (**M16** confirms the test pins it: deleting that one line turns `no-core-root-barrel-import.test.js` red).
- 0 parse errors on either arm.

One harness correction, recorded because it is the kind that silently fabricates a finding: my first census asserted on **all** messages the `Linter` returned and reported **333** on each arm. None came from the rules under test — 260 were `Definition for rule '@typescript-eslint/no-explicit-any' was not found`, 56 `Unused eslint-disable directive`, 14 for `vitest/valid-expect`: all artifacts of running a minimal flat config over files carrying `eslint-disable` comments for rules it does not define. The count was identical on both arms and the real-eslint control said 0, which is what caught it. `eslint-reassert.mjs` filters to the two rules and re-asserts from the saved raw reports; the original 10 rows are excluded from `assertions.json` and listed in `excluded`.

## Mutation matrix

Witness `02-mutation-matrix-15-of-18-killed.png`; raw `logs/mutations.out`, per-oracle logs `logs/mutation-M*.log`. Unmutated baselines ran first and are green — gate exit 0 with `All 96 core subpath specifiers resolve`, gate test 6/6, loader sync 3/3, tsconfig sync 1/1, eslint rule 13/13, dev test 6/6, alias probe exit 0 — so every kill is attributable to its mutation. `git status --porcelain -uno` is empty after all restores; the base worktree, the isolated base worktree and every scratch file were removed.

| # | guard | mutation | oracle | result |
| --- | --- | --- | --- | --- |
| M1 | integration tsconfig carries the core wildcard **at all** | delete the `*` entry | tsconfig sync test | **SURVIVED** — 0f/1p → Finding 1 |
| M2 | …and it points at the real source tree | repoint at `src-wrong/*` | tsconfig sync test | **SURVIVED** — 0f/1p → Finding 1 |
| M3 | *positive control, same file* | delete the `noFollowOpen` named entry | tsconfig sync test | **killed** |
| M4 | loader map points each key at the **right** module *(last round's M13 survivor)* | `goalWire` → `utils/transcript-records.ts` (exists, wrong) | loader sync test | **killed** — 1f/2p, `points every entry at the exports target in source space` |
| M5 | *positive control, same file* | `goalWire` → nonexistent file | loader sync test | **killed** |
| M6 | loader map covers every exports key | delete the `conversationsRuntimeMarker` row | loader sync test | **killed** |
| M7 | reverse key-set (new this round) | add a stale loader-only entry | loader sync test | **killed** — 2f/1p |
| M8 | gate scans `acp-bridge` + `sdk-typescript` | drop both dep dirs | gate test | **killed** — 2f/4p |
| M9 | gate rejects targets outside published `dist/` | disable the containment branch | gate test | **killed** — 1f/5p |
| M10 | gate allows the published `./package.json` | remove `&& resolved !== corePackageJson` | gate test | **killed** — 1f/5p |
| M11 | gate imports a probe target and checks the export | disable the export-name check | gate test | **killed** — 1f/5p |
| M12 | core exports `./*` catch-all *(last round's M1)* | delete `"./*": "./dist/src/*"` | **the gate itself** | **killed** — exit 1, **88 of 96** specifiers fail |
| M13 | exact export keys stay ahead of the wildcard | delete the exact-key branch | eslint rule test | **killed** — 2f/11p |
| M14 | pattern tie-break prefers the longer full key | replace the clause with `false` | eslint rule test | **SURVIVED** — 0f/13p → Finding 4, fix measured |
| M15 | *positive control, same file* | disable pattern matching entirely | eslint rule test | **killed** — 2f/11p, incl. the tie-break test |
| M16 | barrel rule knows the new `<pkg>/index.js` spelling | delete that one specifier | barrel rule test | **killed** — 1f/15p |
| M17 | cli alias map: `goalWire` entry ahead of the wildcard | delete the entry | ① `unit-vitest-configs.test.ts` ② scratch cli test | ① **SURVIVED** 0f/26p ② **killed** → Finding 3 |
| M18 | cli alias map: named entries precede the pattern *(reviewer test plan claim 1)* | move the wildcard first | ① same ② same | ① **SURVIVED** 0f/26p ② **killed** → Finding 3 |
| M19 | the CI lane payload pins the new step *(head commit `cdabd7dad32b`)* | delete the step from `ci.yml` | `ci-platform-lanes.test.js` | **killed** — 1f/35p |
| M20 | *positive control, same file* | delete an older pinned step | `ci-platform-lanes.test.js` | **killed** — 1f/35p |

M17/M18's survivors are **predicted** survivors — the expectation encoded in the harness is "the gate is blind", so they count as passes and the finding is the blindness itself. M1/M2/M14 were predicted kills and are the **3 fails** in `assertions.json`. M19/M20 confirm the head commit's one-line lane pin is load-bearing.

## Not covered

- **The whole cli unit suite on either arm.** ~84 minutes per arm does not fit the budget, so the PR's headline "green across the unit suite" is still not independently reproduced. What ran instead: 32 vitest invocations (16 cells × 2 arms), the gate's own test files, the scripts-suite files named above, and one scratch probe. The remaining ~995 cli test files are unmeasured.
- **Repo-wide `npm run lint` and `npm run typecheck`.** Narrowed rather than skipped: real eslint ran over `packages/core/src` (1350 files, 0 messages) — the only tree the two changed rules can report on — and real `tsc -p integration-tests/tsconfig.json --noEmit` ran on both arms. `packages/cli`'s own typecheck was **not** run, so a type error introduced by the 109-file import migration would not show up here.
- **The suite-level closure census.** This round's census is exact for *production files* but says nothing about how many *test files* stop reaching the barrel; that needs the transitive walk, which was not re-run. The −21/1011 figure quoted in *Closure census* is carried forward from last round's partially calibrated model (30/32 cell agreement) and is labelled an estimate there.
- **Per-commit attribution.** `git rev-parse --is-shallow-repository` → `true`; `git rev-list HEAD^1..HEAD^2` returns **1** while `$QWEN_VERIFY_CONTEXT` lists **29** commits. Only the aggregate `HEAD^1..HEAD` diff was verified. The eight delta commits are identified from the snapshot's commit list, not from reachable objects; the previous round's head `47128e7007c6` is absent locally, so no round-to-round diff was possible.
- **Last round's M3–M6 and §2.** The mock move onto `utils/gitUtils.js`, `dev.js`'s subpath→source redirect, and the loader's end-to-end harness run were not re-mutated. `scripts/dev.js` *is* now pinned by a real loader test (`dev.test.js` evaluates the emitted loader source and asserts both the redirect and the named-subpath fall-through; baseline 6/6 green), but I did not mutate it to prove that test non-vacuous.
- **The two mock moves** the reviewer test plan asks a human to confirm ("what confirms them is that the test still fails when the stubbed behaviour is removed"). Not performed — that requires reverting stubbed behaviour in two suites, which is a third round's worth of budget on its own.
- **The PR's own CI numbers** (2223s collecting, 1372s running, 84 minutes, the 16-file/127-test first full run, the 146 restored modules). Lane history this container cannot see. The `collect` figures in the A/B table are an independent measurement of the same mechanism at per-file granularity, not a check of those totals.
- **Runtime side-effect risk** — the description's named main risk. Not probed: no flipped cell changed its test tally or behaviour, and the shipped bundle evaluates the same modules, but no systematic audit of the 579 barrel modules for order-dependent initialization was attempted.
- **The bundle's size.** The description says it was not measured and should be unchanged. Not measured here either.
- **The `ci.yml` step's `if:` gate** (`ci_profile == 'full'`) was read, not evaluated; the step's `run:` block was not replayed this round (last round replayed it with `REPLAY_EXIT=0`, uncalibrated). What was measured instead is that the lane-payload test pins the step's presence (M19/M20).
- **Suggested fix for Findings 1 and 3.** Finding 4's fix was applied and measured; Finding 1's is a sketch only, and Finding 3 has no proposed fix — gating the cli alias map would mean either a new sync test or deriving the array from core's exports map, which is a design choice for the author.

## Methodology

Environment: the CI verify container, `node:22-bookworm`, Node v22.23.2, TypeScript 5.8.3, working tree at `refs/pull/10957/merge` (depth 2), `npm ci` + `npm run build` already complete. Two control arms were built: `tmp/base-tree` (nested, for the 32 vitest cells — wired by `setup-base.mjs` with 1134 entry-by-entry symlinks, all 24 `@qwen-code/*` links re-rooted at the base tree, no shared vite cache, 12 `dist/` dirs linked from packages the PR touches 0 files in, and 26 contamination/validity assertions) and `/tmp/pr10957-base-isolated` (**outside** the repo, built by `isolated-base-probe.mjs` after the nested tree was shown to leak head's `packages/core/dist` into base-side TypeScript resolution). Both were removed with `git worktree remove --force`; `git worktree list` shows only the main checkout and `git status --porcelain` is clean apart from the artifact directory.

Harnesses are in this directory and rerunnable: `setup-base.mjs` (base arm + 26 assertions), `ab-driver.mjs` (the 32 vitest cells through a counting vite plugin, with cross-tree contamination detection), `ab-assert.mjs` (recomputes the verdict from the raw per-cell logs after ANSI stripping — the driver's tally regex matched vitest's colourised output and produced a vacuous `0f/0p`, so every tally in the table comes from this pass), `tsconfig-probe.mjs` (`ts.resolveModuleName` over each arm's real parsed config, plus `tsc --noEmit` on both arms), `isolated-base-probe.mjs` (the corrected base arm and the one-file compile A/B), `exports-probe-template.mjs` (Node's real resolver over the exports map, written into each arm root so resolution walks that arm's own tree), `c1-text-check.mjs`, `census-exact.mjs` + `census-partition.mjs` (TypeScript-parser census, no graph model), `eslint-census.mjs` + `eslint-reassert.mjs` (rule A/B over an identical corpus with two validity controls), `m14-probe.mjs` + `m14-fix-probe.mjs`, `m19-probe.mjs`, `mutations.mjs` (the matrix, two oracles where a survivor needs its consequence separated from its coverage), and `aggregate.mjs` (builds `assert

...truncated -- full content in the run artifacts.
Flakiness gate log

rounds=5 files=10 skipped=0
file packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/StandaloneSessionPicker.test.tsx
file packages/cli/src/ui/hooks/useGitBranchName.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useGitBranchName.test.ts
file packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useMemoryMonitor.test.ts
file scripts/tests/check-core-subpath-exports.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/check-core-subpath-exports.test.js
file scripts/tests/ci-platform-lanes.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/ci-platform-lanes.test.js
file scripts/tests/dev.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/dev.test.js
file scripts/tests/integration-tsconfig-core-paths-sync.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/integration-tsconfig-core-paths-sync.test.js
file scripts/tests/no-core-root-barrel-import.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/no-core-root-barrel-import.test.js
file scripts/tests/no-core-utils-upward-import.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/no-core-utils-upward-import.test.js
file scripts/tests/text-capture-core-loader-sync.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/text-capture-core-loader-sync.test.js


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: PPPPP
  packages/cli/src/ui/hooks/useGitBranchName.test.ts: PPPPP
  packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: PPPPP
  scripts/tests/check-core-subpath-exports.test.js: PPPPP
  scripts/tests/ci-platform-lanes.test.js: PPPPP
  scripts/tests/dev.test.js: PPPPP
  scripts/tests/integration-tsconfig-core-paths-sync.test.js: PPPPP
  scripts/tests/no-core-root-barrel-import.test.js: PPPPP
  scripts/tests/no-core-utils-upward-import.test.js: PPPPP
  scripts/tests/text-capture-core-loader-sync.test.js: PPPPP

verdict: pass
summary: 10 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 1 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 1 · scripts/tests/ci-platform-lanes.test.js: P (exit 0)
round 1 · scripts/tests/dev.test.js: P (exit 0)
round 1 · scripts/tests/integration-tsconfig-core-paths-sync.test.js: P (exit 0)
round 1 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 1 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 1 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 2 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 2 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 2 · scripts/tests/ci-platform-lanes.test.js: P (exit 0)
round 2 · scripts/tests/dev.test.js: P (exit 0)
round 2 · scripts/tests/integration-tsconfig-core-paths-sync.test.js: P (exit 0)
round 2 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 2 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 2 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 3 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 3 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 3 · scripts/tests/ci-platform-lanes.test.js: P (exit 0)
round 3 · scripts/tests/dev.test.js: P (exit 0)
round 3 · scripts/tests/integration-tsconfig-core-paths-sync.test.js: P (exit 0)
round 3 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 3 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 3 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 4 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 4 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 4 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 4 · scripts/tests/ci-platform-lanes.test.js: P (exit 0)
round 4 · scripts/tests/dev.test.js: P (exit 0)
round 4 · scripts/tests/integration-tsconfig-core-paths-sync.test.js: P (exit 0)
round 4 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 4 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 4 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)
round 5 · packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx: P (exit 0)
round 5 · packages/cli/src/ui/hooks/useGitBranchName.test.ts: P (exit 0)
round 5 · packages/cli/src/ui/hooks/useMemoryMonitor.test.ts: P (exit 0)
round 5 · scripts/tests/check-core-subpath-exports.test.js: P (exit 0)
round 5 · scripts/tests/ci-platform-lanes.test.js: P (exit 0)
round 5 · scripts/tests/dev.test.js: P (exit 0)
round 5 · scripts/tests/integration-tsconfig-core-paths-sync.test.js: P (exit 0)
round 5 · scripts/tests/no-core-root-barrel-import.test.js: P (exit 0)
round 5 · scripts/tests/no-core-utils-upward-import.test.js: P (exit 0)
round 5 · scripts/tests/text-capture-core-loader-sync.test.js: P (exit 0)

Evidence images

01-ab-cells-base-vs-head

02-mutation-matrix-15-of-18-killed

03-integration-tsconfig-ab-base-vs-head

04-m14-tiebreak-survivor-and-measured-fix

05-eslint-rules-zero-false-positives

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @chiga0 — needs a human call on this one.

Triage ran all three stages on cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 and landed at Confidence: 3/5. No correctness defect, no security issue, no regression — I am neither approving nor requesting changes. The full reasoning is in the Stage 3 comment above; the short version is that this PR's engineering is clean but its own author measured the perf win at under 2% and then concluded the batching unit was wrong, which makes it a cost-benefit and public-contract judgement rather than a gate decision.

Three things I could not resolve from the diff, the tests, or the description:

  1. Whether the ./* catch-all on packages/core/package.json is an acceptable permanent widening of the published contract. It exposes no file that ./dist/* did not already reach — I verified that — but it does make every internal core module path spell like a supported subpath, so a future packages/core/src/ reorganisation becomes a breaking change for whoever found the clean spelling.
  2. Whether the 109-module sweep should land here or be re-cut into graph-complete batches, per the re-sequencing the author described, keeping the mechanism in this PR.
  3. Whether the ungated packages/cli/vitest.config.ts alias map needs a gate before merge or can be a follow-up. It fails loudly rather than silently, so it is not a blocker — but three sibling maps got gates and the two that did not are the ones every cli test run touches.

Assigned to @chiga0 so this lands in your filter. You reviewed at ccbc6ac6 and found no blocking issues; the head has moved since, and CI on the current head is green on Linux with macOS, Windows and the CLI integration suite all skipped.

中文说明

⏸️ 上交 @chiga0 —— 这件事需要人来定。

Triage 在 cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 上跑完了全部三个阶段,结论为 Confidence: 3/5。没有正确性缺陷、没有安全问题、没有回归——我既不批准也不 request changes。完整推理见上方 Stage 3 评论;简而言之,本 PR 的工程实现是干净的,但它的作者自己测出的性能收益不到 2%,随后又判定分批单位选错了,这使它成为一个成本收益与公共契约的判断,而不是 gate 能做的决定。

三点我无法从 diff、测试或描述中解决:

  1. packages/core/package.json 上的 ./* catch-all,作为对已发布契约的永久扩大是否可接受。它没有暴露任何 ./dist/* 原本触达不到的文件——这一点我核实过——但它确实让 core 每个内部模块路径都长得像受支持的子路径,于是将来对 packages/core/src/ 的重新组织,对发现这个干净写法的人来说就变成破坏性变更。
  2. 那 109 个模块的迁移应该留在这里,还是按作者描述的重新编排、切成「整张图干净」的批次,而机制部分留在本 PR。
  3. 没有门禁的 packages/cli/vitest.config.ts alias 映射,是合并前必须补门禁,还是可以作为后续项。它的失败是响的而不是静默的,所以不是阻塞项——但三张同级映射拿到了门禁,而没拿到的两张恰恰是每次 cli 测试运行都要经过的。

已指派给 @chiga0,让它进入你的过滤器。你在 ccbc6ac6 上审查过并且没有发现阻塞问题;此后 head 已经变动,当前 head 的 CI 在 Linux 上为绿,macOS、Windows 与 CLI 集成套件均为 skipped。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at cdabd7dad32ba0ee7fe4219da1e2b86aafa67069 · re-run with @qwen-code /triage

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed at head e82b6716 (delta since the round-9 zero-findings sha cdabd7dad3 is two mechanical main-merges; no PR-specific commits). Every historical Critical is verified resolved at this head, and my Critical-only pass — focused on the import-mechanism impact across the cli / daemon / standalone surfaces — finds no blocking defect.

Historical blockers — status at this head:

  • R8-1 (the last standing Critical, round 8): the new Check core subpath exports resolve step in ci.yml is now registered in the pinned lint_and_static step list — verified: cdabd7dad3 adds the exact step name to scripts/tests/ci-platform-lanes.test.js:474, and round 9 re-reviewed at that sha with zero findings. The merge delta afterwards touches no PR-owned logic.
  • Rounds 1–2 Criticals (half-migrated mock pairs, dev.js mode): resolved and confirmed closed by the intervening approvals/rounds; the current suite carries the mock-wiring blocks and the dev-loader sync tests.

Impact assessment (cli / daemon / standalone), verified at this head:

  • Runtime bundle: the strongest hazard of subpath imports — a specifier that resolves in source/vitest but not against the published dist layout, leaving every suite green while qwen breaks at startup — is gated by the new scripts/check-core-subpath-exports.mjs CI step, which builds packages/core and validates every core subpath used by the CLI against the built package. The gate and its 245-line test suite are in this diff.
  • Daemon / serve / standalone-session: these live inside packages/cli and receive the same mechanical rewrite; their behavior lanes (Serve A/B, Real daemon E2E, Integration Tests (no-AK)) are running at this head with no failures recorded so far.
  • Dev mode and integration typecheck: scripts/dev.js's subpath→source loader map and integration-tests/tsconfig.json paths are each pinned to core's exports map by dedicated sync tests (text-capture-core-loader-sync.test.js, integration-tsconfig-core-paths-sync.test.js, dev.test.js), so the three surfaces cannot drift silently.
  • Specifier hygiene: the strengthened eslint rules (no-core-root-barrel-import, no-core-utils-upward-import) keep root-barrel and subpath spellings from mixing, which is what protects module-instance identity in the bundle.

CI at this head: no failing or cancelled checks at review time; Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK), Serve A/B, Real daemon E2E / Java 11 and the Java matrix legs are still pending, which does not gate this review per policy.

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.

6 participants