Skip to content

perf(cli): replace comment-json settings parser - #7747

Merged
doudouOUC merged 1 commit into
QwenLM:mainfrom
doudouOUC:perf/replace-comment-json-parser
Jul 26, 2026
Merged

perf(cli): replace comment-json settings parser#7747
doudouOUC merged 1 commit into
QwenLM:mainfrom
doudouOUC:perf/replace-comment-json-parser

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

What this PR does

This PR replaces the synchronous comment-json-based settings and trusted-folders writer with path-based jsonc-parser edits. It preserves comments, indentation, line endings, final-newline state, UTF-8 BOMs, merge and sync behavior, exact-subtree replacement, trusted-folder locking, validation, and atomic-write guarantees while removing the legacy parser cluster from the ACP eager startup closure.

It also makes the bundle resolve the public jsonc-parser import to its ESM build, while leaving unbundled compiled output on the public Node-compatible package entry, and extends the fast-path bundle guard to reject comment-json, esprima, or the incompatible jsonc-parser UMD build from the ACP static closure.

Why it's needed

ACP imports settings and trusted-folders writers before it can answer initialize, although ordinary startup does not write either file. Removing the legacy JSONC parser cluster reduces the exact ACP static closure from 12,449,869 to 12,145,099 bytes, a reduction of 304,770 bytes (2.45%).

On the established 2-vCPU Linux host, 30 alternating paired cold starts improved channel.initialize P50 by 35.39 ms, process-to-first-session P50 by 38.00 ms, and process-to-first-session-complete P50 by 48.51 ms, with 28 of 30 paired wins for each primary metric and paired-mean 95% bootstrap intervals entirely below zero. Preheated process-to-session-complete behavior was statistically neutral.

Reviewer Test Plan

How to verify

Update existing settings and trusted-folders JSONC containing leading, nested, inline, and trailing comments, then confirm only the requested values change and the original indentation, line endings, final newline, BOM, unrelated keys, and comments remain intact. Exercise merge, sync, and exact-subtree replacement and confirm malformed or non-object JSONC is rejected without overwriting the file. Confirm trusted-folders writes still re-read under lock, validate both disk and proposed state, preserve concurrent disk rules, write atomically with mode 0600, and release the lock after failures.

Run the focused CLI tests, CLI typecheck, lint, clean CLI build and bundle, and the fast-path bundle guard. The generated ACP metafile closure should contain no comment-json, esprima, or jsonc-parser/lib/umd input, and both the unbundled compiled editor and bundled CLI should run under Node.js 22.

Evidence (Before & After)

N/A — this is a non-UI startup and configuration-writing change.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

Environment (optional)

macOS with Node.js 22.22.3 for focused tests, typecheck, lint, clean build, bundle, unbundled runtime smoke, and bundle guard. Linux 6.6.102 with Node.js 22.23.1 on a 2-vCPU, approximately 3.5-GiB, no-swap host for 30 paired cold starts, 30 paired preheated starts, compatibility scenarios, and a separate 10-pair steady-memory follow-up.

Risk & Scope

  • Main risk or tradeoff: JSONC path edits must preserve existing user-authored comments and formatting across unusual files. Focused coverage includes malformed roots, trailing commas, duplicate keys, CRLF, tabs, final newline, BOM, prototype-named keys, deleted inline comments, and write failures. Peak process-tree RSS was about 10.8 MiB higher during initialization, but after a 10-second idle period the paired steady-state median delta was 0.55 MiB with a bootstrap interval spanning zero, indicating transient initialization and garbage-collection timing rather than a persistent footprint increase.
  • Not validated / out of scope: Windows runtime behavior was not manually tested; CI remains the cross-platform check. Settings migrations, trusted-folder semantics, and the strict trusted-folders read path are unchanged.
  • Breaking changes / migration notes: None.

Linked Issues

#7264

中文说明

此 PR 的改动

此 PR 使用基于路径的 jsonc-parser 编辑替换同步的 comment-json settings 与 trusted-folders 写入实现。在移除 ACP 启动静态闭包中的旧解析器依赖簇的同时,保留注释、缩进、换行符、文件末尾换行状态、UTF-8 BOM、merge 与 sync 行为、精确子树替换、trusted-folders 锁、校验和原子写入保证。

同时,bundle 构建会把公开的 jsonc-parser import 解析到其 ESM 构建,而未 bundle 的编译产物仍使用 Node 可直接运行的公开包入口;fast-path bundle guard 也扩展为禁止 comment-jsonesprima 或不兼容的 jsonc-parser UMD 构建进入 ACP 静态闭包。

为什么需要

ACP 必须在回答 initialize 前导入 settings 与 trusted-folders 写入实现,尽管普通启动过程不会写入这两个文件。移除旧 JSONC 解析器依赖簇后,ACP 精确静态闭包从 12,449,869 字节降至 12,145,099 字节,减少 304,770 字节(2.45%)。

在既有 2-vCPU Linux 主机上,30 组交替配对冷启动中,channel.initialize P50 改善 35.39 ms,进程到首次 session 响应 P50 改善 38.00 ms,进程到首次 session 完成 P50 改善 48.51 ms;三个主要指标均为 30 组中 28 组胜出,配对均值的 95% bootstrap 区间全部低于零。预热后的进程到 session 完成行为在统计上持平。

Reviewer 测试计划

如何验证

更新包含前置、嵌套、行内和尾部注释的现有 settings 与 trusted-folders JSONC,确认只有指定值发生变化,原有缩进、换行符、文件末尾换行、BOM、无关字段和注释均保持不变。覆盖 merge、sync 和精确子树替换,并确认格式错误或根节点不是对象的 JSONC 会被拒绝且不会覆盖文件。确认 trusted-folders 写入仍会在持锁后重新读取磁盘、校验磁盘状态与拟写入状态、保留并发写入的磁盘规则、以 0600 权限原子写入,并在失败后释放锁。

