@yottameta/yotta-code-quality 0.3.4 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## v0.4.0 (2026-09-08)
2
+
3
+ **评测驱动完善**:新增 FAQ 与复杂场景走查,安装器错误处理与测试补齐。
4
+
5
+ - 新增 references/faq.md 与 references/walkthroughs.md。
6
+ - 安装器支持 --help、参数校验、统一退出码与人话错误提示。
7
+ - 新增安装器测试,覆盖未知智能体、缺参、成功安装与产物形态。
8
+
1
9
  # 更新日志
2
10
 
3
11
  ## v0.3.4 (2026-08-29)
@@ -8,7 +16,7 @@
8
16
 
9
17
  ## v0.3.3 (2026-08-28)
10
18
 
11
- 中英双语 README 对齐(老张拍板「英文门面 + 中文全档」):
19
+ 中英双语 README 对齐:
12
20
 
13
21
  - **README.md 改为英文**:作为 GitHub / npm / ClawHub 首页的英文门面(翻译 + 精简,覆盖定位 / 核心价值 / 工作流程 / 风险矩阵 / 目录结构 / 安装 / 使用 / 升级卸载 / FAQ / 来源与许可全流程)。
14
22
  - **新增 README.zh-CN.md**:原中文完整主文档整体平移,顶部加语言切换链接。
package/README.md CHANGED
@@ -227,6 +227,11 @@ Agent: PR mode → .code-quality.yaml config → auto scope → 7 steps in pr-re
227
227
 
228
228
  See [CHANGELOG.md](./CHANGELOG.md).
229
229
 
230
+ ## 参考文档
231
+
232
+ - 常见问题:`references/faq.md`
233
+ - 复杂场景走查:`references/walkthroughs.md`
234
+
230
235
  ## License
231
236
 
232
237
  [MIT](./LICENSE) © hyhmrright (original methodology) + YottaMeta (this packaging). "Yuanzhi" / "yotta-code-quality" and the YottaMeta family names (yotta-* prefix) are YottaMeta brand identifiers; derived works must not reuse them, see [NOTICE](./NOTICE).
package/README.zh-CN.md CHANGED
@@ -234,4 +234,9 @@ bash install.sh --list # 列出智能体 -> 默认目录
234
234
  ## 来源与许可
235
235
 
236
236
  - **方法论**蒸馏自 12 本经典软件工程著作及开源社区质量审查实践(MIT);原始方法论版权归 hyhmrright,本技能在其基础上重写为单一自包含、跨智能体通用的版本,并增补 R7 / UX1 / 按需加载会话契约。
237
- - **许可:** MIT —— 详见 `LICENSE`(版权人:hyhmrright(原始方法论)+ YottaMeta(本打包))。
237
+ - **许可:** MIT —— 详见 `LICENSE`(版权人:hyhmrright(原始方法论)+ YottaMeta(本打包))。
238
+
239
+ ## 参考文档
240
+
241
+ - references/faq.md
242
+ - references/walkthroughs.md
package/SKILL.md CHANGED
@@ -9,7 +9,7 @@ description: >-
9
9
  「结对评审」/「发版前扫一眼」/ yotta-code-quality.
10
10
  Do NOT trigger for: greenfield "how do I write X" with no code, pure syntax questions,
11
11
  or tool/framework questions with no shared code.
12
- version: 0.3.4
12
+ version: 0.4.0
13
13
  license: MIT
14
14
  ---
15
15
 
