deveco_hmigbot 0.1.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/LICENSE +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-stubs.mjs — 未落地交互/属性桩检查(补门禁盲区:TODO 桩能编译、能过闭合,但按钮是死的)。
|
|
4
|
+
*
|
|
5
|
+
* 病根:transpile-layout 把无法直译的属性留成 `// TODO(attrs): ... onClick="X" ...` 注释,交由切片实现期接。
|
|
6
|
+
* 若实现期没接,组件就是死按钮(点击无反应)——却过 build(注释能编译)、过闭合(无悬空引用)、
|
|
7
|
+
* 过运行时走查(没测到那个交互)。实测应用科学键盘 49 个按钮全桩住即此类。
|
|
8
|
+
*
|
|
9
|
+
* 判据(确定性、任何阶段都错 → violation):某 `TODO(attrs)` 注释声明了 onClick="handler",
|
|
10
|
+
* 但其所属组件的方法链里**没有真实 `.onClick(`** → 死交互。前瞻窗口内见到 `.onClick(` 则视为已接、放过。
|
|
11
|
+
* 非交互属性桩(fontFamily/background 等未落地)→ case(外观债,集成/收尾阶段裁决)。
|
|
12
|
+
*
|
|
13
|
+
* 用法:check-stubs.mjs --project <鸿蒙工程根> [--json out] 退出码:0 无死交互 / 10 有 / 3 用法错
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
16
|
+
import { join, relative, resolve, dirname } from 'node:path';
|
|
17
|
+
import { walkFiles } from '../lib/scan.mjs';
|
|
18
|
+
|
|
19
|
+
const argv = process.argv.slice(2);
|
|
20
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
|
|
21
|
+
const PROJECT = argOf('--project', null);
|
|
22
|
+
if (!PROJECT) { console.error('usage: check-stubs.mjs --project <root> [--json out]'); process.exit(3); }
|
|
23
|
+
|
|
24
|
+
// ArkUI 源根:优先 entry/src/main/ets,兜底工程根(排除 build/产物)
|
|
25
|
+
const CAND = ['entry/src/main/ets', 'src/main/ets'];
|
|
26
|
+
let SRC = resolve(PROJECT);
|
|
27
|
+
for (const c of CAND) { const p = join(resolve(PROJECT), c); if (existsSync(p)) { SRC = p; break; } }
|
|
28
|
+
const rel = f => relative(resolve(PROJECT), f).replace(/\\/g, '/');
|
|
29
|
+
const SKIP = /(^|\/)(build|node_modules|oh_modules|\.hvigor|\.cxx)(\/|$)/;
|
|
30
|
+
|
|
31
|
+
const findings = [];
|
|
32
|
+
let files = 0, dead = 0, attrCases = 0, markers = 0; // markers=转译桩总数:0 = 本门不适用(Compose 源/手写页面没有 TODO(attrs)),不是"无死交互"
|
|
33
|
+
for (const f of walkFiles(SRC, { match: (_p, e) => e.endsWith('.ets') })) {
|
|
34
|
+
if (SKIP.test(rel(f))) continue;
|
|
35
|
+
files++;
|
|
36
|
+
const lines = readFileSync(f, 'utf8').split('\n');
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
if (!lines[i].includes('TODO(attrs)')) continue;
|
|
39
|
+
markers++;
|
|
40
|
+
const m = lines[i].match(/onClick=["']?([A-Za-z_$][\w$]*)["']?/);
|
|
41
|
+
if (m) {
|
|
42
|
+
// 前瞻:该组件方法链内是否已接真实 .onClick( ;遇下一个元素标记(⚓/TODO)即停
|
|
43
|
+
let implemented = false;
|
|
44
|
+
for (let j = i + 1; j < Math.min(i + 16, lines.length); j++) {
|
|
45
|
+
if (/\.onClick\s*\(/.test(lines[j]) || /\.onClick\b/.test(lines[j])) { implemented = true; break; }
|
|
46
|
+
if (j > i + 1 && /\/\/\s*(⚓|TODO\()/.test(lines[j])) break;
|
|
47
|
+
}
|
|
48
|
+
if (!implemented) {
|
|
49
|
+
dead++;
|
|
50
|
+
findings.push({ severity: 'violation', type: 'dead_interaction', name: m[1], file: rel(f), line: i + 1,
|
|
51
|
+
evidence: `${rel(f)}:${i + 1} 声明 onClick="${m[1]}" 但组件未接 .onClick()——死按钮(点击无反应),实现期漏接` });
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
attrCases++;
|
|
55
|
+
findings.push({ severity: 'case', type: 'attr_stub', name: 'attrs', file: rel(f), line: i + 1,
|
|
56
|
+
evidence: `${rel(f)}:${i + 1} 未落地的样式属性桩(外观债,需裁决:补样式或豁免)` });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const report = { project: relative(process.cwd(), resolve(PROJECT)) || '.', stats: { files, markers, applicable: markers > 0, deadInteractions: dead, attrStubs: attrCases }, findings };
|
|
62
|
+
const out = argOf('--json', null);
|
|
63
|
+
if (out) { mkdirSync(dirname(resolve(out)), { recursive: true }); writeFileSync(out, JSON.stringify(report, null, 2)); }
|
|
64
|
+
const vio = findings.filter(f => f.severity === 'violation');
|
|
65
|
+
console.log(`[check-stubs] 扫 ${files} 个 .ets · 死交互 ${dead} · 样式桩 ${attrCases}`);
|
|
66
|
+
for (const v of vio.slice(0, 12)) console.log(` ✗ ${v.evidence}`);
|
|
67
|
+
if (vio.length > 12) console.log(` …共 ${vio.length} 条死交互`);
|
|
68
|
+
if (!vio.length) console.log(markers ? ' ✓ 转译桩里无未接的 onClick 死交互' : ' · 无转译桩(TODO(attrs))——本门不适用,模型自写组件的点击死只能靠 4b 走查的 tapsNoChange 发现');
|
|
69
|
+
process.exit(vio.length ? 10 : 0);
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* closure-suite.mjs — 闭合检查套件统一入口(时机学说的执行者)。
|
|
4
|
+
*
|
|
5
|
+
* 检查带阶段属性,什么阶段跑什么:
|
|
6
|
+
* slice 切片收尾:资源悬空 + 路由硬违规(ghost)——"任何时候都错"的类别;软立案此阶段不计
|
|
7
|
+
* integration 集成闭合:全量三检查(路由含 orphan/dead/unreachable 立案 + 消费对账 + 资源)
|
|
8
|
+
* final 收尾:同 integration 且 --strict(软立案也阻断,逼裁决清零或豁免)
|
|
9
|
+
*
|
|
10
|
+
* 用法:node closure-suite.mjs --project <鸿蒙工程根> --stage slice|integration|final
|
|
11
|
+
* [--json out] [--profile p.json] [--waivers spec/closure-waivers.json]
|
|
12
|
+
* [--manifest spec/interface-manifest.json] [--idioms <写法表>] [--ledger <ledger.json>] [--prd <PRD 文件>]
|
|
13
|
+
* 退出码:0 通过 / 10 有硬违规 / 2 final 阶段有未裁决立案 / 3 环境错
|
|
14
|
+
* 豁免票:--waivers 文件里的票吸收同 type 软立案(精确票 name;模式票 match 须附 sampled ≥3 条实例证据),
|
|
15
|
+
* 票的 reason ≥20 字;final 阶段只对未被有效票吸收的立案计 exit 2。
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync, writeFileSync, mkdtempSync, existsSync, mkdirSync, statSync, realpathSync } from 'node:fs';
|
|
18
|
+
import { join, dirname, resolve, relative } from 'node:path';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { spawnSync } from 'node:child_process';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
import { moduleSrcRoots, walkFiles, stripComments } from '../lib/scan.mjs';
|
|
23
|
+
|
|
24
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const argv = process.argv.slice(2);
|
|
26
|
+
if (argv.includes('--help')) { console.log(readFileSync(new URL(import.meta.url), 'utf8').split('\n').filter(l => /^ \* /.test(l)).map(l => l.slice(3)).join('\n')); process.exit(0); }
|
|
27
|
+
const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : d; };
|
|
28
|
+
const PROJECT = argOf('--project', null);
|
|
29
|
+
const STAGE = argOf('--stage', 'integration');
|
|
30
|
+
const OUT = argOf('--json', null);
|
|
31
|
+
const PROFILE = argOf('--profile', null);
|
|
32
|
+
if (!PROJECT || !['slice', 'integration', 'final'].includes(STAGE)) {
|
|
33
|
+
console.error('usage: closure-suite.mjs --project <root> --stage slice|integration|final [--json out] [--profile p.json] [--waivers f] [--manifest f] [--idioms f] [--ledger f] [--prd f]');
|
|
34
|
+
process.exit(3);
|
|
35
|
+
}
|
|
36
|
+
const tmp = mkdtempSync(join(tmpdir(), 'closure-'));
|
|
37
|
+
// 子工具崩溃/超时/报告缺失绝不静默当"通过"——记 tool_error 硬违规(吞错=最危险的假阴)
|
|
38
|
+
function runTool(script, extra = []) {
|
|
39
|
+
const jf = join(tmp, script.replace(/\W/g, '_') + '.json');
|
|
40
|
+
const args = [join(HERE, script), '--project', PROJECT, '--json', jf, ...extra];
|
|
41
|
+
if (PROFILE) args.push('--profile', PROFILE);
|
|
42
|
+
const r = spawnSync(process.execPath, args, { encoding: 'utf8', timeout: 180000 });
|
|
43
|
+
let rep = null;
|
|
44
|
+
try { rep = JSON.parse(readFileSync(jf, 'utf8')); } catch { rep = null; }
|
|
45
|
+
const ranOk = rep && Array.isArray(rep.findings) && (r.status === 0 || r.status === 10 || r.status === 2);
|
|
46
|
+
const toolError = ranOk ? null : {
|
|
47
|
+
severity: 'violation', type: 'tool_error', name: script,
|
|
48
|
+
basis: '判据:闭合子工具未产出有效报告(崩溃/超时/退出码异常)——检查结果不可信,必须先修工具',
|
|
49
|
+
evidence: `${script} exit=${r.status === null ? 'timeout/killed' : r.status};stderr: ${(r.stderr || '').trim().slice(0, 300) || '(空)'}`,
|
|
50
|
+
};
|
|
51
|
+
return { code: r.status, rep: ranOk ? rep : null, toolError, out: (r.stdout || '').trim().split('\n').slice(0, 3).join(' | ') };
|
|
52
|
+
}
|
|
53
|
+
// 跑 closure 目录外的检查器(如 interface.mjs),自带 --json 写临时文件供解析
|
|
54
|
+
function runTool2(scriptPath, args) {
|
|
55
|
+
const jf = join(tmp, scriptPath.replace(/\W/g, '_') + '.json');
|
|
56
|
+
const r = spawnSync(process.execPath, [scriptPath, ...args, '--json', jf], { encoding: 'utf8', timeout: 180000 });
|
|
57
|
+
let rep = null;
|
|
58
|
+
try { rep = JSON.parse(readFileSync(jf, 'utf8')); } catch { rep = null; }
|
|
59
|
+
const ranOk = rep && Array.isArray(rep.findings) && (r.status === 0 || r.status === 10);
|
|
60
|
+
const toolError = ranOk ? null : { severity: 'violation', type: 'tool_error', name: scriptPath, basis: '判据:子工具未产出有效报告', evidence: `${scriptPath} exit=${r.status === null ? 'timeout/killed' : r.status};stderr: ${(r.stderr || '').trim().slice(0, 300) || '(空)'}` };
|
|
61
|
+
return { code: r.status, rep: ranOk ? rep : null, toolError };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const results = [];
|
|
65
|
+
const toolErrors = [];
|
|
66
|
+
const routes = runTool('check-routes.mjs', ['--stage', STAGE]);
|
|
67
|
+
if (routes.toolError) toolErrors.push(routes.toolError);
|
|
68
|
+
const routeVio = routes.rep ? routes.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
69
|
+
const routeCases = routes.rep ? routes.rep.findings.filter(f => f.severity === 'case') : [];
|
|
70
|
+
results.push({ tool: 'routes', violations: routeVio.length, cases: STAGE === 'slice' ? `(${routeCases.length} 条软立案,本阶段不计)` : routeCases.length });
|
|
71
|
+
|
|
72
|
+
const res = runTool('check-resources.mjs');
|
|
73
|
+
if (res.toolError) toolErrors.push(res.toolError);
|
|
74
|
+
const resVio = res.rep ? res.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
75
|
+
const resCases = res.rep ? res.rep.findings.filter(f => f.severity === 'case') : [];
|
|
76
|
+
results.push({ tool: 'resources', violations: resVio.length, cases: STAGE === 'slice' ? `(${resCases.length} 条软立案,本阶段不计)` : resCases.length });
|
|
77
|
+
|
|
78
|
+
let consFindings = [];
|
|
79
|
+
let stubVio = [], stubCases = [], permVio = [], permCases = [], guardVio = [], guardCases = [], saCases = [], ancCases = [];
|
|
80
|
+
if (STAGE !== 'slice') {
|
|
81
|
+
// --idioms <file>:透传给 check-consumers(数据驱动的写法表覆盖——项目私有 helper 名与
|
|
82
|
+
// 泛型 prefs API 撞名时按项目覆盖,改数据不改代码)。
|
|
83
|
+
const idiomsFile = argOf('--idioms', null);
|
|
84
|
+
const cons = runTool('check-consumers.mjs', idiomsFile ? ['--idioms', idiomsFile] : []);
|
|
85
|
+
if (cons.toolError) toolErrors.push(cons.toolError);
|
|
86
|
+
consFindings = cons.rep ? cons.rep.findings : [];
|
|
87
|
+
results.push({ tool: 'consumers', violations: 0, cases: consFindings.length });
|
|
88
|
+
|
|
89
|
+
// 未落地交互桩:TODO(attrs) 声明了 onClick 但组件没接 → 死按钮(硬违规)。只在集成/收尾跑——
|
|
90
|
+
// 切片阶段别的未实现片仍是原始桩,会误伤;集成期"该接的都接了吗"才是正确时机。样式桩记软立案。
|
|
91
|
+
const stubs = runTool('check-stubs.mjs');
|
|
92
|
+
if (stubs.toolError) toolErrors.push(stubs.toolError);
|
|
93
|
+
stubVio = stubs.rep ? stubs.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
94
|
+
stubCases = stubs.rep ? stubs.rep.findings.filter(f => f.severity === 'case') : [];
|
|
95
|
+
results.push({ tool: 'stubs', violations: stubVio.length, cases: stubCases.length });
|
|
96
|
+
|
|
97
|
+
// 权限完整性:用到要权限的 API 但 module.json5 没声明 → 真机能力失效/崩(硬违规)。
|
|
98
|
+
const perms = runTool('check-permissions.mjs');
|
|
99
|
+
if (perms.toolError) toolErrors.push(perms.toolError);
|
|
100
|
+
permVio = perms.rep ? perms.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
101
|
+
// 契约链断环/静默降级为软立案(final 阶段裁决)——能力"声明齐但运行时静默死"类
|
|
102
|
+
permCases = perms.rep ? perms.rep.findings.filter(f => f.severity === 'case') : [];
|
|
103
|
+
results.push({ tool: 'permissions', violations: permVio.length, cases: permCases.length });
|
|
104
|
+
|
|
105
|
+
// 崩溃防护:同步会抛的平台 API 未 try/catch → 未捕获异常杀进程(硬违规)。
|
|
106
|
+
const guards = runTool('check-api-guards.mjs');
|
|
107
|
+
if (guards.toolError) toolErrors.push(guards.toolError);
|
|
108
|
+
guardVio = guards.rep ? guards.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
109
|
+
// 立案:async 体/Promise 回调内裸调(静默失败档)、@Builder 裸引用等——final 阶段裁决
|
|
110
|
+
guardCases = guards.rep ? guards.rep.findings.filter(f => f.severity === 'case') : [];
|
|
111
|
+
results.push({ tool: 'api-guards', violations: guardVio.length, cases: guardCases.length });
|
|
112
|
+
|
|
113
|
+
// 布局保真度(安全区):edge-to-edge 页顶部避让脆弱 → 顶部重合。概率性、非确定性坏,故记**软立案**
|
|
114
|
+
// (final 阶段裁决:真重合就修、时序稳则豁免),绝不设硬违规——避免假阳性让模型循环修。
|
|
115
|
+
const sa = runTool('check-safearea.mjs');
|
|
116
|
+
if (sa.toolError) toolErrors.push(sa.toolError);
|
|
117
|
+
saCases = sa.rep ? sa.rep.findings.filter(f => f.severity === 'case') : [];
|
|
118
|
+
results.push({ tool: 'safearea', violations: 0, cases: saCases.length });
|
|
119
|
+
|
|
120
|
+
// 字面锚点:断言/PRD 引用的稳定 id 与可见文本,产物里须真有(只软立案——字面匹配有天然误报;
|
|
121
|
+
// 实证基准 13 条失败 11 条属此类)。--ledger 缺省 <project>/spec/ledger/ledger.json;--prd 缺省按断言锚点自动定位;都没有就空跑。
|
|
122
|
+
const anchorExtra = [];
|
|
123
|
+
const ledgerArg = argOf('--ledger', null), prdArg = argOf('--prd', null);
|
|
124
|
+
if (ledgerArg) anchorExtra.push('--ledger', ledgerArg);
|
|
125
|
+
if (prdArg) anchorExtra.push('--prd', prdArg);
|
|
126
|
+
const anc = runTool('check-anchors.mjs', anchorExtra);
|
|
127
|
+
if (anc.toolError) toolErrors.push(anc.toolError);
|
|
128
|
+
ancCases = anc.rep ? anc.rep.findings.filter(f => f.severity === 'case') : [];
|
|
129
|
+
results.push({ tool: 'anchors', violations: 0, cases: ancCases.length });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 跨切片接口一致性(契约靠派生):--manifest 提供时,下游对地基 import 必须对齐 manifest。
|
|
133
|
+
// 漂移=硬违规(确定性、任何阶段都错)——把契约冲突从末端 verify 降到检查点。地基未建(无 manifest)时跳过。
|
|
134
|
+
let ifaceVio = [], ifaceCases = [];
|
|
135
|
+
const MANIFEST = argOf('--manifest', null);
|
|
136
|
+
if (MANIFEST && existsSync(MANIFEST)) {
|
|
137
|
+
const iface = runTool2(join(HERE, '..', 'interface', 'interface.mjs'), ['check', '--project', PROJECT, '--manifest', MANIFEST]);
|
|
138
|
+
if (iface.toolError) toolErrors.push(iface.toolError);
|
|
139
|
+
ifaceVio = iface.rep ? iface.rep.findings.filter(f => f.severity === 'violation') : [];
|
|
140
|
+
// manifest 过期(下游 import 与真实导出一致、只是快照没跟上)=立案:重跑 extract 即消,不是下游漂移
|
|
141
|
+
ifaceCases = iface.rep ? iface.rep.findings.filter(f => f.severity === 'case') : [];
|
|
142
|
+
results.push({ tool: 'interface', violations: ifaceVio.length, cases: ifaceCases.length });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- 死文件(结构判据:引用图):ets 树里既没被任何相对 import 引用、也不在路由表/页面表/ability 入口/worker 清单里的 .ets/.ts ----
|
|
146
|
+
// 实证 45 工程:244 份转译骨架 244 份从未被引用(现已改为参考件不进工程)、antennapod 终检删了 10 个孤儿骨架页;死文件里的
|
|
147
|
+
// 悬空资源/样式桩每一波都要模型裁决。规则不看目录名:引用来源只认平台自己的登记表(route_map.pageSourceFile、main_pages.src、
|
|
148
|
+
// module.json5 srcEntry、build-profile workers)与源码 import。非 final 阶段把死文件内的立案/违规降为 info;final 记 dead_file 硬违规。
|
|
149
|
+
const deadFiles = [];
|
|
150
|
+
{
|
|
151
|
+
const norm = (x) => String(x).replace(/\\/g, '/');
|
|
152
|
+
const P = resolve(PROJECT); const relP = (f) => norm(relative(P, f));
|
|
153
|
+
const exists = (f) => { try { return statSync(f).isFile(); } catch { return false; } };
|
|
154
|
+
// 引用键统一取真实路径(macOS/Windows 文件系统大小写不敏感:import '../feedback' 能解析到 Index.ets,键必须按盘上真实大小写比)
|
|
155
|
+
const key = (f) => { try { return realpathSync.native(f); } catch { return resolve(f); } };
|
|
156
|
+
const resolveImport = (from, spec) => { const base = resolve(dirname(from), spec); for (const c of [base, base + '.ets', base + '.ts', join(base, 'index.ets'), join(base, 'index.ts')]) if (exists(c)) return c; return null; };
|
|
157
|
+
const referenced = new Set(); const all = [];
|
|
158
|
+
for (const etsRoot of moduleSrcRoots(P)) {
|
|
159
|
+
const mainDir = dirname(etsRoot), modRoot = dirname(dirname(mainDir));
|
|
160
|
+
const files = [...walkFiles(etsRoot, { match: (_p, e) => e.endsWith('.ets') || e.endsWith('.ts') })];
|
|
161
|
+
all.push(...files);
|
|
162
|
+
for (const f of files) {
|
|
163
|
+
let code; try { code = stripComments(readFileSync(f, 'utf8')); } catch { continue; }
|
|
164
|
+
for (const m of code.matchAll(/(?:from\s+|import\s*\(?\s*)['"](\.[^'"]+)['"]/g)) { const t = resolveImport(f, m[1]); if (t) referenced.add(key(t)); }
|
|
165
|
+
}
|
|
166
|
+
const addIf = (f) => { if (f && exists(f)) referenced.add(key(f)); };
|
|
167
|
+
// 平台登记表
|
|
168
|
+
const rm = join(mainDir, 'resources', 'base', 'profile', 'route_map.json');
|
|
169
|
+
if (exists(rm)) { try { for (const r of JSON.parse(readFileSync(rm, 'utf8')).routerMap || []) if (r.pageSourceFile) addIf(join(modRoot, r.pageSourceFile)); } catch { /* 坏 JSON 由路由门报 */ } }
|
|
170
|
+
const mj = join(mainDir, 'module.json5');
|
|
171
|
+
const mjText = exists(mj) ? stripComments(readFileSync(mj, 'utf8')) : '';
|
|
172
|
+
for (const m of mjText.matchAll(/["']srcEntry["']\s*:\s*["']([^"']+)["']/g)) addIf(join(mainDir, m[1]));
|
|
173
|
+
// 页面表文件名取 module.json5 的 "pages": "$profile:<名>"(不写死 main_pages)
|
|
174
|
+
for (const m of mjText.matchAll(/["']pages["']\s*:\s*["']\$profile:([^"']+)["']/g)) {
|
|
175
|
+
const mp = join(mainDir, 'resources', 'base', 'profile', m[1] + '.json');
|
|
176
|
+
if (exists(mp)) { try { for (const src of JSON.parse(readFileSync(mp, 'utf8')).src || []) addIf(join(etsRoot, src + '.ets')); } catch { /* 坏 JSON 由路由门报 */ } }
|
|
177
|
+
}
|
|
178
|
+
const bp = join(modRoot, 'build-profile.json5');
|
|
179
|
+
if (exists(bp)) { const t = stripComments(readFileSync(bp, 'utf8')); const w = t.match(/["']workers["']\s*:\s*\[([^\]]*)\]/); if (w) for (const m of w[1].matchAll(/["']([^"']+)["']/g)) addIf(join(modRoot, m[1])); }
|
|
180
|
+
}
|
|
181
|
+
for (const f of all) if (!referenced.has(key(f))) deadFiles.push(relP(f));
|
|
182
|
+
}
|
|
183
|
+
const deadSet = new Set(deadFiles);
|
|
184
|
+
const fileOf = (x) => String(x.file || (String(x.evidence || '').match(/^([^\s:]+\.(?:ets|ts))/) || [])[1] || '').replace(/\\/g, '/');
|
|
185
|
+
const inDead = (x) => deadSet.has(fileOf(x));
|
|
186
|
+
const demote = (arr) => arr.filter(x => { if (!inDead(x)) return true; x.severity = 'info'; x.demoted = 'dead_file'; return false; });
|
|
187
|
+
for (const arr of [routeVio, resVio, ifaceVio, stubVio, permVio, guardVio, routeCases, resCases, consFindings, stubCases, permCases, guardCases, saCases, ancCases, ifaceCases]) arr.splice(0, arr.length, ...demote(arr));
|
|
188
|
+
const deadFindings = STAGE === 'final' ? deadFiles.map(f => ({ severity: 'violation', type: 'dead_file', name: f.replace(/^.*\//, '').replace(/\.(ets|ts)$/, ''), file: f,
|
|
189
|
+
basis: '判据:该文件不在任何相对 import、route_map.pageSourceFile、main_pages.src、module.json5 srcEntry、build-profile workers 里——不在任何渲染/执行路径上',
|
|
190
|
+
evidence: `${f} 无任何引用;交付物不带死代码:删除它(其 $r 悬空引用与样式桩随之消失),确需保留则让真正的页面/组件 import 它` })) : [];
|
|
191
|
+
// 降级后重算各工具计数行(行是在降级前 push 的)
|
|
192
|
+
const recount = { routes: [routeVio, routeCases], resources: [resVio, resCases], consumers: [[], consFindings], stubs: [stubVio, stubCases], permissions: [permVio, permCases], 'api-guards': [guardVio, guardCases], safearea: [[], saCases], anchors: [[], ancCases], interface: [ifaceVio, ifaceCases] };
|
|
193
|
+
for (const r of results) { const pair = recount[r.tool]; if (pair && typeof r.cases === 'number') { r.violations = pair[0].length; r.cases = pair[1].length; } }
|
|
194
|
+
if (deadFiles.length) results.push({ tool: 'dead-files', violations: deadFindings.length, cases: `${deadFiles.length} 个未被引用的 .ets/.ts${STAGE === 'final' ? '(final 记 dead_file 硬违规)' : '(其内立案/违规已降 info)'}` });
|
|
195
|
+
|
|
196
|
+
const violations = [...toolErrors, ...routeVio, ...resVio, ...ifaceVio, ...stubVio, ...permVio, ...guardVio, ...deadFindings];
|
|
197
|
+
const rawCases = STAGE === 'slice' ? [] : [...routeCases, ...resCases, ...consFindings, ...stubCases, ...permCases, ...guardCases, ...saCases, ...ancCases, ...ifaceCases];
|
|
198
|
+
|
|
199
|
+
// ---- 豁免票(--waivers <file>):软立案裁决落盘且可核,批量豁免有门槛 ----
|
|
200
|
+
// 实证:550 条软立案被 4 张自写批量票全豁免。规则:每票 type + reason(≥20 字) + (name 精确 | match 正则);
|
|
201
|
+
// 模式票(match)必须附 sampled ≥3 条逐条核过的实例证据,否则票无效;票只吸收同 type 的立案。
|
|
202
|
+
// 格式:{ "waivers": [ { "type": "attr_stub", "match": "^layouts/", "reason": "...", "sampled": ["layouts/a.ets:12 …", …] },
|
|
203
|
+
// { "type": "orphan_route", "name": "DeepLinkEntry", "reason": "深链入口,无静态跳转属合法" } ] }
|
|
204
|
+
const WAIVERS = argOf('--waivers', null);
|
|
205
|
+
const waiverStat = { file: WAIVERS, valid: 0, invalid: [], absorbed: 0, byTicket: [] };
|
|
206
|
+
let cases = rawCases;
|
|
207
|
+
if (WAIVERS && existsSync(WAIVERS)) {
|
|
208
|
+
let entries = [];
|
|
209
|
+
try { const w = JSON.parse(readFileSync(WAIVERS, 'utf8')); entries = Array.isArray(w) ? w : (w.waivers || []); } catch { waiverStat.invalid.push('文件解析失败'); }
|
|
210
|
+
const tickets = [];
|
|
211
|
+
for (const e of entries) {
|
|
212
|
+
const label = `${e.type || e.id || e.title || '?'}:${e.match || e.name || '?'}`;
|
|
213
|
+
if (!e.type || !(e.match || e.name)) { waiverStat.invalid.push(`${label}(缺 type 或 name/match)`); continue; }
|
|
214
|
+
if (typeof e.reason !== 'string' || e.reason.trim().length < 20) { waiverStat.invalid.push(`${label}(reason 不足 20 字)`); continue; }
|
|
215
|
+
if (e.match && !(Array.isArray(e.sampled) && e.sampled.length >= 3)) { waiverStat.invalid.push(`${label}(模式票须附 sampled ≥3 条实例证据)`); continue; }
|
|
216
|
+
let re = null; if (e.match) { try { re = new RegExp(e.match); } catch { waiverStat.invalid.push(`${label}(match 非法正则)`); continue; } }
|
|
217
|
+
// 无界票:能匹配任意乱序 token 的正则(.* / .+ / ^ / [\s\S]*)等于豁免整个 type——实证一轮 3 张 .* 票吸收 72 条
|
|
218
|
+
if (re && ['zq9x7k2wv4nm', 'QX-mt_88Kd', '乱序票核'].every(t => re.test(t))) { waiverStat.invalid.push(`${label}(match 无界:能匹配任意文本,票等于豁免整个 ${e.type})`); continue; }
|
|
219
|
+
// 模式票的 sampled 必须指向本次报告里真实存在的立案(name 或 file:line 出现在 sampled 文本里)≥3 条——抄名单不算证据
|
|
220
|
+
if (re) {
|
|
221
|
+
const pool = rawCases.filter(c => c.type === e.type);
|
|
222
|
+
const realHit = (e.sampled || []).filter(sm => pool.some(c => (c.name && String(sm).includes(String(c.name))) || (c.file && String(sm).includes(`${c.file}:${c.line}`)) || (c.file && c.line == null && String(sm).includes(c.file))));
|
|
223
|
+
if (new Set(realHit).size < 3) { waiverStat.invalid.push(`${label}(sampled 里对得上本次立案的只有 ${new Set(realHit).size} 条,须 ≥3 条真实立案的 name 或 file:line)`); continue; }
|
|
224
|
+
}
|
|
225
|
+
tickets.push({ label, type: e.type, re, name: e.name, absorbed: 0 });
|
|
226
|
+
}
|
|
227
|
+
waiverStat.valid = tickets.length;
|
|
228
|
+
cases = rawCases.filter(c => {
|
|
229
|
+
const key = `${c.name || c.key || ''} ${c.file || ''} ${c.evidence || ''}`;
|
|
230
|
+
const hit = tickets.find(t => t.type === c.type && (t.re ? t.re.test(key) : t.name === (c.name || c.key)));
|
|
231
|
+
if (hit) { hit.absorbed++; waiverStat.absorbed++; return false; }
|
|
232
|
+
return true;
|
|
233
|
+
});
|
|
234
|
+
waiverStat.byTicket = tickets.map(t => ({ ticket: t.label, absorbed: t.absorbed }));
|
|
235
|
+
}
|
|
236
|
+
const summary = { project: PROJECT, stage: STAGE, violations, cases, results, ...(WAIVERS ? { waivers: waiverStat } : {}) };
|
|
237
|
+
if (OUT) { mkdirSync(dirname(resolve(OUT)), { recursive: true }); writeFileSync(OUT, JSON.stringify(summary, null, 2)); }
|
|
238
|
+
|
|
239
|
+
console.log(`[closure-suite] stage=${STAGE}`);
|
|
240
|
+
for (const r of results) console.log(` ${r.tool}: 硬违规 ${r.violations} · 立案 ${r.cases}`);
|
|
241
|
+
if (WAIVERS) {
|
|
242
|
+
console.log(` 豁免票:有效 ${waiverStat.valid} 张吸收 ${waiverStat.absorbed} 条${waiverStat.byTicket.length ? '(' + waiverStat.byTicket.map(t => `${t.ticket}→${t.absorbed}`).join(';') + ')' : ''}${waiverStat.invalid.length ? `;无效票 ${waiverStat.invalid.length}:${waiverStat.invalid.join(',')}` : ''}`);
|
|
243
|
+
}
|
|
244
|
+
for (const v of violations.slice(0, 10)) console.log(` ✗ ${v.type || ''} ${v.name || ''} ${v.evidence || ''}`.slice(0, 160));
|
|
245
|
+
// 立案全部按类型分组打出(名字 + 证据首句),模型据此直接裁决/写票,不必再去翻 JSON(实证:一次跑里 4 次 node -e 读报告)
|
|
246
|
+
if (STAGE !== 'slice' && cases.length) {
|
|
247
|
+
const byType = new Map(); for (const c of cases) { if (!byType.has(c.type)) byType.set(c.type, []); byType.get(c.type).push(c); }
|
|
248
|
+
let budget = 80;
|
|
249
|
+
for (const [t, arr] of byType) {
|
|
250
|
+
console.log(` ▲ ${t} ×${arr.length}`);
|
|
251
|
+
for (const c of arr) { if (budget-- <= 0) { console.log(` …(其余见 --json 报告)`); break; } console.log(` - [${c.name || c.key}] ${String(c.evidence || '').replace(/\s+/g, ' ').slice(0, 130)}`); }
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const exitCode = violations.length ? 10 : (STAGE === 'final' && cases.length ? 2 : 0);
|
|
255
|
+
console.log(exitCode === 0 ? ' ✓ 本阶段闭合' : exitCode === 2 ? ` ✋ final 阶段有 ${cases.length} 条立案未裁决(去裁决:修复或豁免)` : ' ✗ 有硬违规');
|
|
256
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.1.2",
|
|
3
|
+
"_doc": "闭合检查器的写法知识表(数据即语料:新写法加条目→过 benchmarks→发版,不改检查器代码)。每次修订必须跑 benchmarks/run.mjs 全绿。",
|
|
4
|
+
"nav": {
|
|
5
|
+
"pushApis": [
|
|
6
|
+
"pushPathByName",
|
|
7
|
+
"replacePathByName",
|
|
8
|
+
"pushDestinationByName"
|
|
9
|
+
],
|
|
10
|
+
"pushObjApis": [
|
|
11
|
+
"pushDestination",
|
|
12
|
+
"pushDestinationByName",
|
|
13
|
+
"pushPath"
|
|
14
|
+
],
|
|
15
|
+
"registrationVars": [
|
|
16
|
+
"name",
|
|
17
|
+
"routeName",
|
|
18
|
+
"destName"
|
|
19
|
+
],
|
|
20
|
+
"registrationMarker": ".navDestination(",
|
|
21
|
+
"patternPrefixMinLen": 2
|
|
22
|
+
},
|
|
23
|
+
"pageRouter": {
|
|
24
|
+
"pushApis": [
|
|
25
|
+
"pushUrl",
|
|
26
|
+
"replaceUrl"
|
|
27
|
+
],
|
|
28
|
+
"profileFile": "main_pages.json"
|
|
29
|
+
},
|
|
30
|
+
"prefs": {
|
|
31
|
+
"writeApis": [
|
|
32
|
+
"putSync",
|
|
33
|
+
"put",
|
|
34
|
+
"setOrCreate",
|
|
35
|
+
"setAndProp",
|
|
36
|
+
"setAndLink",
|
|
37
|
+
"set",
|
|
38
|
+
"setStr",
|
|
39
|
+
"setInt",
|
|
40
|
+
"setBool",
|
|
41
|
+
"setLong",
|
|
42
|
+
"setFloat",
|
|
43
|
+
"putString",
|
|
44
|
+
"putInt",
|
|
45
|
+
"putBoolean",
|
|
46
|
+
"putLong",
|
|
47
|
+
"putFloat",
|
|
48
|
+
"putStringSet"
|
|
49
|
+
],
|
|
50
|
+
"readApis": [
|
|
51
|
+
"getSync",
|
|
52
|
+
"get",
|
|
53
|
+
"has",
|
|
54
|
+
"hasSync",
|
|
55
|
+
"link",
|
|
56
|
+
"prop",
|
|
57
|
+
"getStr",
|
|
58
|
+
"getInt",
|
|
59
|
+
"getBool",
|
|
60
|
+
"getLong",
|
|
61
|
+
"getFloat",
|
|
62
|
+
"getString",
|
|
63
|
+
"getBoolean",
|
|
64
|
+
"getStringSet",
|
|
65
|
+
"getOptionalString",
|
|
66
|
+
"getOptionalInt",
|
|
67
|
+
"getOptionalBool",
|
|
68
|
+
"getOptionalLong",
|
|
69
|
+
"getOptionalFloat",
|
|
70
|
+
"getOptional"
|
|
71
|
+
],
|
|
72
|
+
"deleteApis": [
|
|
73
|
+
"deleteSync",
|
|
74
|
+
"delete"
|
|
75
|
+
],
|
|
76
|
+
"strongApis": [
|
|
77
|
+
"putSync",
|
|
78
|
+
"getSync",
|
|
79
|
+
"setOrCreate",
|
|
80
|
+
"hasSync",
|
|
81
|
+
"deleteSync"
|
|
82
|
+
],
|
|
83
|
+
"recvHint": "pref|storage|store|settings|config|kv",
|
|
84
|
+
"persistKeyShape": "^[a-z][a-z0-9_.-]{3,63}$"
|
|
85
|
+
},
|
|
86
|
+
"wrappers": {
|
|
87
|
+
"excludeFnNames": [
|
|
88
|
+
"if",
|
|
89
|
+
"for",
|
|
90
|
+
"while",
|
|
91
|
+
"switch",
|
|
92
|
+
"catch",
|
|
93
|
+
"constructor",
|
|
94
|
+
"return"
|
|
95
|
+
],
|
|
96
|
+
"componentDecorators": "@(Component|ComponentV2|Entry|CustomDialog|Reusable|Builder)\\b",
|
|
97
|
+
"chainRounds": 3
|
|
98
|
+
},
|
|
99
|
+
"risk": {
|
|
100
|
+
"keywords": "auth|pay|vip|push|share|oauth|crypto|wallet|login|会员|支付|登录|鉴权|推送|分享"
|
|
101
|
+
},
|
|
102
|
+
"plan": {
|
|
103
|
+
"unitIdPattern": "u-[a-z0-9-]+"
|
|
104
|
+
}
|
|
105
|
+
}
|