运行定向 CLI 测试、CLI typecheck、lint、干净的 CLI build 与 bundle,以及 fast-path bundle guard。生成的 ACP metafile 闭包不应包含任何 comment-jsonesprimajsonc-parser/lib/umd 输入,并且未 bundle 的编译编辑器与 bundled CLI 均应能在 Node.js 22 下运行。

证据(Before & After)

N/A — 这是非 UI 的启动和配置写入改动。

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux

环境(可选)

macOS、Node.js 22.22.3:定向测试、typecheck、lint、干净 build、bundle、未 bundle 运行时 smoke 和 bundle guard。Linux 6.6.102、Node.js 22.23.1、2-vCPU、约 3.5 GiB、无 swap:30 组配对冷启动、30 组配对预热启动、兼容性场景,以及单独的 10 组配对稳态内存复测。

风险与范围

  • 主要风险或权衡:JSONC 路径编辑必须在非常规文件中保持用户已有的注释与格式。定向覆盖包括格式错误的根节点、尾逗号、重复键、CRLF、tab、文件末尾换行、BOM、原型同名键、被删除字段的行内注释和写入失败。初始化期间进程树峰值 RSS 约增加 10.8 MiB,但等待 10 秒后配对稳态中位数差值为 0.55 MiB,且 bootstrap 区间跨零,说明这是初始化和垃圾回收时序造成的瞬时差异,而不是持续占用增长。
  • 未验证 / 范围外:未手工验证 Windows 运行时行为;跨平台检查由 CI 完成。Settings migrations、trusted-folder 语义和严格的 trusted-folders 读取路径均未改变。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

#7264

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@doudouOUC

Copy link
Copy Markdown
Collaborator Author

E2E and performance report

Static closure

  • Control: 12,449,869 bytes
  • Candidate: 12,145,099 bytes
  • Delta: -304,770 bytes (-2.45%)
  • Candidate inputs: comment-json 0, esprima 0, jsonc-parser/lib/umd 0

2-vCPU Linux benchmark

The control and candidate were built from the same baseline with identical runtime assets. The host had 2 Intel Xeon Platinum vCPUs, approximately 3.5 GiB RAM, no swap, Linux 6.6.102, and Node.js 22.23.1. One warmup preceded 30 alternating paired cold starts and 30 alternating paired starts after a 3-second preheat.

Metric Control P50 Candidate P50 P50 delta Paired result
Cold channel.initialize 892.45 ms 857.06 ms -35.39 ms 28/30 wins; mean -32.97 ms, 95% bootstrap CI [-40.91, -24.85]
Cold process to first session response 1251.03 ms 1213.02 ms -38.00 ms 28/30 wins; mean -40.76 ms, 95% bootstrap CI [-51.93, -30.90]
Cold process to first session complete 1854.11 ms 1805.60 ms -48.51 ms 28/30 wins; mean -48.73 ms, 95% bootstrap CI [-60.78, -37.87]
Preheated channel.initialize 846.30 ms 814.46 ms -31.84 ms 28/30 wins; mean -29.82 ms, 95% bootstrap CI [-35.04, -23.99]
Preheated process to session complete Mean -0.97 ms, 95% bootstrap CI [-6.34, 4.04]

Peak process-tree RSS was about 10.8 MiB higher during initialization. A separate 10-pair follow-up sampled after a 10-second idle period; the paired steady-state median delta was +0.55 MiB with a 95% bootstrap interval spanning zero. Per-process inspection showed no additional process or persistent footprint increase.

Concurrent first sessions, telemetry-disabled mode, outfile telemetry mode, and legacy single-session mode all succeeded. Enabled profiles validated, telemetry-disabled mode emitted no telemetry records, cleanup found no residual PID, and the daemon port was unreachable after shutdown.

Local verification

  • 223 focused editor, settings, and trusted-folders tests passed.
  • 31 fast-path bundle guard tests passed.
  • CLI typecheck and changed-file ESLint passed.
  • Clean CLI build and bundle passed.
  • Unbundled compiled JSONC runtime smoke and bundled CLI smoke passed on Node.js 22.
  • The generated ACP closure contains no forbidden parser input.

The full workspace typecheck still has the pre-existing untouched Web Shell errors at client/App.tsx:1554 and client/App.tsx:1558 because DaemonConnectionState.gitStatus is absent; the CLI workspace typecheck passes.

@doudouOUC
doudouOUC marked this pull request as ready for review July 26, 2026 12:56
@doudouOUC
doudouOUC enabled auto-merge July 26, 2026 12:56
@doudouOUC
doudouOUC requested a review from wenshao July 26, 2026 12:56
@doudouOUC doudouOUC self-assigned this Jul 26, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: observed and well-measured. Linked to #7264 (ACP eager-closure audit), with 30 alternating paired cold starts showing 35–49 ms P50 improvements on channel.initialize and process-to-first-session metrics, 28/30 paired wins, and bootstrap CIs entirely below zero. This is a concrete, reproduced performance bottleneck, not a theoretical concern.

Direction: aligned — removing the comment-json/esprima cluster from the ACP static closure is candidate 6 in the tracking issue, and the 305 KB (2.45%) closure reduction directly targets cold-start latency. CHANGELOG: no direct reference, but the area (ACP startup performance) is actively tracked.

Size: touches core paths (packages/cli/src/config/settings.ts, packages/cli/src/config/trustedFolders.ts). Production logic: ~616 lines (additions + deletions, excluding tests). Test lines: ~229. Design doc: 79 lines. Flagging for maintainer awareness per the 500+ production-line threshold — the core-path changes themselves are small (import swaps + a 29-line trustedFolders adaptation); the bulk is the new jsonc-editor.ts utility in packages/cli/src/utils/.

Approach: the scope feels right. jsonc-parser is already in the dev dependency graph and has a much smaller footprint than comment-json + esprima. The new editor handles BOM, CRLF, duplicate keys, inline-comment cleanup, and output validation — all the edge cases that comment-json's stringify used to cover implicitly. The applyUpdates logic carries over with minimal changes. Bundle guard extension prevents regression. Design doc is thorough. No drive-by changes or scope creep.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:已观测且充分量化。关联 #7264(ACP 静态闭包审计),30 组交替配对冷启动显示 channel.initialize 及进程到首次 session 指标 P50 改善 35–49 ms,30 组中 28 组胜出,bootstrap 置信区间完全低于零。这是具体的、可复现的性能瓶颈,而非理论性顾虑。