@@ -128,3 +128,5 @@ Scoring math and report template: **`references/common.md` only** (do not duplic
128
128
  | `references/examples.md` | Tone calibration |
129
129
  | `references/AGENTS-template.md` | Optional repo drop-in |
130
130
  | `references/hooks.json` | Optional dangerous-command hook |
131
+ | `references/faq.md` | Common questions and troubleshooting |
132
+ | `references/walkthroughs.md` | Complex review walkthroughs |
package/bin/install.js CHANGED
@@ -1,51 +1,97 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * yotta-code-quality 跨平台安装器(YottaSkills)
4
- * 用法:
5
- * npx -y @yottameta/yotta-code-quality --agent <name> # 按智能体默认用户级目录安装(推荐)
6
- * npx -y @yottameta/yotta-code-quality --dir PATH # 装到指定目录(用户改了目录的智能体)
7
- * npx -y @yottameta/yotta-code-quality -g # 安装到全部已知智能体用户级目录
8
- * npx -y @yottameta/yotta-code-quality # 安装到检测到的项目级目录
9
- * npx -y @yottameta/yotta-code-quality --list # 列出智能体 -> 默认目录
3
+ * @yottameta/yotta-code-quality cross-platform installer.
4
+ *
5
+ * Usage:
6
+ * npx -y @yottameta/yotta-code-quality --agent <name>
7
+ * npx -y @yottameta/yotta-code-quality --dir <path>
8
+ * npx -y @yottameta/yotta-code-quality -g
9
+ * npx -y @yottameta/yotta-code-quality
10
+ * npx -y @yottameta/yotta-code-quality --list
10
11
  */
11
12
  'use strict';
13
+
12
14
  const fs = require('fs');
13
15
  const path = require('path');
14
16
  const os = require('os');
15
17
 
16
18
  const SKILL_NAME = 'yotta-code-quality';
17
19
  const PKG_ROOT = path.join(__dirname, '..');
20
+ const COPY_SKIP = new Set(['package.json', 'bin', 'node_modules', '.git', '.github', 'test']);
18
21
 
19
- // 智能体 -> 用户级默认技能目录(dirs 按优先级排列;--agent 装到第一个)
20
- // 依据官方文档:.agents/skills 并非通用目录,被 OpenCode / Cursor / Cline / Amp /
21
- // Kimi / Gemini CLI / GitHub Copilot 等读取;Claude Code 与 Codex 默认不读 .agents。
22
22
  const AGENT_DIRS = {
23
23
  claude: { label: 'Claude Code', dirs: ['.claude/skills'] },
24
24
  cursor: { label: 'Cursor', dirs: ['.cursor/skills', '.agents/skills'] },
25
- codex: { label: 'Codex', dirs: ['.codex/skills'] }, // 特判:$CODEX_HOME/skills
25
+ codex: { label: 'Codex', dirs: ['.codex/skills'] },
26
26
  gemini: { label: 'Gemini CLI', dirs: ['.gemini/skills', '.agents/skills'] },
27
27
  goose: { label: 'Goose', dirs: ['.config/goose/skills', '.agents/skills'] },
28
28
  amp: { label: 'Amp', dirs: ['.config/agents/skills', '.agents/skills'] },
29
- opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] }, // 特判:$XDG_CONFIG_HOME
29
+ opencode: { label: 'OpenCode', dirs: ['.config/opencode/skills'] },
30
30
  windsurf: { label: 'Windsurf', dirs: ['.codeium/windsurf/skills'] },
31
31
  workbuddy: { label: 'WorkBuddy', dirs: ['.workbuddy/skills'] },
32
32
  kiro: { label: 'Kiro', dirs: ['.kiro/skills'] },
33
33
  trae: { label: 'Trae Code CLI', dirs: ['.traecli/skills'] },
34
- 'trae-cn': { label: 'Trae IDE(国内)', dirs: ['.trae-cn/skills'] },
34
+ 'trae-cn': { label: 'Trae IDE', dirs: ['.trae-cn/skills'] },
35
35
  qwen: { label: 'Qwen Code', dirs: ['.qwen/skills'] },
36
- comate: { label: 'Comate 文心快码', dirs: ['.comate/skills'] },
36
+ comate: { label: 'Comate', dirs: ['.comate/skills'] },
37
37
  codebuddy: { label: 'CodeBuddy Code', dirs: ['.codebuddy/skills'] },
38
38
  kimi: { label: 'Kimi Code CLI', dirs: ['.kimi/skills'] },
39
- agents: { label: '通用 AGENTS.md', dirs: ['.agents/skills'] },
39
+ agents: { label: 'Generic AGENTS.md', dirs: ['.agents/skills'] },
40
40
  };
41
41
 
