deveco_hmigbot 0.21.5

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.
Files changed (101) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +50 -0
  3. package/agents/hmigbot-worker.md +61 -0
  4. package/agents/hmigbot.md +22 -0
  5. package/agents/workflow-subagent.md +55 -0
  6. package/commands/hmigbot.md +17 -0
  7. package/dist/index.js +1 -0
  8. package/manifest.json +19 -0
  9. package/package.json +29 -0
  10. package/skills/migrate-core/FILES.md +26 -0
  11. package/skills/migrate-core/SKILL.md +484 -0
  12. package/skills/migrate-core/references/README.md +64 -0
  13. package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
  14. package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
  15. package/skills/migrate-core/references/flow/conventions-template.md +244 -0
  16. package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
  17. package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
  18. package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
  19. package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
  20. package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
  21. package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
  22. package/skills/migrate-core/references/host-capabilities.md +24 -0
  23. package/skills/migrate-core/references/topics/app-identity.md +214 -0
  24. package/skills/migrate-core/references/topics/env-doctor.md +245 -0
  25. package/skills/migrate-core/references/topics/i18n/README.md +458 -0
  26. package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
  27. package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
  28. package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
  29. package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
  30. package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
  31. package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
  32. package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
  33. package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
  34. package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
  35. package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
  36. package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
  37. package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
  38. package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
  39. package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
  40. package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
  41. package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
  42. package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
  43. package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
  44. package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
  45. package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
  46. package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
  47. package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
  48. package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
  49. package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
  50. package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
  51. package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
  52. package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
  53. package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
  54. package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
  55. package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
  56. package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
  57. package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
  58. package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
  59. package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
  60. package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
  61. package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
  62. package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
  63. package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
  64. package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
  65. package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
  66. package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
  67. package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
  68. package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
  69. package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
  70. package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
  71. package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
  72. package/skills/migrate-core/scripts/closure/idioms.json +105 -0
  73. package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
  74. package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
  75. package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
  76. package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
  77. package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
  78. package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
  79. package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
  80. package/skills/migrate-core/scripts/front.mjs +107 -0
  81. package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
  82. package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
  83. package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
  84. package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
  85. package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
  86. package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
  87. package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
  88. package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
  89. package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
  90. package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
  91. package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
  92. package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
  93. package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
  94. package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
  95. package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
  96. package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
  97. package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
  98. package/skills/migrate-core/templates/acceptance.js +365 -0
  99. package/skills/migrate-core/templates/explore.js +86 -0
  100. package/skills/migrate-core/templates/implement.js +211 -0
  101. package/skills/migrate-core/templates/mig_slices.js +491 -0
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * front.mjs — 2a 静态前场一条命令(确定性环节首尾相接,根会话一次调用、只处置报出来的问题)。
4
+ *
5
+ * 顺序(与研发面干跑基准同一条代码路径——基准验证的就是这里执行的):
6
+ * ⓪ --feasibility 时先跑 feasibility --gate(红=exit 5 停下让你定改道/尽力而为;黄=记录继续,逐项处置归 2b)
7
+ * ① setup-arkts-test(hypium 脚手架)② convert-resources ③ wire-routes(--src 三轨合并)+ main_pages 并入
8
+ * ④ transpile-layout(参考件,落 spec/front/layouts,不进编译树——实证 45 工程 244/244 份骨架从未被页面 import,进工程只产死代码与悬空立案)
9
+ * ⑤ align-sdk(设备就绪 + API 对齐)⑥ ensure-sign ⑦ hvigorw assembleHap
10
+ * ⑧ closure-suite --stage slice(转译布局的资源悬空在前场清零)⑨ smoke(首屏冒烟)
11
+ * 每步退出码+摘要进 <spec>/front/front-report.json;某步失败不掩盖后续(构建红则跳过冒烟)。
12
+ * 单个脚本保持可独立重跑(定向修完只重跑该步),本脚本不改任何一步的判定逻辑,各步退出码原样入报告。
13
+ * 阶段一不必再单独跑 align-sdk / ensure-sign(实证一轮里被跑了两遍):建好骨架工程后直接跑本命令。
14
+ *
15
+ * 用法:node front.mjs --src <安卓根> --dst <鸿蒙工程根> [--feasibility] [--spec <dst>/spec] [--extra-res a,b] [--no-sign] [--no-smoke] [--json out]
16
+ * 退出码:0 全绿(冒烟 scaffold 属预期)/ 5 可行性红(未进后续步)/ 10 构建红或闭合硬违规 / 2 冒烟立案(崩溃除外)/ 3 环境阻塞 / 1 某步脚本异常
17
+ */
18
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'node:fs';
19
+ import { resolveHvigorw, quoteIfSpace } from './lib/scan.mjs';
20
+ import { join, resolve, dirname } from 'node:path';
21
+ import { spawnSync } from 'node:child_process';
22
+ import { fileURLToPath } from 'node:url';
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 SRC = argOf('--src', null), DST = argOf('--dst', null);
29
+ if (!SRC || !DST) { console.error('usage: front.mjs --src <android root> --dst <harmony root> [--feasibility] [--spec dir] [--extra-res a,b] [--no-sign] [--no-smoke] [--json out]'); process.exit(3); }
30
+ const SPEC = argOf('--spec', join(DST, 'spec'));
31
+ const EXTRA = argOf('--extra-res', null);
32
+ const NO_SIGN = argv.includes('--no-sign'), NO_SMOKE = argv.includes('--no-smoke'), FEAS = argv.includes('--feasibility');
33
+ const JSON_OUT = argOf('--json', join(SPEC, 'front', 'front-report.json'));
34
+ mkdirSync(join(SPEC, 'front'), { recursive: true });
35
+ mkdirSync(join(SPEC, 'smoke'), { recursive: true });
36
+
37
+ // 入口模块目录从 build-profile 派生(modules[].srcPath 中 module.json5 type=entry),不写死 entry
38
+ function entryModule() {
39
+ try {
40
+ const bp = readFileSync(join(DST, 'build-profile.json5'), 'utf8').replace(/\/\/.*$/gm, '');
41
+ for (const m of bp.matchAll(/["']?srcPath["']?\s*:\s*["']([^"']+)["']/g)) {
42
+ const mj = join(DST, m[1], 'src', 'main', 'module.json5');
43
+ if (existsSync(mj) && /["']?type["']?\s*:\s*["']entry["']/.test(readFileSync(mj, 'utf8'))) return m[1].replace(/^\.\//, '');
44
+ }
45
+ } catch { /* 回退默认 */ }
46
+ return 'entry';
47
+ }
48
+ const MOD = entryModule();
49
+ const ETS = join(DST, MOD, 'src', 'main', 'ets');
50
+ const strip = (s) => String(s || '').replace(/\x1b\[[0-9;]*m/g, '');
51
+ const tail = (s, n = 3) => strip(s).trim().split('\n').filter(Boolean).slice(-n).join(' | ').slice(0, 300);
52
+
53
+ const steps = [];
54
+ function run(name, cmd, args, opts = {}) {
55
+ const t0 = Date.now();
56
+ const r = spawnSync(cmd, args, { encoding: 'utf8', timeout: opts.timeout || 600000, cwd: opts.cwd, shell: opts.shell || false, env: process.env });
57
+ const out = (r.stdout || '') + (r.stderr || '');
58
+ // status===null 有两种:超时被杀,或根本没起来(ENOENT:hvigorw/node 不在 PATH)——后者按 timeout 报会把环境问题写成"构建超时"
59
+ const st = { name, exit: r.status === null ? (r.error && r.error.code ? `spawn:${r.error.code}` : 'timeout') : r.status, seconds: Math.round((Date.now() - t0) / 1000), tail: tail(out) || (r.error ? String(r.error.message || r.error) : ''), ...(opts.keep ? { lines: strip(out).trim().split('\n').filter(l => opts.keep.test(l)).slice(0, 40) } : {}) };
60
+ steps.push(st);
61
+ console.log(`[front] ${st.exit === 0 ? '✓' : '▲'} ${name} exit=${st.exit} ${st.seconds}s${st.exit !== 0 ? ' — ' + st.tail : ''}`);
62
+ return st;
63
+ }
64
+ const node = (script, args, opts) => run(script.split('/').pop().replace('.mjs', ''), process.execPath, [join(HERE, script), ...args], opts);
65
+
66
+ // ⓪ 可行性:红(exit 5)就停——改道还是尽力而为是人的决定,不在这里替人做;黄(4)记录继续,逐项处置归 2b
67
+ let feas = null;
68
+ if (FEAS) {
69
+ mkdirSync(SPEC, { recursive: true });
70
+ feas = node('feasibility/feasibility.mjs', ['--src', SRC, '--json', join(SPEC, 'feasibility.json'), '--gate'], { timeout: 300000, keep: /^\s*(🟢|🟡|🔴|\?)/ });
71
+ if (feas.exit === 5) {
72
+ const report = { src: SRC, dst: DST, exit: 5, steps, next: '可行性红:先定改道或尽力而为(见 spec/feasibility.json),再不带 --feasibility 重跑本命令继续前场' };
73
+ mkdirSync(dirname(resolve(JSON_OUT)), { recursive: true }); writeFileSync(resolve(JSON_OUT), JSON.stringify(report, null, 2));
74
+ console.log(`[front] exit=5 · ${report.next}`); process.exit(5);
75
+ }
76
+ }
77
+ node('vectors/setup-arkts-test.mjs', ['--project', DST]);
78
+ node('convert/convert-resources.mjs', ['--src', SRC, '--dst', DST, '--out', join(SPEC, 'front'), ...(EXTRA ? ['--extra-res', EXTRA] : [])]);
79
+ const wr = node('wire/wire-routes.mjs', ['--src', SRC, '--out', ETS, '--json', join(SPEC, 'front', 'wiring-report.json')]);
80
+ try {
81
+ const gen = join(ETS, 'main_pages.json.generated');
82
+ if (existsSync(gen)) { const dst = join(DST, MOD, 'src', 'main', 'resources', 'base', 'profile', 'main_pages.json'); mkdirSync(dirname(dst), { recursive: true }); copyFileSync(gen, dst); }
83
+ } catch (e) { steps.push({ name: 'main_pages', exit: 1, tail: String(e.message) }); }
84
+ node('transpile/transpile-layout.mjs', ['--src', SRC, '--out', join(SPEC, 'front', 'layouts'), '--json', join(SPEC, 'front', 'transpile-report.json'), ...(EXTRA ? ['--extra-res', EXTRA] : [])]);
85
+ node('smoke/align-sdk.mjs', ['--project', DST], { timeout: 300000 });
86
+ if (!NO_SIGN) node('smoke/ensure-sign.mjs', ['--project', DST], { timeout: 400000 });
87
+ const HVIGORW = resolveHvigorw(DST); // 工程本地 → PATH → DevEco 安装根;报告里记下解析结果,后续 args 的 build/selfTest 照抄这个命令
88
+ const build = run('build', quoteIfSpace(HVIGORW), ['assembleHap', '--no-daemon'], { cwd: DST, shell: process.platform === 'win32', timeout: 900000, keep: /Error Message|ERROR:/ });
89
+ const closure = node('closure/closure-suite.mjs', ['--project', DST, '--stage', 'slice', '--json', join(SPEC, 'front', 'closure-slice.json')], { keep: /^\s*✗/ });
90
+ let smoke = null;
91
+ if (!NO_SMOKE && build.exit === 0) smoke = node('smoke/smoke.mjs', ['--project', DST, '--out', join(SPEC, 'smoke'), '--json', join(SPEC, 'smoke', 'smoke-2a.json')], { timeout: 600000, keep: /^\s*[✓▲✗]|\[smoke\]/ });
92
+
93
+ // 汇总:构建/闭合是硬门;冒烟 3=环境阻塞(如实记)、10=崩溃、2=立案;其余步骤非 0 记异常
94
+ let exit = 0;
95
+ const scriptFail = steps.filter(s => !['build', 'closure-suite', 'smoke', 'align-sdk', 'ensure-sign', 'feasibility'].includes(s.name) && s.exit !== 0);
96
+ if (build.exit !== 0 || closure.exit === 10) exit = 10;
97
+ else if (smoke && smoke.exit === 10) exit = 10;
98
+ else if (smoke && smoke.exit === 3) exit = 3;
99
+ else if (smoke && smoke.exit === 2) exit = 2;
100
+ else if (scriptFail.length) exit = 1;
101
+ let wiring = null; try { const w = JSON.parse(readFileSync(join(SPEC, 'front', 'wiring-report.json'), 'utf8')); wiring = { routes: w.routes.length, launcher: w.launcher, sources: w.sources }; } catch { /* wire 失败已在 steps */ }
102
+ const report = { src: SRC, dst: DST, module: MOD, exit, feasibility: feas ? feas.exit : null, wiring, steps, toolchain: { hvigorw: HVIGORW, buildCmd: `${quoteIfSpace(HVIGORW)} assembleHap --no-daemon` },
103
+ next: exit === 0 ? '前场绿:进 2b' : exit === 10 ? '先修构建红/闭合硬违规(转译布局悬空资源:补 --extra-res 或改 TODO(res)),只重跑涉事步骤' : exit === 3 ? '冒烟环境阻塞(设备/装机/hap):报告如实记"未冒烟",不当通过' : exit === 2 ? '冒烟立案:按 smoke 报告处置(崩溃/着陆错当场修)' : '某步脚本异常退出,见 steps.tail' };
104
+ mkdirSync(dirname(resolve(JSON_OUT)), { recursive: true });
105
+ writeFileSync(resolve(JSON_OUT), JSON.stringify(report, null, 2));
106
+ console.log(`[front] exit=${exit} · 路由 ${wiring ? wiring.routes + ' 首页 ' + wiring.launcher : '?'} · ${report.next} · 报告 ${JSON_OUT}`);
107
+ process.exit(exit);
@@ -0,0 +1,172 @@
1
+ /**
2
+ * ark-extract.mjs — ArkAnalyzer 抽取适配器(接口派生的 AST 级增强,正则的兜底超集)。
3
+ *
4
+ * 契约靠派生:正则抽取有盲区(export async function / export type{}from / export * / 字符串内假符号),
5
+ * ArkAnalyzer 建 SceneGraph 后拿真实导出/种类/跨文件 import 解析。本适配器只做一件事:
6
+ * extractSymbols(project, dirs) → { symbols:{name:{kind,file}}, fileCount, engine } 或 null(不可用/出错)。
7
+ * 由 interface.mjs 与正则基线**并集**(正则恒跑保底,Ark 成功则并入其超集),故对 ArkAnalyzer 版本漂移免疫。
8
+ *
9
+ * 解析顺序(免装优先):环境变量 HMIGBOT_ARKANALYZER → DevEco Studio 自带 → 套件本地 → 工程本地 → 裸 require。
10
+ * 找不到或任一 API 抛错 → 返回 null → 上游纯回退正则。全程不污染 stdout(log4js 噪音静音)。
11
+ */
12
+ import { createRequire } from 'node:module';
13
+ import { resolve, relative, join } from 'node:path';
14
+ import { existsSync, realpathSync, readdirSync, readFileSync } from 'node:fs';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ const HERE = (await import('node:path')).dirname((await import('node:url')).fileURLToPath(import.meta.url));
18
+
19
+ // 枚举整数→名(不在顶层 export,按 .d.ts 定值硬编码;集中此处,版本升级只改这里)
20
+ // 3='local'(ArkAnalyzer 对 export const/let 的分类)统一回 'const',与正则词表一致(class/interface/enum/const/function/struct/type/namespace)
21
+ const EXPORT_TYPE = { 0: 'namespace', 1: 'class', 2: 'function', 3: 'const', 4: 'type', 9: 'unknown' };
22
+ const CLASS_CATEGORY = { 0: 'class', 1: 'struct', 2: 'interface', 3: 'enum', 4: 'type', 5: 'object', 6: 'union' };
23
+ const NAME_RE = /^[A-Za-z_$][\w$]*$/;
24
+
25
+ // 解析 arkanalyzer 包目录:DevEco 内置优先(用户偏好,免装)→ 环境覆盖 → 套件本地。命中路径缓存。
26
+ let _arkDir; // undefined=未探测 / null=确认无 / string=命中路径
27
+ function arkPackageDir() {
28
+ if (_arkDir !== undefined) return _arkDir;
29
+ if (process.env.HMIGBOT_NO_ARKANALYZER) { _arkDir = null; return null; } // 显式关闭:强制纯正则
30
+ const cands = [
31
+ process.env.HMIGBOT_ARKANALYZER, // 显式覆盖(最高优先)
32
+ ...devecoStudioArkDirs(), // DevEco Studio 内置(免装,首选)
33
+ join(HERE, '..', '..', 'node_modules', 'arkanalyzer'), // 套件本地(仅当显式装过)
34
+ ].filter(Boolean);
35
+ for (const c of cands) { try { if (existsSync(join(c, 'package.json'))) { _arkDir = c; return c; } } catch { /* next */ } }
36
+ _arkDir = null; return null;
37
+ }
38
+
39
+ // DevEco Studio 安装根候选(跨平台:mac/win/linux;DEVECO_STUDIO_HOME 覆盖最高优先)
40
+ function devecoStudioBases() {
41
+ const bases = [];
42
+ if (process.env.DEVECO_STUDIO_HOME) bases.push(process.env.DEVECO_STUDIO_HOME);
43
+ const HOME = process.env.HOME || process.env.USERPROFILE;
44
+ if (process.platform === 'darwin') {
45
+ bases.push('/Applications/DevEco-Studio.app');
46
+ if (HOME) bases.push(join(HOME, 'Applications', 'DevEco-Studio.app'));
47
+ } else if (process.platform === 'win32') {
48
+ for (const pf of [process.env.ProgramFiles, process.env['ProgramFiles(x86)'], process.env.LOCALAPPDATA]) {
49
+ if (pf) bases.push(join(pf, 'Huawei', 'DevEco Studio'));
50
+ }
51
+ } else { // linux 及其他
52
+ bases.push('/opt/deveco-studio', '/usr/local/deveco-studio');
53
+ if (HOME) bases.push(join(HOME, 'deveco-studio'));
54
+ }
55
+ return bases;
56
+ }
57
+
58
+ // DevEco Studio 内置 arkanalyzer(跨平台 + 跨版本鲁棒):已知子路径命中即用,失效则在 codelinter 插件下定深兜底搜。
59
+ // mac 插件在 Contents/ 下,win/linux 直接在安装根下——两种前缀都试。
60
+ function devecoStudioArkDirs() {
61
+ const SUB = ['plugins', 'codelinter', 'arkPerfCheck', 'node_modules', 'arkanalyzer'];
62
+ const out = [];
63
+ for (const b of devecoStudioBases()) {
64
+ for (const mid of [[], ['Contents']]) {
65
+ const known = join(b, ...mid, ...SUB);
66
+ if (existsSync(known)) { out.push(known); continue; }
67
+ try { out.push(...shallowFindArk(join(b, ...mid, 'plugins', 'codelinter'), 4)); } catch { /* ignore */ }
68
+ }
69
+ }
70
+ return out;
71
+ }
72
+
73
+ function shallowFindArk(dir, depth) {
74
+ const hits = [];
75
+ if (depth < 0 || !existsSync(dir)) return hits;
76
+ let entries = [];
77
+ try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return hits; }
78
+ for (const e of entries) {
79
+ if (!e.isDirectory()) continue;
80
+ if (e.name === 'arkanalyzer') hits.push(join(dir, e.name));
81
+ else if (depth > 0) hits.push(...shallowFindArk(join(dir, e.name), depth - 1));
82
+ }
83
+ return hits;
84
+ }
85
+
86
+ /** require arkanalyzer 模块(DevEco 内置优先),找不到/加载失败返回 null。 */
87
+ export function loadArkAnalyzer() {
88
+ const dir = arkPackageDir();
89
+ if (!dir) return null;
90
+ try { return require(dir); } catch { return null; }
91
+ }
92
+
93
+ /** 命中的 arkanalyzer 版本(用于 manifest 记账),取不到返回 undefined。 */
94
+ export function arkVersion() {
95
+ const dir = arkPackageDir();
96
+ if (!dir) return undefined;
97
+ try { return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).version; } catch { return undefined; }
98
+ }
99
+
100
+ function refineKind(e) {
101
+ try {
102
+ const ax = e.getArkExport?.();
103
+ if (ax && typeof ax.getCategory === 'function') { const c = ax.getCategory(); if (CLASS_CATEGORY[c]) return CLASS_CATEGORY[c]; }
104
+ if (ax && ax.constructor?.name === 'ArkMethod') return 'function';
105
+ if (ax && ax.constructor?.name === 'ArkNamespace') return 'namespace';
106
+ } catch { /* ignore */ }
107
+ const t = e.getExportClauseType?.();
108
+ return EXPORT_TYPE[t] ?? 'unknown';
109
+ }
110
+
111
+ /**
112
+ * 用 ArkAnalyzer 提取 project 下、位于 dirs 任一目录内的文件的导出符号。
113
+ * @returns {{symbols:Object, fileCount:number, engine:string, arkVersion?:string}|null}
114
+ */
115
+ export function extractSymbols(project, dirs) {
116
+ const ark = loadArkAnalyzer();
117
+ if (!ark) return null;
118
+ const { SceneConfig, Scene } = ark;
119
+ if (typeof SceneConfig !== 'function' || typeof Scene !== 'function') return null;
120
+
121
+ // log4js 噪音静音:建图期屏蔽 console.log/info(业务无输出走这里)
122
+ const savedLog = console.log, savedInfo = console.info;
123
+ console.log = () => {}; console.info = () => {};
124
+ try {
125
+ // realpath 归一:ArkAnalyzer 返回真实路径,projRoot 若在符号链接下(如 macOS /tmp→/private/var)
126
+ // 会让 relative() 算出 ../.. 前缀导致 inScope 全落空、静默回退。两端归一即修。
127
+ let projRoot;
128
+ try { projRoot = realpathSync(resolve(project)); } catch { projRoot = resolve(project); }
129
+ const config = new SceneConfig();
130
+ config.buildFromProjectDir(projRoot);
131
+ const scene = new Scene();
132
+ scene.buildSceneFromProjectDir(config);
133
+ try { scene.inferTypes(); } catch { /* SDK 缺失可忽略,仅影响 @ 依赖类型 */ }
134
+
135
+ const files = scene.getFiles?.() || [];
136
+ if (!files.length) return null; // 建图空 = 视为不可用,回退正则
137
+
138
+ const dirPrefixes = (dirs || []).map(d => d.replace(/^\.\//, '').replace(/\\/g, '/').replace(/\/$/, ''));
139
+ const inScope = (relPath) => dirPrefixes.length === 0 || dirPrefixes.some(d => relPath === d || relPath.startsWith(d + '/'));
140
+
141
+ const symbols = {};
142
+ let fileCount = 0;
143
+ for (const f of files) {
144
+ let fp;
145
+ try {
146
+ let abs = f.getFilePath();
147
+ try { abs = realpathSync(abs); } catch { /* 用原路径 */ }
148
+ fp = relative(projRoot, abs).replace(/\\/g, '/');
149
+ } catch { continue; }
150
+ if (!inScope(fp)) continue;
151
+ fileCount++;
152
+ let exps = [];
153
+ try { exps = f.getExportInfos?.() || []; } catch { exps = []; }
154
+ for (const e of exps) {
155
+ let name;
156
+ try { name = e.getExportClauseName?.() ?? e.getOriginName?.(); } catch { name = null; }
157
+ if (!name) continue;
158
+ if (name === 'default' || name === '%dflt') {
159
+ try { const bn = e.getArkExport?.()?.getName?.(); if (bn && bn !== '%dflt') name = bn; } catch { /* ignore */ }
160
+ }
161
+ if (!NAME_RE.test(name)) continue; // 防御脏名
162
+ if (!symbols[name]) symbols[name] = { kind: refineKind(e), file: fp };
163
+ }
164
+ }
165
+ if (Object.keys(symbols).length === 0) return null; // 一个都没提到 = 视为失败,回退
166
+ return { symbols, fileCount, engine: 'arkanalyzer', arkVersion: arkVersion() };
167
+ } catch {
168
+ return null; // 任一步抛错 → 回退正则
169
+ } finally {
170
+ console.log = savedLog; console.info = savedInfo;
171
+ }
172
+ }
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * interface.mjs — 跨切片接口的"派生 + 一致性检查"(契约靠派生不靠预测,见
4
+ * docs/design-2026-09-06-derived-contract.md)。
5
+ *
6
+ * 病根:Android→HarmonyOS 的跨切片接口无法提前准确预测,模型预测的契约会带错一路向下。
7
+ * 对策:地基先建,从地基**真实代码**提取导出符号成 interface-manifest.json(派生,不是预测);
8
+ * 下游切片只准 import 这份 manifest 里的符号;一致性检查把"契约冲突"从末端 verify
9
+ * 降到检查点的确定性门(更早、更廉价、可重跑同答案)。
10
+ *
11
+ * 子命令:
12
+ * extract --project <工程> --dirs <地基目录,逗号分隔> --out spec/interface-manifest.json
13
+ * → 遍历地基目录 .ets,提取 export 符号(class/interface/enum/const/function/struct/type)
14
+ * check --project <工程> --manifest spec/interface-manifest.json [--slice <目录>] [--json out]
15
+ * → 下游文件对地基模块的 import 必须解析到 manifest 符号;否则 violation(下游假设了
16
+ * 地基没提供的接口=发散)。退出码 0 通过 / 10 有违规 / 3 用法错。
17
+ *
18
+ * 契约演进:地基接口要改时 extract 重跑(manifest 版本自然更新),受影响下游由台账 debts
19
+ * 定向复验——受控传播,不是冻死。
20
+ */
21
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
22
+ import { join, dirname, resolve, relative } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
24
+ import { stripComments, walkFiles } from '../lib/scan.mjs';
25
+ import { extractSymbols as arkExtractSymbols } from './ark-extract.mjs';
26
+
27
+ const argv = process.argv.slice(2);
28
+ const CMDS = ['extract', 'check'];
29
+ const cmd = argv.find(a => CMDS.includes(a));
30
+ const argOf = (n, d) => { const i = argv.indexOf(n); return i >= 0 && argv[i + 1] ? argv[i + 1] : d; };
31
+ const PROJECT = argOf('--project', null);
32
+ if (!cmd || !PROJECT) { console.error('usage: interface.mjs <extract|check> --project <root> ...'); process.exit(3); }
33
+ const rel = f => relative(resolve(PROJECT), f).replace(/\\/g, '/');
34
+
35
+ // 导出声明:export [default] (class|interface|enum|const|function|struct|type|namespace) Name
36
+ // 含 @Component 前缀的 struct(export @Component struct X 不合法,ArkTS 是 export struct X)。
37
+ const EXPORT_RE = /\bexport\s+(?:default\s+)?(?:declare\s+)?(?:abstract\s+)?(?:async\s+)?(class|interface|enum|const|let|var|function|struct|type|namespace)\s+([A-Za-z_$][\w$]*)/g; // async function(语料 45 工程 108 处)/var 也认
38
+ // 具名再导出:export { A, B as C } from '...'
39
+ const REEXPORT_RE = /\bexport\s*\{([^}]+)\}/g;
40
+
41
+ function extractExports(code) {
42
+ const out = [];
43
+ for (const m of code.matchAll(EXPORT_RE)) out.push({ name: m[2], kind: m[1] === 'let' ? 'const' : m[1] });
44
+ for (const m of code.matchAll(REEXPORT_RE)) {
45
+ for (const part of m[1].split(',')) {
46
+ const nm = part.trim().split(/\s+as\s+/).pop().trim();
47
+ if (/^[A-Za-z_$][\w$]*$/.test(nm)) out.push({ name: nm, kind: 'reexport' });
48
+ }
49
+ }
50
+ return out;
51
+ }
52
+
53
+ if (cmd === 'extract') {
54
+ const dirs = (argOf('--dirs', '') || '').split(',').map(s => s.trim()).filter(Boolean);
55
+ if (!dirs.length) { console.error('extract 需 --dirs <地基目录,..>'); process.exit(3); }
56
+ const symbols = {};
57
+ let fileCount = 0;
58
+ // ① 正则基线(零依赖保底,恒跑)——即便 ArkAnalyzer 缺失/版本不符也保证不劣于旧版
59
+ for (const d of dirs) {
60
+ const root = resolve(PROJECT, d);
61
+ for (const f of walkFiles(root, { match: (_p, e) => e.endsWith('.ets') || e.endsWith('.ts') })) {
62
+ fileCount++;
63
+ const code = stripComments(readFileSync(f, 'utf8'));
64
+ for (const s of extractExports(code)) {
65
+ if (!symbols[s.name]) symbols[s.name] = { kind: s.kind, file: rel(f) };
66
+ }
67
+ }
68
+ }
69
+ // ② ArkAnalyzer 超集增强(AST 级,可用则并入):补正则盲区(export async function /
70
+ // export type{}from / export * / 字符串内假符号误报),并给精确种类。名覆盖是并集,故恒 ≥ 正则。
71
+ let engine = 'regex', arkAdded = 0, arkVer;
72
+ try {
73
+ const ark = arkExtractSymbols(PROJECT, dirs);
74
+ if (ark && ark.symbols && Object.keys(ark.symbols).length) {
75
+ engine = 'arkanalyzer+regex';
76
+ arkVer = ark.arkVersion;
77
+ for (const [name, info] of Object.entries(ark.symbols)) {
78
+ if (!symbols[name]) arkAdded++;
79
+ symbols[name] = info; // Ark 优先(种类更准);正则独有的名保留 → 并集
80
+ }
81
+ }
82
+ } catch { /* 保持正则基线,静默回退 */ }
83
+ const manifest = { version: '0.1.0', engine, ...(arkVer ? { arkVersion: arkVer } : {}), sources: dirs, fileCount, symbolCount: Object.keys(symbols).length, symbols };
84
+ const out = argOf('--out', null);
85
+ if (out) { mkdirSync(dirname(resolve(out)), { recursive: true }); writeFileSync(out, JSON.stringify(manifest, null, 2)); }
86
+ console.log(`[interface:extract] 地基 ${fileCount} 文件 → 导出符号 ${manifest.symbolCount} 个(引擎 ${engine}${engine !== 'regex' ? `,AST 补 ${arkAdded} 个正则盲区符号` : ',ArkAnalyzer 不可达'})${out ? ` → ${out}` : ''}`);
87
+ const bykind = {};
88
+ for (const s of Object.values(symbols)) bykind[s.kind] = (bykind[s.kind] || 0) + 1;
89
+ console.log(` ${Object.entries(bykind).map(([k, n]) => `${k}:${n}`).join(' ')}`);
90
+ } else if (cmd === 'check') {
91
+ const manifestPath = argOf('--manifest', null);
92
+ if (!manifestPath) { console.error('check 需 --manifest'); process.exit(3); }
93
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
94
+ const known = new Set(Object.keys(manifest.symbols || {}));
95
+ // 地基源文件(manifest 里记录的文件)——这些文件自身不算"下游"
96
+ const foundationFiles = new Set(Object.values(manifest.symbols || {}).map(s => s.file));
97
+ const foundationDirs = (manifest.sources || []).map(d => d.replace(/\\/g, '/'));
98
+ const isFoundation = (relPath) => foundationFiles.has(relPath) || foundationDirs.some(d => relPath.startsWith(d.replace(/^\.\//, '')));
99
+
100
+ const sliceDir = argOf('--slice', null);
101
+ const scanRoot = sliceDir ? resolve(PROJECT, sliceDir) : resolve(PROJECT);
102
+ const findings = [];
103
+ let importsChecked = 0;
104
+ // 目标文件真实导出(按需提取,缓存):manifest 是派生时刻的快照,切片后来在地基目录里新建的文件、
105
+ // 地基新增的导出都不在里面——这不是下游漂移,是 manifest 过期,两者要分开判。
106
+ const exportCache = new Map();
107
+ const realExportsOf = (target) => {
108
+ const file = [target + '.ets', target + '.ts', join(target, 'index.ets')].find(p => existsSync(p));
109
+ if (!file) return null;
110
+ if (!exportCache.has(file)) exportCache.set(file, { file, names: new Set(extractExports(stripComments(readFileSync(file, 'utf8'))).map(s => s.name)) });
111
+ return exportCache.get(file);
112
+ };
113
+ const staleFiles = new Set();
114
+ for (const f of walkFiles(scanRoot, { match: (_p, e) => e.endsWith('.ets') })) {
115
+ const relf = rel(f);
116
+ if (isFoundation(relf)) continue; // 地基自身不查
117
+ const code = stripComments(readFileSync(f, 'utf8'));
118
+ for (const m of code.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"](\.[^'"]+)['"]/g)) {
119
+ // 只查指向地基模块的 import:解析路径落在地基目录/文件内
120
+ const target = resolve(dirname(f), m[2]);
121
+ const targetRel = rel(target) + '.ets';
122
+ const pointsToFoundation = foundationDirs.some(d => rel(target).startsWith(d.replace(/^\.\//, ''))) || foundationFiles.has(targetRel);
123
+ if (!pointsToFoundation) continue;
124
+ for (const raw of m[1].split(',')) {
125
+ const nm = raw.trim().split(/\s+as\s+/)[0].trim();
126
+ if (!/^[A-Za-z_$][\w$]*$/.test(nm)) continue;
127
+ importsChecked++;
128
+ if (known.has(nm)) continue;
129
+ // manifest 没有:核对目标文件——真实导出 → manifest 过期(立案,按文件一次);不存在/未导出 → 真漂移(违规)
130
+ const real = realExportsOf(target);
131
+ if (real && real.names.has(nm)) { staleFiles.add(rel(real.file)); continue; }
132
+ findings.push({ severity: 'violation', type: 'interface_drift', name: nm, evidence: `${relf}: import { ${nm} } from '${m[2]}' —— 地基未导出该符号(下游假设了不存在的接口;地基改了就 extract 重派生,或修下游对齐)` });
133
+ }
134
+ }
135
+ }
136
+ for (const sf of staleFiles) {
137
+ findings.push({ severity: 'case', type: 'manifest_stale', name: sf,
138
+ basis: '判据:下游 import 的符号确在该文件真实导出,只是不在 manifest 快照里(地基增长或切片在地基目录内新建文件)——不是下游漂移',
139
+ evidence: `${sf} 的导出不在 manifest 里——重跑 interface.mjs extract 重派生即可;下游 import 与真实导出一致,不判漂移` });
140
+ }
141
+ const report = { project: relative(process.cwd(), resolve(PROJECT)) || '.', manifestSymbols: known.size, importsChecked, findings };
142
+ const outJson = argOf('--json', null);
143
+ if (outJson) writeFileSync(outJson, JSON.stringify(report, null, 2));
144
+ const vio = findings.filter(f => f.severity === 'violation');
145
+ const cas = findings.filter(f => f.severity === 'case');
146
+ console.log(`[interface:check] 地基符号 ${known.size} · 下游对地基 import ${importsChecked} 处 · 漂移 ${vio.length}${cas.length ? ` · manifest 过期文件 ${cas.length}(立案)` : ''}`);
147
+ for (const v of vio.slice(0, 15)) console.log(` ✗ ${v.evidence}`);
148
+ if (vio.length > 15) console.log(` …共 ${vio.length} 条`);
149
+ for (const c of cas.slice(0, 8)) console.log(` ▲ CASE ${c.type} — ${c.evidence}`);
150
+ if (!vio.length) console.log(' ✓ 下游全部对齐地基派生接口(零漂移)');
151
+ process.exit(vio.length ? 10 : 0);
152
+ }