方向:对齐——移除 ACP 静态闭包中的 comment-json/esprima 依赖簇是跟踪 issue 中的候选 6,305 KB(2.45%)的闭包缩减直接针对冷启动延迟。

规模:触及核心路径(packages/cli/src/config/settings.tspackages/cli/src/config/trustedFolders.ts)。生产逻辑约 616 行(增删合计,不含测试)。测试约 229 行。设计文档 79 行。按 500+ 生产行阈值提请维护者关注——核心路径改动本身很小(import 替换 + trustedFolders 29 行适配),主体是 packages/cli/src/utils/ 下的新 jsonc-editor.ts 工具模块。

方案:范围合理。jsonc-parser 已在开发依赖图中, footprint 远小于 comment-json + esprima。新编辑器处理了 BOM、CRLF、重复键、行内注释清理和输出校验——这些是 comment-json 的 stringify 之前隐式覆盖的边界情况。applyUpdates 逻辑以最小改动延续。Bundle guard 扩展防止回归。设计文档详尽。无顺手改动或范围蔓延。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review

Independent proposal: I would have replaced comment-json with jsonc-parser (already in the dev dependency graph), wrapped its parseTree/modify/applyEdits APIs in a utility that preserves the existing updateSettingsFilePreservingFormat contract, aliased the esbuild entry to the ESM build, and extended the bundle guard. The PR does exactly this.