42
- // Codex 用户级目录特判:优先 $CODEX_HOME/skills,否则 ~/.codex/skills
42
+ class UsageError extends Error {}
43
+ class TargetError extends Error {}
44
+ class InstallError extends Error {}
45
+
46
+ function usage() {
47
+ console.log('yotta-code-quality installer');
48
+ console.log('');
49
+ console.log('Usage:');
50
+ console.log(' npx -y @yottameta/yotta-code-quality --agent <name> Install to an agent default directory');
51
+ console.log(' npx -y @yottameta/yotta-code-quality --dir <path> Install to a custom directory');
52
+ console.log(' npx -y @yottameta/yotta-code-quality -g Install to all known agent directories');
53
+ console.log(' npx -y @yottameta/yotta-code-quality Auto-detect project-level directories');
54
+ console.log(' npx -y @yottameta/yotta-code-quality --list List supported agent directories');
55
+ console.log('');
56
+ console.log('Options:');
57
+ console.log(' --agent <name> Agent key, see --list');
58
+ console.log(' --dir <path> Custom skills directory');
59
+ console.log(' -g, --global Install to all known user-level directories');
60
+ console.log(' --list, -l List supported agents');
61
+ console.log(' -h, --help Show this help');
62
+ }
63
+
64
+ function parseArgs(argv) {
65
+ const opts = { help: false, list: false, global: false, dir: null, agent: null };
66
+ for (let i = 0; i < argv.length; i++) {
67
+ const arg = argv[i];
68
+ if (arg === '--help' || arg === '-h') opts.help = true;
69
+ else if (arg === '--list' || arg === '-l') opts.list = true;
70
+ else if (arg === '--global' || arg === '-g') opts.global = true;
71
+ else if (arg === '--dir') {
72
+ const value = argv[++i];
73
+ if (!value) throw new UsageError('--dir requires a non-empty path');
74
+ opts.dir = value;
75
+ } else if (arg === '--agent') {
76
+ const value = argv[++i];
77
+ if (!value) throw new UsageError('--agent requires a non-empty name');
78
+ opts.agent = value.toLowerCase();
79
+ } else {
80
+ throw new UsageError('Unknown argument: ' + arg);
81
+ }
82
+ }
83
+ if (!opts.help) {
84
+ const selected = [opts.dir, opts.agent, opts.global].filter(Boolean).length;
85
+ if (selected > 1) throw new UsageError('Use only one of --dir, --agent, or -g');
86
+ }
87
+ return opts;
88
+ }
89
+
43
90
  function codexUserDir() {
44
91
  const base = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
45
92
  return path.join(base, 'skills');
46
93
  }
47
94
 