Comparison with the diff: the implementation goes further than my baseline in good ways — duplicate-key normalization (renaming earlier occurrences before removing them, working around jsonc-parser's first-match modify() behavior), inline-comment removal on property deletion (preventing orphaned comments attaching to the preceding property), and a re-parse-and-deep-compare validation step before any write. These are real edge cases that comment-json's round-trip stringify used to handle implicitly.

No critical blockers found. A few observations:

  • The applyUpdates sync-mode filter changed from !(key in updates) to !Object.hasOwn(updates, key) — this is a correctness improvement. The old in operator matched inherited Object.prototype properties (toString, constructor), so sync mode would incorrectly delete own keys with those names. The new test covers this.
  • parseJsoncObject uses JSON.parse(JSON.stringify(getNodeValue(root))) rather than structuredClone — deliberate, since it strips undefined and normalizes special numbers, and the test confirms __proto__ becomes an own property without polluting Object.prototype.
  • updateSettingsFilePreservingFormat parses the content twice (once for validation, once inside updateJsoncContent). Minor redundancy, but settings writes are infrequent and the separation gives cleaner error reporting. Not worth changing.
  • The detectFormattingOptions heuristic reads indentation from the first property's leading whitespace, skipping quoted-comment false positives (tested). Reasonable for real-world settings files.

Conventions: kebab-case filename ✓, collocated tests ✓, ESM imports ✓, no any ✓, legacy-filenames allowlist updated ✓, design doc in docs/design/ per AGENTS.md ✓.

Files changed (14 of 14 shown)
File What changed
docs/design/lightweight-jsonc-settings-editor.md New design doc covering context, goals, alternatives, and validation methodology
esbuild.config.js Alias jsonc-parser to its ESM build so the split bundle does not pull the UMD entry
eslint.legacy-filenames.mjs Remove commentJson from the camelCase allowlist (file deleted)
package-lock.json Drop comment-json and its transitive deps, promote jsonc-parser from dev to prod
packages/cli/package.json Swap comment-json dependency for jsonc-parser
packages/cli/src/config/settings.test.ts Update mocks from commentJson to jsonc-editor module
packages/cli/src/config/settings.ts One-line import swap to the new editor
packages/cli/src/config/trustedFolders.test.ts Rewrite the invalid-output test to mock updateJsoncContent instead of comment-json parse
packages/cli/src/config/trustedFolders.ts Replace comment-json parse/stringify with parseJsoncObject and updateJsoncContent
packages/cli/src/utils/commentJson.ts Deleted — replaced by jsonc-editor.ts
packages/cli/src/utils/jsonc-editor.test.ts Renamed from commentJson.test.ts, adds 137 lines of new edge-case coverage
packages/cli/src/utils/jsonc-editor.ts New 411-line JSONC editor: parse, path-edit, duplicate-key normalization, BOM/CRLF/comment preservation
scripts/check-serve-fast-path-bundle.js Add comment-json, esprima, and jsonc-parser UMD to the ACP forbidden-packages guard
scripts/tests/serve-fast-path-bundle-check.test.js Two new tests for the bundle guard additions

CI test evidence

All PR CI workflow runs on this commit have completed. The primary test suite (ubuntu-latest, Node 22.x) passed in ~35 minutes. macOS and Windows test jobs were skipped (conditional trigger), as were integration tests. No failures.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped
Test (windows-latest, Node 22.x) ⏭️ skipped
Integration Tests (CLI, No Sandbox) ⏭️ skipped
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Real daemon E2E / Java 11 ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success

Not verified: Windows runtime behavior (author notes CI is the cross-platform check; Windows test job was skipped on this commit). The author's cold-start benchmark numbers (35–49 ms P50 improvement) are self-reported from a controlled 2-vCPU Linux host — not independently reproduced here.

中文说明

代码审查

独立方案: 我会用 jsonc-parser(已在开发依赖图中)替换 comment-json,将其 parseTree/modify/applyEdits API 封装为保持现有 updateSettingsFilePreservingFormat 契约的工具模块,在 esbuild 中将入口别名到 ESM 构建,并扩展 bundle guard。PR 的做法与此一致。

与 diff 的比较: 实现比我的基线更深入——重复键规范化(先重命名早期出现再删除,绕过 jsonc-parser 的首匹配 modify() 行为)、删除属性时移除行内注释(防止孤立注释附着到前一属性)、以及写入前的重新解析深比较校验。这些都是 comment-json 的往返 stringify 之前隐式处理的真实边界情况。

未发现关键阻塞问题。几个观察:

  • applyUpdates 的 sync 模式过滤从 !(key in updates) 改为 !Object.hasOwn(updates, key)——这是正确性改进。旧的 in 运算符会匹配继承的 Object.prototype 属性(toStringconstructor),导致 sync 模式错误删除同名的自有键。新测试覆盖了这一点。
  • parseJsoncObject 使用 JSON.parse(JSON.stringify(getNodeValue(root))) 而非 structuredClone——刻意为之,因为它会剥离 undefined 并规范化特殊数值,测试确认 __proto__ 成为自有属性而不会污染 Object.prototype
  • 约定:kebab-case 文件名 ✓、同目录测试 ✓、ESM 导入 ✓、无 any ✓、legacy-filenames 允许列表已更新 ✓、设计文档在 docs/design/ ✓。

CI 测试证据

此提交的所有 PR CI 工作流运行已完成。主测试套件(ubuntu-latest, Node 22.x)在约 35 分钟内通过。macOS 和 Windows 测试作业被跳过(条件触发),集成测试同样跳过。无失败。

未验证:Windows 运行时行为(作者注明 CI 是跨平台检查;此提交的 Windows 测试作业被跳过)。作者的冷启动基准数字(P50 改善 35–49 ms)来自受控 2-vCPU Linux 主机的自报告——未在此独立复现。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review, but the 500+ production-line core-path threshold requires a maintainer's sign-off.

This is a well-executed dependency swap. The problem is real and measured (305 KB closure reduction, 35–49 ms P50 cold-start improvement across 30 paired runs), the implementation is thorough without being over-engineered, and every file in the diff serves the stated goal. The new jsonc-editor.ts handles edge cases that comment-json's round-trip stringify used to cover implicitly — BOM, CRLF, duplicate keys, inline-comment orphaning — and the re-parse-and-deep-compare validation before any write is a good safety net. The applyUpdates sync-mode fix (Object.hasOwn instead of in) is a genuine correctness improvement over the old code.

My independent proposal matched the PR's approach; I didn't find a simpler path it missed. CI is green on ubuntu. The code follows project conventions throughout.

Deferring only because the production-line count (616) on core paths (packages/cli/src/config/**) crosses the 500-line maintainer-awareness threshold. The core-path changes themselves are small and safe (import swaps + a 29-line trustedFolders adaptation), but policy calls for a human call at this size.

中文说明

置信度:3/5 —— 审查干净,但 500+ 生产行核心路径阈值需要维护者签字。

这是一次执行良好的依赖替换。问题真实且已量化(305 KB 闭包缩减,30 组配对运行中 P50 冷启动改善 35–49 ms),实现详尽但不过度工程化,diff 中每个文件都服务于既定目标。新的 jsonc-editor.ts 处理了 comment-json 往返 stringify 之前隐式覆盖的边界情况——BOM、CRLF、重复键、行内注释孤立——写入前的重新解析深比较校验是良好的安全网。applyUpdates sync 模式修复(Object.hasOwn 替代 in)是对旧代码的真实正确性改进。

我的独立方案与 PR 方法一致;未发现更简路径。CI 在 ubuntu 上通过。代码全程遵循项目约定。

仅因生产行数(616)触及核心路径(packages/cli/src/config/**)超过 500 行维护者关注阈值而转交。核心路径改动本身很小且安全(import 替换 + 29 行 trustedFolders 适配),但策略要求在此规模下由人工判断。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

⏸️ Deferring to @wenshao — the review is clean (no blockers, CI green, conventions followed), but this PR touches core paths (packages/cli/src/config/**) with 616 production logic lines, crossing the 500-line maintainer-awareness threshold. The core-path changes themselves are minimal import swaps; the bulk is the new jsonc-editor.ts utility. Needs a human call on whether the scope warrants a second pair of eyes before merge.

@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.

No issues found. LGTM! ✅

— qwen3.7-max via Qwen Code /review

@doudouOUC
doudouOUC requested a review from yiliang114 July 26, 2026 14:40

@yiliang114 yiliang114 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.

Review Summary

This is a well-designed and thoroughly implemented performance optimization that replaces comment-json with jsonc-parser for settings and trusted-folders writes. The PR demonstrates strong attention to edge cases, security, and test coverage.


✅ Strengths

1. Comprehensive Edge Case Handling

The implementation correctly handles:

  • UTF-8 BOM preservation (\uFEFF)
  • CRLF/LF line endings
  • Tab vs space indentation
  • Final newline preservation
  • Inline comments on deleted properties
  • Duplicate keys (removes earlier occurrences, keeps the effective last value)
  • Prototype pollution protection (__proto__, constructor, prototype filtered)

2. Output Validation

The updateJsoncContent function re-parses and deep-compares the edited output against the intended target before returning. This is a critical safety check that prevents silent corruption.

3. Good Test Coverage

Tests cover the important cases including:

  • Malformed input rejection
  • Non-object roots (arrays, primitives)
  • CRLF + tabs + BOM + final newline
  • Duplicate key normalization
  • Prototype-named properties (__proto__, constructor)
  • No-op byte-for-byte unchanged detection
  • Inline comment removal on deleted properties

4. Bundle Guard Extension

Properly extends the fast-path bundle guard to forbid comment-json, esprima, and the jsonc-parser UMD build from the ACP static closure.

5. Clean Dependency Changes

Correctly moves jsonc-parser from devDependencies to dependencies and removes the comment-json dependency chain.


🔍 Minor Observations

1. Duplicate Key Handling Complexity

The normalizeDuplicateProperties function uses a clever temporary key renaming approach. While it works, it iterates until no duplicates remain, which could theoretically be O(n²) for files with many duplicate keys. In practice, this is unlikely to be a problem since duplicate keys in config files are rare and files are small.

2. Structured Clone vs JSON Round-trip

In updateJsoncContent, the structuredClone followed by JSON.parse(JSON.stringify()) is intentional to handle undefined values. A brief comment explaining the two-step process would help future maintainers.

3. Trailing Comma in Test Expectation

The test expectation correctly accounts for the trailing comma that jsonc-parser.modify() produces given allowTrailingComma: true.


✅ Security Review

  • Prototype Pollution: Properly filtered in applyUpdates loop
  • No eval or dynamic code execution: Uses jsonc-parser's safe parse APIs
  • Atomic writes: Maintains the existing writeWithBackupSync pattern
  • Input validation: Rejects malformed JSONC before writing

✅ Correctness Verification

The implementation correctly:

  1. Strips BOM before parsing, re-adds after editing
  2. Detects indentation from actual property nodes (not comment contents)
  3. Removes inline comments when deleting properties (prevents orphaned comments)
  4. Validates output matches intended value before returning

📊 Performance Claims

The PR provides concrete benchmark data showing:

  • Bundle size reduction: 304,770 bytes (2.45%)
  • Cold start P50 improvements: 35-48ms across metrics
  • Statistical significance: 95% bootstrap intervals, 28/30 paired wins

The measurement methodology appears sound.


Conclusion

This is a high-quality implementation that achieves its performance goals while maintaining correctness and adding comprehensive test coverage. The edge case handling for comment preservation, formatting, and duplicate keys is thorough.

@doudouOUC
doudouOUC added this pull request to the merge queue Jul 26, 2026
Merged via the queue into QwenLM:main with commit 9bdc62c Jul 26, 2026
82 checks passed
@doudouOUC
doudouOUC deleted the perf/replace-comment-json-parser branch July 26, 2026 14:43
@wenshao

wenshao commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — local build + real end-to-end testing

I ran an independent local verification of this change at head 8034f66c2. The PR merged as 9bdc62c74 about 25 minutes after I started, so this is a post-merge report: everything below was measured against exactly the commit that landed on main, and the open items are follow-ups rather than merge blockers.

Verdict: the change does what it claims. The parser swap is behaviour-preserving on the settings values, the startup-closure reduction reproduces independently, and the security boundaries around trustedFolders are unchanged. On the common write path it is a clear improvement in format fidelity over comment-json. I found one narrow comment-retention regression and three test-coverage gaps, all listed at the end.

Environment: macOS 24.6.0, Node v22.23.1, isolated worktrees for 8034f66c2 (candidate) and its merge-base main@27927c46d (control), packages/core pre-built, both bundles produced with the same npm run check:serve-fast-path-bundle.


1. Gates

Check Result
jsonc-editor.test.ts + settings.test.ts + trustedFolders.test.ts 223 passed
Whole src/config + src/utils surface 86 files / 2094 tests passed
scripts/tests/serve-fast-path-bundle-check.test.js 31 passed
tsc --noEmit (packages/cli) clean
eslint --max-warnings 0 (all 10 changed files) clean
prettier --check (changed files + design doc) clean
npm run check:serve-fast-path-bundle passed on both control and candidate

gates


2. Behavioural equivalence — old writer vs new writer, side by side

I imported the deleted commentJson.ts and the new jsonc-editor.ts into a single process and ran both against byte-identical copies of every corpus file with the same update: 33 hand-written cases (comments in every position, CRLF, tabs, BOM, trailing commas, duplicate keys, prototype-named keys, deep nesting, arrays-of-objects, unicode, malformed and non-object roots) plus 400 seeded-random JSONC documents.

428 / 433 agree exactly. All five differences are intentional and are improvements:

Case old (comment-json) new (jsonc-parser)
settings.json is [1,2,3] returned true, rewrote the file as a reformatted array and silently dropped the update returns false + stderr diagnostic, file untouched
settings.json is "hello" / 42 returned true, silently dropped the update returns false + stderr diagnostic, file untouched
"toString": "zombie" in sync mode kept the zombie key ('toString' in updates is true via the prototype) correctly removed — this is the Object.hasOwn fix
own __proto__ key deleted it keeps it in merge mode, removes it in sync mode. Neither pollutes Object.prototype (verified).

equivalence


3. Real end-to-end A/B with the actual bundled product

Two isolated HOMEs, byte-identical v1 settings.json full of comments, one run of the real bundled CLI against each — no mocks anywhere.

Migration write-back (qwen mcp list triggers persistSettingsObject(sync=true)):

  • Settings values after migration: semantically identical between control and candidate.
  • Candidate preserves the final newline and the trailing // ── end of file ── comment that main destroys.
  • Candidate loses one inline comment that main keeps — see finding F1.

migration

Merge write path (qwen mcp add --scope user git uvx mcp-server-git) — here the candidate is strictly better: it keeps a leading comment and the final newline that main throws away, with identical semantics.

merge


4. Formatting churn — the practical win

For a single one-value edit (ui.theme: "Dracula" → "Default") on a 21-line settings file:

implementation lines before lines after kept verbatim rewritten
comment-json (main) 21 46 13 / 21 8
jsonc-parser (this PR) 21 21 20 / 21 1

main explodes every compact object and array in the file on every write. This PR touches only the edited line. For anyone who hand-maintains settings.json, that is the headline behavioural change and it is not mentioned in the PR description.

churn


5. The performance claim reproduces

I measured the ACP static closure myself — BFS over import-statement edges from the ACP entry chunk in each generated dist/esbuild.json, summing output bytes:

closure bytes
control (main@27927c46d) 12,528,737
candidate (PR 7747) 12,223,454
delta −305,283 bytes (−2.44 %)
PR's own claim −304,770 bytes (−2.45 %)

comment-json, esprima, array-timsort, repeat-string and has-own-prop are gone from the closure — and from the whole bundle (grep count 0). jsonc-parser resolves to the ESM build inside the closure with the UMD build absent, confirming the esbuild alias works as designed.

I did not attempt to reproduce the 2-vCPU Linux latency numbers; the closure reduction that drives them is confirmed.

closure


6. trustedFolders security boundaries — unchanged

A real-filesystem, no-mocks harness run against both implementations, 22 checks each: rule add/remove, 0600 mode, atomic write, concurrent-disk-rule preservation via the under-lock re-read, symlinked config path refused with the target untouched, lock released after failure, non-object/malformed roots rejected without overwriting, invalid trust levels on disk and in the proposal rejected, fresh-file creation.

22 / 22 pass on both. The only difference is comment retention on rule removal (3/4 vs 4/4) — the same F1 pattern.

trustedfolders


7. Unbundled compiled output under plain Node 22

No tsx, no vitest, no bundler — the path the published npm package takes. Node resolves the public entry to jsonc-parser/lib/umd/main.js while esbuild aliases to ESM; both work. All 8 assertions pass.

unbundled


8. Mutation verification of the new tests

I broke one guarantee at a time in jsonc-editor.ts and checked whether the 223 focused tests go red. 11 / 16 mutants killed.

Killed: BOM strip and re-attach, inline-comment removal, duplicate-key normalisation, the Object.hasOwn sync fix, non-object-root rejection, parse-error rejection, deletion collection, insertSpaces, replacePath, the prototype-key guard. That is genuinely good coverage for the load-bearing logic.

The 5 survivors break down as 3 equivalent mutants and 2 real gaps — I verified each rather than assuming:

  • insertFinalNewline and eol in detectFormattingOptions are inert for modify(). Proved by driving jsonc-parser directly: insertFinalNewline: true and false produce identical output, and eol: '\n' on a CRLF document still yields CRLF. Final-newline and CRLF preservation are real, but they come from jsonc-parser normalising to the document, not from these fields. The two assertions that look like they test this code do not.
  • F3 and F4 below are real coverage gaps.

mutation
survivors


Follow-ups

None of these block anything — the PR is merged and behaving correctly. Listing them so they are on record.

F1 · Deleting a property also deletes an inline comment belonging to a neighbouring surviving property. (low–medium)

removeTrailingInlineComment() handles the comment on the property being deleted, but jsonc-parser's removal range extends back over the separating comma and swallows an inline comment sitting on the preceding property; deleting the first property instead swallows the following property's leading comment. Reproduced three independent ways: the comment-fidelity matrix, the real bundled-CLI migration A/B, and the trustedFolders harness. It fires on persistSettingsObject(sync=true) — the migration write-back that runs on the first startup after an upgrade — and on trusted-folder rule removal, where per-rule comments have audit value.

Net effect is a trade, not a pure loss: the PR also keeps tail comments and the final newline that comment-json destroys. In the matrix, main and the PR each lose comments the other keeps.

verbatim
matrix

F2 · Duplicate-key normalisation also removes the comment describing the surviving occurrence. (low)

The design doc promises only that "comments owned by removed duplicate occurrences are removed with them". In practice, for

{
  // comment for the ignored duplicate
  "model": "a", // inline on ignored duplicate
  // comment for the EFFECTIVE occurrence
  "model": "b"
}

comment-json produced { // comment for the EFFECTIVE occurrence\n "model": "c" ... }; the new editor produces { "model": "c" } — all three comments gone. Correct value, over-eager comment removal. Duplicate keys in settings.json are pathological, hence low.

F3 · tabSize is load-bearing but untested. (test gap)

4-space indentation is preserved (verified), but forcing tabSize: 2 breaks nothing in the suite. A file indented with 4 spaces would silently gain 2-space keys. Worth a test — 4-space settings.json is common.

F4 · The output-vs-intent safety net has no test. (test gap)

Deleting if (!isDeepStrictEqual(reparsed, target)) throw … — the last line of defence against writing settings that do not match the caller's intent — breaks none of the 223 tests.

F5 · One test does not exercise what its name says. (minor)

detects indentation from properties rather than quoted comments: in that fixture the first property key already sits on its own line, so the indentation candidate is "\t" and the /^[ \t]*$/ guard never fires. The guard only fires when the first key is not at line start — e.g. a single-line { "a": 1 }. Adding that case would make the test match its name.


中文版本

维护者验证 —— 本地构建 + 真实端到端测试

我在 head 8034f66c2 上做了一次独立的本地验证。本 PR 在我开始约 25 分钟后以 9bdc62c74 合入,所以这是一份合并后报告:以下所有测量都针对最终落到 main 的那个 commit,遗留项属于后续跟进,而非合并阻塞。

结论:改动符合其声明。 解析器替换在 settings 取值层面保持行为不变,启动闭包缩减可独立复现,trustedFolders 的安全边界没有变化。在常见写入路径上,格式保真度相比 comment-json 明显更好。我发现一处很窄的注释保留回归和三处测试覆盖缺口,列在最后。

环境:macOS 24.6.0、Node v22.23.1,为 8034f66c2(candidate)与其 merge-base main@27927c46d(control)各建独立 worktree,预先构建 packages/core,两个 bundle 均用同一条 npm run check:serve-fast-path-bundle 产出。

1. 各项门禁

检查项 结果
jsonc-editor.test.ts + settings.test.ts + trustedFolders.test.ts 223 通过
整个 src/config + src/utils 86 文件 / 2094 测试通过
scripts/tests/serve-fast-path-bundle-check.test.js 31 通过
tsc --noEmit(packages/cli) 干净
eslint --max-warnings 0(全部 10 个改动文件) 干净
prettier --check(改动文件 + 设计文档) 干净
npm run check:serve-fast-path-bundle control 与 candidate 均通过

2. 行为等价性 —— 新旧写入实现并排对比

我把被删除的 commentJson.ts 与新的 jsonc-editor.ts 导入同一个进程,对每个语料文件的逐字节相同副本施加相同更新:33 个手写用例(各种位置的注释、CRLF、tab、BOM、尾逗号、重复键、原型同名键、深层嵌套、对象数组、Unicode、格式错误与非对象根)外加 400 个带种子的随机 JSONC 文档

433 个用例中 428 个完全一致。 5 处差异全部是有意为之,且都是改进:

场景 旧(comment-json 新(jsonc-parser
settings.json[1,2,3] 返回 true把文件重排成数组并静默丢弃本次更新 返回 false + stderr 提示,文件不动
settings.json"hello" / 42 返回 true静默丢弃更新 返回 false + stderr 提示,文件不动
sync 模式下的 "toString": "zombie" 保留该僵尸键('toString' in updates 经原型链为真) 正确删除 —— 即 Object.hasOwn 修复
自有 __proto__ 直接删掉 merge 模式保留、sync 模式删除;两者都不会污染 Object.prototype(已验证)

3. 用真实产物做端到端 A/B

两个隔离的 HOME,逐字节相同、写满注释的 v1 settings.json,各自跑一次真实 bundled CLI —— 全程无 mock。

迁移回写qwen mcp list 会触发 persistSettingsObject(sync=true)):

  • 迁移后的 settings 取值在 control 与 candidate 之间语义完全一致
  • candidate 保住了文件末尾换行main 会破坏掉的尾部 // ── end of file ── 注释。
  • candidate 丢了一条 main 保住的行内注释 —— 见 F1

merge 写入路径qwen mcp add --scope user git uvx mcp-server-git)—— 这里 candidate 严格更优:在语义一致的前提下,保住了 main 会丢掉的前置注释和末尾换行。

4. 格式扰动 —— 实际收益所在

对一个 21 行的 settings 文件做单个值的修改ui.theme: "Dracula" → "Default"):

实现 修改前行数 修改后行数 逐字保留 被重写
comment-json(main) 21 46 13 / 21 8
jsonc-parser(本 PR) 21 21 20 / 21 1

main 每次写入都会把文件里所有紧凑对象和数组展开。本 PR 只动被修改的那一行。对手工维护 settings.json 的人来说,这才是最显著的行为变化,而 PR 描述里并没有提到。

5. 性能声明可复现

我自行测量了 ACP 静态闭包 —— 在各自生成的 dist/esbuild.json 中,从 ACP 入口 chunk 沿 import-statement 边做 BFS 并累加 output 字节:

闭包字节数
control(main@27927c46d 12,528,737
candidate(PR 7747) 12,223,454
差值 −305,283 字节(−2.44 %)
PR 自称 −304,770 字节(−2.45 %)

comment-jsonesprimaarray-timsortrepeat-stringhas-own-prop 已从闭包中消失,也从整个 bundle 中消失(grep 计数为 0)。闭包内 jsonc-parser 解析到 ESM 构建,UMD 构建不存在,确认 esbuild alias 按设计生效。

我没有尝试复现 2-vCPU Linux 的时延数字;驱动这些数字的闭包缩减已得到确认。

6. trustedFolders 安全边界 —— 未变

用真实文件系统、无 mock 的 harness 对两个实现各跑 22 项检查:规则增删、0600 权限、原子写、通过持锁重读保留并发磁盘规则、拒绝符号链接配置路径且目标文件不受影响、失败后释放锁、拒绝非对象/格式错误根且不覆盖文件、拒绝磁盘上和拟写入的非法信任级别、新建文件。

两侧均 22 / 22 通过。 唯一差异是删除规则时的注释保留(3/4 对 4/4)—— 同属 F1

7. 未 bundle 的编译产物在纯 Node 22 下运行

不用 tsx、不用 vitest、不用打包器 —— 即已发布 npm 包所走的路径。Node 把公开入口解析到 jsonc-parser/lib/umd/main.js,而 esbuild alias 到 ESM;两条路都能跑。8 项断言全部通过。

8. 对新增测试做变异验证

我每次只破坏 jsonc-editor.ts 中的一条保证,检查那 223 个定向测试是否变红。16 个变异体杀掉 11 个。

被杀掉的包括:BOM 剥离与回贴、行内注释移除、重复键归一化、Object.hasOwn 的 sync 修复、非对象根拒绝、解析错误拒绝、删除操作收集、insertSpacesreplacePath、原型键防护。对核心逻辑而言这个覆盖确实不错。

5 个存活变异体可分为 3 个等价变异体 + 2 个真实缺口 —— 我逐一做了验证而非臆断:

  • detectFormattingOptions 里的 insertFinalNewlineeolmodify() 而言是惰性的。直接驱动 jsonc-parser 可证:insertFinalNewlinetruefalse 输出完全相同;对 CRLF 文档设 eol: '\n' 仍然输出 CRLF。末尾换行与 CRLF 的保留是真实的,但来自 jsonc-parser 自身按文档归一化,而不是这两个字段。因此看起来在测试这段代码的那两条断言,实际上并没有。
  • 下面的 F3F4 是真实的覆盖缺口。

后续跟进项

以下均不阻塞任何事 —— PR 已合并且行为正确,列出来只为存档。

F1 · 删除某个属性时,会连带删掉相邻存活属性的行内注释。(中低)

removeTrailingInlineComment() 处理了被删属性自身的注释,但 jsonc-parser 的删除区间会向前吃掉分隔逗号,从而把落在前一个属性行尾的行内注释一并吞掉;而删除第一个属性时,吞掉的是后一个属性的前置注释。已用三种独立方式复现:注释保真矩阵、真实 bundled CLI 迁移 A/B、trustedFolders harness。它会在 persistSettingsObject(sync=true)(升级后首次启动的迁移回写)以及删除受信目录规则时触发,而后者的逐条注释具有审计价值。

净效果是一种取舍而非纯粹损失:本 PR 同时保住了 comment-json 会破坏的尾部注释和末尾换行。在矩阵里,main 和本 PR 各自丢掉了对方保住的注释。

F2 · 重复键归一化会连带删掉描述存活那一份的注释。(低)

设计文档只承诺"随被移除的重复项一起删除其拥有的注释"。实测对于:

{
  // comment for the ignored duplicate
  "model": "a", // inline on ignored duplicate
  // comment for the EFFECTIVE occurrence
  "model": "b"
}

comment-json 产出 { // comment for the EFFECTIVE occurrence\n "model": "c" ... };新编辑器产出 { "model": "c" } —— 三条注释全没了。取值正确,但注释删得过头。settings.json 里出现重复键本身就很病态,故定为低。

F3 · tabSize 是承重逻辑却无测试覆盖。(测试缺口)

4 空格缩进确实被保留(已验证),但把 tabSize 强制为 2 不会让任何测试变红。这意味着 4 空格缩进的文件会悄悄多出 2 空格的键。值得补一条测试 —— 4 空格的 settings.json 很常见。

F4 · "输出与意图一致"这道保险没有任何测试。(测试缺口)

删掉 if (!isDeepStrictEqual(reparsed, target)) throw … —— 防止写出与调用方意图不符的 settings 的最后一道防线 —— 223 个测试无一变红。

F5 · 有一条测试并未测它名字所说的东西。(次要)

detects indentation from properties rather than quoted comments:该 fixture 里第一个属性键本就独占一行,缩进候选值已经是 "\t"/^[ \t]*$/ 这道防护根本不会触发。只有当第一个键不在行首时才会触发 —— 例如单行的 { "a": 1 }。补上这个用例才能名副其实。

@yiliang114

Copy link
Copy Markdown
Collaborator

Code Review Summary

This is a well-executed performance optimization that replaces comment-json with jsonc-parser for settings and trusted-folders writes. The implementation is thorough and the performance gains are well-documented.


✅ Strengths

1. Comprehensive Edge Case Handling

The new jsonc-editor.ts correctly handles:

  • UTF-8 BOM preservation (strips before parsing, re-adds after editing)
  • CRLF/LF line ending detection and preservation
  • Tab vs space indentation detection
  • Final newline preservation
  • Inline comment removal when deleting properties
  • Duplicate key normalization (removes earlier occurrences, keeps the effective last value)
  • Prototype pollution protection (__proto__, constructor, prototype filtered in applyUpdates)

2. Correctness Improvements

The change from !(key in updates) to !Object.hasOwn(updates, key) in sync mode is a genuine correctness fix. The old in operator would incorrectly match inherited Object.prototype properties (toString, constructor), causing sync mode to delete own keys with those names.

3. Output Validation Safety Net

The updateJsoncContent function re-parses the edited output and deep-compares it against the intended target before returning. This prevents silent corruption:

const reparsed = parseJsoncObject(updated);
if (!isDeepStrictEqual(reparsed, target)) {
  throw new Error('Edited JSONC does not match the intended settings.');
}

4. Better Format Preservation

A key behavioral improvement not emphasized in the PR description: comment-json would reformat the entire file on every write (expanding compact objects), while jsonc-parser.modify() only touches edited paths. For a 21-line settings file with a single value change, the old implementation rewrote 8 lines to 46; this PR touches just 1 line.

5. Proper Error Handling

  • Malformed JSONC: rejected without overwriting the file
  • Non-object roots (arrays, primitives): rejected with stderr diagnostic
  • Previous implementation would silently drop updates for non-object files

6. Bundle Guard Extensions

Correctly extends the fast-path bundle guard to forbid comment-json, esprima, and the jsonc-parser UMD build from the ACP static closure, preventing future regression.


🔍 Observations

1. Comment Retention Edge Case (low-medium)

As noted in the maintainer verification, deleting a property can also remove inline comments belonging to neighboring surviving properties due to how jsonc-parser's removal range extends over the separating comma. This is a trade-off: the PR also preserves trailing comments and final newlines that comment-json would destroy.

2. Duplicate Key Comment Handling (low)

The duplicate key normalization removes all comments associated with duplicate keys, including comments describing the surviving (effective) occurrence. Since duplicate keys in settings.json are pathological, this is appropriately low-priority.

3. Test Coverage Gaps (test)

  • tabSize is load-bearing but not directly tested
  • The output-vs-intent safety net (isDeepStrictEqual check) lacks test coverage
  • Consider adding tests for 4-space indentation preservation and forced mismatch scenarios

✅ Security Review

Check Status
Prototype pollution protection ✅ Filtered in applyUpdates
No dynamic code execution ✅ Uses safe parse APIs
Atomic writes maintained writeWithBackupSync preserved
Input validation ✅ Rejects malformed/non-object input
Trusted folders lock boundaries ✅ Unchanged

📊 Performance Claims Verification

The bundle size reduction is reproducible:

  • Claimed: −304,770 bytes (−2.45%)
  • Independently verified: −305,283 bytes (−2.44%)

The comment-json, esprima, array-timsort, repeat-string, and has-own-prop dependencies are fully removed from the bundle.


Conclusion

Approved

This is a high-quality implementation achieving meaningful performance improvements while maintaining correctness. The edge case handling for comment preservation, formatting, and duplicate keys is thorough. The follow-up items identified in the maintainer verification (F1-F5) are minor and do not block this change.


中文说明

代码审查总结

这是一次执行良好的性能优化,用 jsonc-parser 替换 comment-json 进行 settings 和 trusted-folders 写入。实现详尽,性能提升有据可查。

优点

  1. 边界情况处理完善:BOM、CRLF、tab 缩进、末尾换行、行内注释、重复键规范化、原型污染防护
  2. 正确性改进Object.hasOwn 替代 in 运算符,修复 sync 模式下的原型属性匹配问题
  3. 输出校验安全网:重新解析并深比较,防止静默损坏
  4. 格式保真度提升:只修改编辑路径,不重写整个文件
  5. 正确拒绝非对象根:旧实现会静默丢弃更新,新实现返回错误

观察

  • 删除属性时可能连带删除相邻存活属性的行内注释(取舍问题)
  • 重复键归一化会删除描述存活项的注释(低优先级)
  • 建议补充 tabSize 和输出校验的测试覆盖

结论

批准 ✅ 高质量实现,性能提升明确,正确性有保障。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.1.

@yiliang114

Copy link
Copy Markdown
Collaborator

⚠️ Failed to process this request. Please re-mention the bot to retry.

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.

4 participants