48
- // OpenCode 用户级目录特判:优先 $XDG_CONFIG_HOME/opencode/skills,否则 ~/.config/opencode/skills
49
95
  function opencodeUserDir() {
50
96
  const base = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
51
97
  return path.join(base, 'opencode', 'skills');
@@ -57,82 +103,65 @@ function resolveUserDir(rel) {
57
103
  return path.join(os.homedir(), rel);
58
104
  }
59
105
 
60
- function installTo(dest) {
61
- const target = path.join(dest, SKILL_NAME);
62
- fs.mkdirSync(target, { recursive: true });
63
- copyDir(PKG_ROOT, target, new Set(['package.json', 'bin', 'node_modules', '.git']));
64
- console.log('installed -> ' + target);
65
- }
66
-
67
- function copyDir(src, dst, skip) {
68
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
69
- if (skip.has(entry.name)) continue;
70
- const s = path.join(src, entry.name);
71
- const d = path.join(dst, entry.name);
72
- if (entry.isDirectory()) {
73
- fs.mkdirSync(d, { recursive: true });
74
- copyDir(s, d, skip);
75
- } else if (entry.isFile()) {
76
- fs.copyFileSync(s, d);
77
- }
78
- }
79
- }
80
-
81
106
  function displayDir(rel) {
82
107
  if (process.platform === 'win32') return '%USERPROFILE%\\' + rel.replace(/\//g, '\\');
83
108
  return '~/' + rel;
84
109
  }
85
110
 
86
- function main() {
87
- const args = process.argv.slice(2);
88
- const isGlobal = args.includes('-g') || args.includes('--global');
89
- const list = args.includes('--list') || args.includes('-l');
90
- let explicitDir = null;
91
- const di = args.indexOf('--dir');
92
- if (di !== -1 && args[di + 1]) explicitDir = args[di + 1];
93
- let agent = null;
94
- const ai = args.indexOf('--agent');
95
- if (ai !== -1 && args[ai + 1]) agent = String(args[ai + 1]).toLowerCase();
96
-
97
- if (list) {
98
- console.log('智能体 -> 默认技能目录(--agent <name> 装到第一个,用户级):');
99
- for (const [key, v] of Object.entries(AGENT_DIRS)) {
100
- const resolved = v.dirs.map(displayDir);
101
- console.log(' ' + key.padEnd(10) + v.label.padEnd(18) + resolved.join('、'));
102
- }
103
- console.log('\n说明:Windows 用 %USERPROFILE%,Linux/macOS 用 ~;仅收录有官方默认目录的智能体。');
104
- console.log('改了目录的请用 --dir <路径>,不要依赖默认位置;若设置了 CODEX_HOME / XDG_CONFIG_HOME,安装自动以该变量为准。');
105
- return;
111
+ function printList() {
112
+ console.log('Agent -> default skill directory:');
113
+ for (const [key, value] of Object.entries(AGENT_DIRS)) {
114
+ console.log(' ' + key.padEnd(12) + value.label.padEnd(20) + value.dirs.map(displayDir).join(', '));
106
115
  }
116
+ console.log('');
117
+ console.log('Use --dir <path> for agents not listed. CODEX_HOME and XDG_CONFIG_HOME are respected.');
118
+ }
107
119
 
108
- if (explicitDir) { installTo(explicitDir); return; }
120
+ function assertSafeTarget(target) {
121
+ const rel = path.relative(PKG_ROOT, target);
122
+ if (rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel))) {
123
+ throw new UsageError('Target directory must be outside the skill source directory');
124
+ }
125
+ }
109
126
 
110
- if (agent) {
111
- const info = AGENT_DIRS[agent];
112
- if (!info) {
113
- console.log('未收录智能体: ' + agent + '。请用 --dir <路径> 指定技能目录。');
114
- console.log('可用: ' + Object.keys(AGENT_DIRS).join(', '));
115
- return;
127
+ function copyDir(src, dst, skip) {
128
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
129
+ if (skip.has(entry.name)) continue;
130
+ const from = path.join(src, entry.name);
131
+ const to = path.join(dst, entry.name);
132
+ try {
133
+ if (entry.isDirectory()) {
134
+ fs.mkdirSync(to, { recursive: true });
135
+ copyDir(from, to, skip);
136
+ } else if (entry.isFile()) {
137
+ fs.copyFileSync(from, to);
138
+ }
139
+ } catch (err) {
140
+ throw new InstallError('Failed to copy ' + from + ' -> ' + to + ': ' + err.message);
116
141
  }
117
- installTo(resolveUserDir(info.dirs[0]));
118
- console.log('完成。');
119
- return;
120
142
  }
143
+ }
121
144
 
122
- if (isGlobal) {
123
- const seen = new Set();
124
- for (const v of Object.values(AGENT_DIRS)) {
125
- for (const d of v.dirs) {
126
- if (seen.has(d)) continue;
127
- seen.add(d);
128
- installTo(resolveUserDir(d));
129
- }
145
+ function installTo(dest) {
146
+ if (!dest || typeof dest !== 'string') throw new UsageError('Destination directory is required');
147
+ const target = path.resolve(dest, SKILL_NAME);
148
+ assertSafeTarget(target);
149
+ try {
150
+ fs.mkdirSync(target, { recursive: true });
151
+ copyDir(PKG_ROOT, target, COPY_SKIP);
152
+ if (!fs.existsSync(path.join(target, 'SKILL.md'))) {
153
+ throw new InstallError('Installed directory is missing SKILL.md');
130
154
  }
131
- console.log('完成。');
132
- return;
155
+ } catch (err) {
156
+ if (err instanceof UsageError || err instanceof InstallError) throw err;
157
+ throw new InstallError('Cannot install to ' + target + ': ' + err.message);
133
158
  }
159
+ console.log('installed -> ' + target);
160
+ return target;
161
+ }
134
162
 
135
- const PROJECT_DIRS = [
163
+ function projectDirs() {
164
+ return [
136
165
  '.claude/skills',
137
166
  '.cursor/skills',
138
167
  '.codex/skills',
@@ -150,14 +179,67 @@ function main() {
150
179
  '.codebuddy/skills',
151
180
  '.kimi/skills',
152
181
  '.agents/skills',
153
- ];
154
- let installedAny = false;
155
- for (const d of PROJECT_DIRS) {
156
- if (fs.existsSync(d)) { installTo(d); installedAny = true; }
182
+ ].filter((dir) => fs.existsSync(dir));
183
+ }
184
+
185
+ function run() {
186
+ const opts = parseArgs(process.argv.slice(2));
187
+ if (opts.help) { usage(); return; }
188
+ if (opts.list) { printList(); return; }
189
+
190
+ if (opts.dir) { installTo(opts.dir); return; }
191
+
192
+ if (opts.agent) {
193
+ const info = AGENT_DIRS[opts.agent];
194
+ if (!info) {
195
+ throw new UsageError('Unknown agent: ' + opts.agent + '. Available: ' + Object.keys(AGENT_DIRS).join(', ') + '. Use --dir for a custom directory.');
196
+ }
197
+ installTo(resolveUserDir(info.dirs[0]));
198
+ return;
157
199
  }
158
- if (!installedAny) {
159
- console.log('未检测到项目级智能体目录。可手动复制,或用 --agent <name> / -g 装到用户级。');
200
+
201
+ if (opts.global) {
202
+ const seen = new Set();
203
+ let count = 0;
204
+ for (const value of Object.values(AGENT_DIRS)) {
205
+ for (const dir of value.dirs) {
206
+ if (seen.has(dir)) continue;
207
+ seen.add(dir);
208
+ installTo(resolveUserDir(dir));
209
+ count++;
210
+ }
211
+ }
212
+ console.log('Installed to ' + count + ' directories.');
213
+ return;
214
+ }
215
+
216
+ const dirs = projectDirs();
217
+ if (!dirs.length) {
218
+ throw new TargetError('No project-level agent directory detected. Use --agent <name> or --dir <path>.');
219
+ }
220
+ for (const dir of dirs) installTo(dir);
221
+ }
222
+
223
+ function main() {
224
+ try {
225
+ run();
226
+ } catch (err) {
227
+ if (err instanceof UsageError) {
228
+ console.error('Usage error: ' + err.message);
229
+ usage();
230
+ process.exitCode = 2;
231
+ } else if (err instanceof TargetError) {
232
+ console.error('Target error: ' + err.message);
233
+ process.exitCode = 4;
234
+ } else if (err instanceof InstallError) {
235
+ console.error('Install failed: ' + err.message);
236
+ console.error('Fix: check directory permissions and free space, then retry. Use --dir to choose another directory.');
237
+ process.exitCode = 1;
238
+ } else {
239
+ console.error('Unexpected error: ' + (err && err.message ? err.message : String(err)));
240
+ process.exitCode = 1;
241
+ }
160
242
  }
161
243
  }
162
244
 
163
- main();
245
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-code-quality",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Pair-style code quality reviewer: twelve book-grounded decay risks (R1-R6, T1-T6) plus release-safety and first-paint UX checks; Iron Law findings (Symptom -> Source -> Consequence -> Remedy) and 0-100 Health Score.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -31,5 +31,8 @@
31
31
  },
32
32
  "bin": {
33
33
  "yotta-code-quality": "bin/install.js"
34
+ },
35
+ "scripts": {
36
+ "test": "node --test \"test/*.test.js\""
34
37
  }
35
38
  }
@@ -0,0 +1,50 @@
1
+ # 元质常见问题
2
+
3
+ ## 速查索引
4
+
5
+ - **触发**:什么时候用 · 每次都读全部文档吗
6
+ - **评审**:评分怎么算 · 大 PR 怎么办 · 没有测试怎么办
7
+ - **集成**:CI / hook 怎么接
8
+ - **边界**:和编译器 / 测试的关系 · 输出格式
9
+
10
+ ---
11
+
12
+ ## 1. 什么时候应该触发?
13
+
14
+ 用户要求 review、检查代码质量、判断是否可合并、识别技术债、评估测试质量,或在发布前做质量门时触发。
15
+
16
+ ## 2. 每次都要读全部参考文档吗?
17
+
18
+ 不需要。先读 `SKILL.md`,按模式加载:日常 review 读 `pr-review-guide.md` 与 `common.md`;需要风险定义时读 `decay-risks.md` / `test-decay-risks.md`;发布或 UX 专项再读 `editorial-extensions.md`。
19
+
20
+ ## 3. 健康分怎么算?
21
+
22
+ 按发现数量与严重度加权,具体公式和报告模板在 `references/common.md`。分数用于趋势比较,不替代人工判断。
23
+
24
+ ## 4. 发现必须按什么格式写?
25
+
26
+ 使用 Iron Law 四段:Symptom → Source → Consequence → Remedy。缺少任一段应补齐后再输出。
27
+
28
+ ## 5. 大 PR 怎么处理?
29
+
30
+ 先分层:入口 / 配置 / 数据流 / 测试 / 文档。优先 R2(正确性),再看 R1 / R3 / R4;只对高风险路径展开全文,不在低风险格式化改动上消耗篇幅。
31
+
32
+ ## 6. 没有测试的项目怎么评审?
33
+
34
+ 明确标注“测试信号缺失”,并把它作为发现输出;不要假设测试通过。可建议最小回归清单。
35
+
36
+ ## 7. 可以接入 CI 或 hook 吗?
37
+
38
+ 可以。`references/hooks.json` 提供危险命令拦截示例,`references/AGENTS-template.md` 可作为仓库规则模板。接入前先在本地试运行。
39
+
40
+ ## 8. 发现太多时怎么排序?
41
+
42
+ 按 Blocker > Major > Warning > Suggestion。同级里先输出影响正确性和发布安全的发现。
43
+
44
+ ## 9. 它会替代编译器或测试吗?
45
+
46
+ 不会。元质负责评审决策与风险解释;编译、测试、运行验证仍由对应工具完成。
47
+
48
+ ## 10. 输出可以直接贴到 PR 吗?
49
+
50
+ 可以。按 `references/common.md` 的模板输出,包含健康分、发现清单与复核建议。
@@ -0,0 +1,30 @@
1
+ # 元质复杂场景走查
2
+
3
+ ## 走查 1:小改动发布评审
4
+
5
+ 输入:一个 40 行的服务层改动,修改错误处理并新增一个参数。
6
+
7
+ 1. 先读 diff 与调用点,确认入口和影响面。
8
+ 2. 按 R2 检查错误分支是否改变语义。
9
+ 3. 按 R3 检查新增参数是否有默认值与调用方兼容。
10
+ 4. 按 T1/T3 检查测试是否覆盖新错误分支。
11
+ 5. 输出 2-3 条高价值发现,附 Iron Law 四段与健康分。
12
+
13
+ ## 走查 2:大型 PR 分层评审
14
+
15
+ 输入:一个 2000 行重构 PR。
16
+
17
+ 1. 只看文件清单与依赖图,先分出入口、领域、基础设施、测试。
18
+ 2. 对高风险模块做全文评审,低风险机械改动只抽样。
19
+ 3. 优先检查 R4(耦合)、R5(结构)、R6(命名)。
20
+ 4. 输出分层结论和必须人工复核的路径。
21
+ 5. 标注“未全文评审”的范围,不假装全覆盖。
22
+
23
+ ## 走查 3:首屏 UX 专项
24
+
25
+ 输入:一个新页面,功能可用但首屏偏慢。
26
+
27
+ 1. 按 UX1 检查首屏关键路径。
28
+ 2. 识别阻塞渲染的脚本、字体和图片。
29
+ 3. 按 R7 检查是否存在不必要的复杂度。
30
+ 4. 输出可验证的优化建议,如“延迟加载非首屏模块”“压缩首屏图片”。