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.
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,258 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * extractors.mjs — 路由真源提取器(三轨之二:navGraph / Compose;manifest 轨在 wire-routes 主体)。
4
+ *
5
+ * 安卓导航真源按应用形态分三轨(形态画像由 feasibility 产出):
6
+ * ① 多 Activity:AndroidManifest(wire-routes 原生)
7
+ * ② Jetpack Navigation:res/navigation/*.xml 配置(本文件 extractNavGraph——配置化最可靠)
8
+ * ③ 单 Activity + Compose:NavHost { composable("route") } 代码(本文件 extractCompose)
9
+ * Fragment 手动事务等散代码形态无静态真源 → 走台账 pages 账兜底(wire-routes --from-ledger)。
10
+ * 输出统一 {route, fqcn, launcher} 形状(与 manifest 轨一致),路由名规范化为标识符安全形式。
11
+ */
12
+
13
+ /** 路由名规范化:路径参数剥离(details/{id}→Details)、分隔符驼峰、首字母大写、非法字符清除。 */
14
+ export function normalizeRoute(raw) {
15
+ const base = String(raw).split('?')[0].split('/').filter(s => s && !/^\{.*\}$/.test(s)).join('_');
16
+ const cleaned = base.replace(/\{[^}]*\}/g, '').replace(/[^\w]+/g, '_').replace(/^_+|_+$/g, '');
17
+ return cleaned.split('_').filter(Boolean).map(s => s[0].toUpperCase() + s.slice(1)).join('') || 'Route';
18
+ }
19
+
20
+ /**
21
+ * ② Jetpack Navigation:解析 res/navigation/*.xml。
22
+ * <navigation app:startDestination="@id/homeFragment"> <fragment android:id="@+id/homeFragment" android:name="com.x.HomeFragment"/>
23
+ * dialog/activity 目的地同收。返回 [{route, fqcn, launcher}]。
24
+ */
25
+ export function extractNavGraph(xml) {
26
+ const text = xml.replace(/<!--[\s\S]*?-->/g, '');
27
+ const start = (text.match(/startDestination\s*=\s*"@\+?id\/([^"]+)"/) || [])[1] || null;
28
+ const out = [];
29
+ for (const m of text.matchAll(/<(fragment|dialog|activity)\b([^>]*)>/g)) {
30
+ const attrs = m[2];
31
+ const id = (attrs.match(/android:id\s*=\s*"@\+?id\/([^"]+)"/) || [])[1];
32
+ const name = (attrs.match(/android:name\s*=\s*"([^"]+)"/) || [])[1] || '';
33
+ if (!id) continue;
34
+ out.push({ route: normalizeRoute(id), fqcn: name || `navgraph:${id}`, launcher: id === start });
35
+ }
36
+ return out;
37
+ }
38
+
39
+ /**
40
+ * ③ Compose Navigation:解析含 NavHost 的 Kotlin 文本。
41
+ * 认三种注册形态:composable("route/{id}") / composable<TypedRoute>() / dialog("route");
42
+ * startDestination = "route" | TypedRoute 判 launcher。串内路由为真源;typed route 取类型名。
43
+ */
44
+ export function extractCompose(kt) {
45
+ const text = kt.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
46
+ // startDestination(Navigation 2)| rememberNavBackStack(<首键>)(Navigation 3,无 startDestination)判 launcher
47
+ const start = (text.match(/startDestination\s*=\s*"([^"]+)"/) || text.match(/startDestination\s*=\s*([A-Z][\w.]*)/)
48
+ || text.match(/rememberNavBackStack\s*\(\s*([A-Za-z_][\w.]*)/) || [])[1] || null;
49
+ const out = []; const seen = new Set();
50
+ const push = (rawRoute, fqcn, rawForLauncher) => {
51
+ const route = normalizeRoute(rawRoute);
52
+ if (seen.has(route)) return; seen.add(route);
53
+ out.push({ route, fqcn, launcher: start !== null && rawForLauncher === start });
54
+ };
55
+ for (const m of text.matchAll(/\b(?:composable|dialog|bottomSheet)\s*\(\s*(?:route\s*=\s*)?"([^"]+)"/g)) {
56
+ push(m[1], `compose:"${m[1]}"`, m[1]);
57
+ }
58
+ for (const m of text.matchAll(/\b(?:composable|dialog|entry)\s*<\s*([A-Za-z_][\w.]*)\s*>/g)) {
59
+ push(m[1].replace(/(NavKey|Route)$/, "") || m[1], `compose:<${m[1]}>`, m[1]);
60
+ }
61
+ return out;
62
+ }
63
+
64
+ /**
65
+ * ③b Compose 工程级提取(常量引用式实证形态):composable(Destinations.RECIPE_LIST) 散布在
66
+ * 各页面文件、常量定义在别处——两遍扫:先收全工程 `const val NAME = "串"` 表(Container.NAME
67
+ * 与 NAME 双键),再解 composable(引用/字面量) 与 startDestination。files: [{path, text}]。
68
+ */
69
+ export function extractComposeProject(files, opts = {}) {
70
+ const strip = (t) => t.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
71
+ const consts = new Map();
72
+ for (const { text } of files) {
73
+ const t = strip(text);
74
+ for (const m of t.matchAll(/\bconst\s+val\s+(\w+)\s*=\s*"([^"]+)"/g)) consts.set(m[1], m[2]);
75
+ }
76
+ const deref = (ref) => consts.get(ref.split('.').pop()) ?? null;
77
+ let start = null;
78
+ const cands = []; const seen = new Set();
79
+ const push = (raw, fqcn, launchKey) => {
80
+ const route = normalizeRoute(raw);
81
+ if (seen.has(route)) return; seen.add(route);
82
+ cands.push({ route, fqcn, launchKey });
83
+ };
84
+ // startDestination 归属:多模块工程里各特性各有 NavHost,"第一个遇到的 startDestination"是任意的;
85
+ // 顶层宿主文件(opts.hostFiles,由 launcher activity 两跳定位)先扫,它的起点才是应用首页。
86
+ const hostSet = new Set(opts.hostFiles || []);
87
+ const ordered = hostSet.size ? [...files.filter(f => hostSet.has(f.path)), ...files.filter(f => !hostSet.has(f.path))] : files;
88
+ const stripped = ordered.map(f => strip(f.text));
89
+ // Navigation 3 经包装函数起栈:rememberNavBackStack(param) 且 param 是所在函数形参 → 到调用点取实参(命名或按位)。
90
+ // 这是 Kotlin 形参转发的通用解析,不是某个工程的特判;解不到就交给下一条规则。
91
+ const forwarded = (t, ident, idx) => {
92
+ const before = t.slice(0, idx);
93
+ const fi = before.lastIndexOf('fun ');
94
+ if (fi < 0) return null;
95
+ const open = before.indexOf('(', fi); if (open < 0) return null;
96
+ const fname = before.slice(fi + 4, open).trim().split(/\s+/).pop();
97
+ const params = balancedArgs(t, open).map(p => p.split(':')[0].trim());
98
+ const pi = params.indexOf(ident); if (pi < 0) return null;
99
+ const callRe = new RegExp(`\\b${fname}\\s*\\(`, 'g');
100
+ for (const t2 of stripped) {
101
+ let c;
102
+ while ((c = callRe.exec(t2)) !== null) {
103
+ if (t2.slice(Math.max(0, c.index - 4), c.index).includes('fun ')) continue; // 跳过定义本身
104
+ const args = balancedArgs(t2, c.index + c[0].length - 1);
105
+ const named = args.find(a => new RegExp(`^${ident}\\s*=`).test(a));
106
+ const val = (named ? named.split('=').slice(1).join('=') : args[pi] || '').trim();
107
+ if (/^[A-Za-z_][\w.]*$/.test(val)) return val;
108
+ const lit = val.match(/^"([^"]+)"$/); if (lit) return lit[1];
109
+ }
110
+ }
111
+ return null;
112
+ };
113
+ for (const t of stripped) {
114
+ const s = t.match(/startDestination\s*=\s*"([^"]+)"/) || t.match(/startDestination\s*=\s*([A-Za-z_][\w.]*)/);
115
+ if (s && start === null) start = s[1].includes('"') ? s[1] : (deref(s[1]) ?? s[1]);
116
+ // Navigation 3:无 startDestination,首页 = rememberNavBackStack(<首个 NavKey>) 的初始键(typed 形态,与 entry<Key> 同键匹配)
117
+ const n3 = t.match(/rememberNavBackStack\s*\(\s*([A-Za-z_][\w.]*)/);
118
+ if (n3 && start === null) start = forwarded(t, n3[1], n3.index) ?? n3[1];
119
+ for (const m of t.matchAll(/\b(?:composable|dialog|bottomSheet)\s*\(\s*(?:route\s*=\s*)?"([^"]+)"/g)) push(m[1], `compose:"${m[1]}"`, m[1]);
120
+ for (const m of t.matchAll(/\b(?:composable|dialog|bottomSheet)\s*\(\s*(?:route\s*=\s*)?([A-Za-z_][\w.]*)\s*[,)]/g)) {
121
+ const lit = deref(m[1]);
122
+ if (lit !== null) push(lit, `compose:${m[1]}="${lit}"`, lit);
123
+ }
124
+ for (const m of t.matchAll(/\b(?:composable|dialog|entry)\s*<\s*([A-Za-z_][\w.]*)\s*>/g)) push(m[1].replace(/(NavKey|Route)$/, "") || m[1], `compose:<${m[1]}>`, m[1]);
125
+ }
126
+ return cands.map(c => ({ route: c.route, fqcn: c.fqcn, launcher: start !== null && c.launchKey === start }));
127
+ }
128
+
129
+ /** 括号平衡的实参/形参切分:text[openIdx]==='(',按顶层逗号切,遇匹配的 ')' 结束(泛型尖括号一并计深) */
130
+ function balancedArgs(text, openIdx) {
131
+ const out = []; let depth = 0, cur = '';
132
+ for (let i = openIdx; i < text.length; i++) {
133
+ const ch = text[i];
134
+ if ('({[<'.includes(ch)) { depth++; if (depth === 1) continue; }
135
+ else if (')}]>'.includes(ch)) { depth--; if (depth === 0) { if (cur.trim()) out.push(cur.trim()); return out; } }
136
+ else if (ch === ',' && depth === 1) { out.push(cur.trim()); cur = ''; continue; }
137
+ cur += ch;
138
+ }
139
+ return out;
140
+ }
141
+
142
+ /**
143
+ * 顶层 NavHost 宿主定位:从 launcher activity 文件出发,沿"调用的 @Composable → 定义它的文件"最多走 maxHops 跳,
144
+ * 含 NavHost( / NavDisplay( 的文件即宿主(setContent { App() } → App.kt 里的 NavHost 是常见形态)。
145
+ * 找不到宿主 = launcher activity 不是 Compose 壳(经典多屏 Activity 内嵌特性级 NavHost),首页应保留为该 activity。
146
+ * files: [{path, text}];返回宿主文件路径数组。
147
+ */
148
+ export function findComposeHostFiles(files, activityPath, maxHops = 2) {
149
+ const HOST = /\bNavHost\s*\(|\bNavDisplay\s*\(/;
150
+ const strip = (t) => t.replace(/\/\/[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '');
151
+ const byPath = new Map(files.map(f => [f.path, strip(f.text)]));
152
+ const startText = byPath.get(activityPath);
153
+ if (startText === undefined) return [];
154
+ const hosts = new Set(); const visited = new Set([activityPath]);
155
+ if (HOST.test(startText)) hosts.add(activityPath);
156
+ let frontier = [activityPath];
157
+ for (let hop = 0; hop < maxHops && frontier.length; hop++) {
158
+ const next = [];
159
+ for (const p of frontier) {
160
+ const callees = [...new Set([...byPath.get(p).matchAll(/\b([A-Z][A-Za-z0-9_]*)\s*\(/g)].map(m => m[1]))];
161
+ if (!callees.length) continue;
162
+ const defRe = new RegExp(`\\bfun\\s+(?:${callees.join('|')})\\s*\\(`);
163
+ for (const [q, t] of byPath) {
164
+ if (visited.has(q) || !defRe.test(t)) continue;
165
+ visited.add(q); next.push(q);
166
+ if (HOST.test(t)) hosts.add(q);
167
+ }
168
+ }
169
+ frontier = next;
170
+ }
171
+ return [...hosts];
172
+ }
173
+
174
+ /**
175
+ * 应用级初始化真源。安卓 Application.onCreate 在任何页面之前必跑(数据库/偏好/通知渠道/DI…),
176
+ * 鸿蒙由 EntryAbility.onCreate 承担;规划按页面与后端域切片时这段横切初始化没有归属,漏接只在下游以别的错误露头
177
+ * (实证两应用崩溃)。这里只做确定性定位,不判怎么迁、不拆项:
178
+ * manifest <application android:name> → 类文件(按包声明精确匹配,工程内沿父类链最多 4 跳)→ 每级 onCreate 的文件与行范围;
179
+ * 另抽 ContentProvider 与 androidx.startup 初始化器(都是"启动即跑"的入口);DI 框架只做标注。
180
+ * 调用名清单(callHints)只是提示,不进台账:正则抽调用在不同语言习惯下有噪声,逐项拆解由 2b 读锚定的源码段完成。
181
+ * 入参:xml=manifest 文本(已去注释),files=[{path,text}] 工程内 .kt/.java。返回 null=无自定义 Application 且无 provider/startup。
182
+ */
183
+ export function extractAppInit(xml, files, opts = {}) {
184
+ // 多模块工程:Application 类常声明在 app 模块 manifest,而承载 LAUNCHER 的可能是别的模块(实证两工程)——
185
+ // 主 manifest 没有就按 extras(其余候选 manifest)顺序找;provider/startup 各 manifest 取并集(按名去重)。
186
+ const manifests = [{ path: opts.manifest || 'AndroidManifest.xml', text: xml }, ...((opts.extras || []).filter(e => e && e.text))];
187
+ const nameOf = (attrs) => (attrs.match(/android:name\s*=\s*["']([^"']+)["']/) || [])[1];
188
+ const attrsOf = (text, tag) => [...text.matchAll(new RegExp(`<${tag}\\b((?:"[^"]*"|'[^']*'|[^>"'])*)(/?)>`, 'g'))];
189
+ // 多产品仓(一个仓多个 app 模块各有 Application):全部候选如实列出,取首个(承载 launcher 的模块优先),ambiguous 标记交流程确认产品
190
+ let nameAttr = null, pkg = '', appManifest = null;
191
+ const applicationCandidates = [];
192
+ for (const mf of manifests) {
193
+ const app = attrsOf(mf.text, 'application')[0];
194
+ const nm = app ? nameOf(app[1]) : null;
195
+ if (!nm) continue;
196
+ const p = (mf.text.match(/\bpackage\s*=\s*["']([^"']+)["']/) || [])[1] || '';
197
+ const cls = nm.startsWith('.') ? p + nm : nm;
198
+ if (!applicationCandidates.some(c => c.applicationClass === cls)) applicationCandidates.push({ applicationClass: cls, manifest: mf.path });
199
+ // 首个为默认;opts.preferClass(wire-routes --app-class,多产品仓按所选产品显式指定)命中则以它为主
200
+ if (!nameAttr || (opts.preferClass && (cls === opts.preferClass || cls.endsWith('.' + opts.preferClass)))) { nameAttr = nm; pkg = p; appManifest = mf.path; }
201
+ }
202
+ const providers = [], startupInitializers = [];
203
+ for (const mf of manifests) {
204
+ const lineAt = (idx) => mf.text.slice(0, idx).split('\n').length;
205
+ for (const m of attrsOf(mf.text, 'provider')) { const nm = nameOf(m[1]); if (nm && !providers.some(p => p.name === nm)) providers.push({ name: nm, line: lineAt(m.index), manifest: mf.path }); }
206
+ for (const m of attrsOf(mf.text, 'meta-data')) {
207
+ if (!/android:value\s*=\s*["']androidx\.startup["']/.test(m[1])) continue;
208
+ const nm = nameOf(m[1]); if (nm && !startupInitializers.some(p => p.name === nm)) startupInitializers.push({ name: nm, line: lineAt(m.index), manifest: mf.path });
209
+ }
210
+ }
211
+ if (!nameAttr && !providers.length && !startupInitializers.length) return null;
212
+ const fqcn = nameAttr ? (nameAttr.startsWith('.') ? pkg + nameAttr : nameAttr) : null;
213
+ const src = files.filter(f => /\.(kt|java)$/.test(f.path));
214
+ // 类文件定位:同名类可能散在多个模块/目录(实证多模块工程),先按包声明精确匹配,再退到仅按类名;都要求文件里真有 class 声明。
215
+ const findClass = (nm, pkgHint) => {
216
+ const decl = new RegExp(`\\bclass\\s+${nm}\\b`);
217
+ const hits = src.filter(f => decl.test(f.text));
218
+ if (pkgHint) { const byPkg = hits.find(f => new RegExp(`^\\s*package\\s+${pkgHint.replace(/\./g, '\\.')}\\s*;?\\s*$`, 'm').test(f.text)); if (byPkg) return byPkg; }
219
+ return hits[0] || null;
220
+ };
221
+ // 只定位,不解释:onCreate 的文件与行范围(子类与每级父类各一段)是确定性事实;每一项怎么迁由 2b 读这段源码逐项判断。
222
+ // 调用名清单仅作提示(callHints),不进台账——正则抽调用在不同语言习惯下有噪声,噪声不能变成断言。
223
+ const onCreateSites = [], callHints = [], superChain = [];
224
+ const simple = fqcn ? fqcn.split('.').pop() : null;
225
+ let cur = fqcn ? findClass(simple, fqcn.slice(0, -(simple.length + 1))) : null, curName = simple, hops = 0;
226
+ const file = cur ? cur.path : null;
227
+ let di = null;
228
+ while (cur && hops < 4) {
229
+ superChain.push(curName);
230
+ if (!di) { const code = cur.text.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, ''); di = /@HiltAndroidApp/.test(code) ? 'Hilt' : /\bstartKoin\b/.test(code) ? 'Koin' : /Dagger\w*Component/.test(code) ? 'Dagger' : null; }
231
+ const oc = cur.text.search(/\b(?:fun|void)\s+onCreate\s*\(\s*\)/);
232
+ if (oc >= 0) {
233
+ const open = cur.text.indexOf('{', oc);
234
+ let d = 0, end = -1;
235
+ for (let j = open; j >= 0 && j < cur.text.length; j++) { if (cur.text[j] === '{') d++; else if (cur.text[j] === '}') { d--; if (!d) { end = j; break; } } }
236
+ if (open >= 0 && end > open) {
237
+ const startLine = cur.text.slice(0, oc).split('\n').length, endLine = cur.text.slice(0, end).split('\n').length;
238
+ const body = cur.text.slice(open + 1, end).replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
239
+ const meaningful = body.split('\n').filter(l => l.trim() && !/^\s*super\.onCreate\s*\(/.test(l)).length;
240
+ onCreateSites.push({ cls: curName, file: cur.path, startLine, endLine, bodyLines: meaningful });
241
+ for (const m of body.matchAll(/(?<![.\w$])([A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*)\s*[({]/g)) {
242
+ const callee = m[1];
243
+ if (/^(if|for|while|when|switch|return|catch|try|finally|else|do|init|object|companion|class|fun|synchronized|super|this)$/.test(callee) || /^super\./.test(callee)) continue;
244
+ if (!callHints.includes(callee)) callHints.push(callee);
245
+ }
246
+ }
247
+ }
248
+ const sup = (cur.text.match(new RegExp(`\\bclass\\s+${curName}\\b[^{]*?(?::|\\bextends)\\s*([A-Za-z_][\\w.]*)`)) || [])[1];
249
+ if (!sup) break;
250
+ const supSimple = sup.split('.').pop();
251
+ if (/^(Application|MultiDexApplication|DaggerApplication|Hilt_\w+|Any|Object)$/.test(supSimple)) break;
252
+ const next = findClass(supSimple, sup.includes('.') ? sup.slice(0, -(supSimple.length + 1)) : null);
253
+ if (!next) { superChain.push(supSimple + '?'); break; }
254
+ cur = next; curName = supSimple; hops++;
255
+ }
256
+ const workManagerProvider = src.some(f => superChain.some(c => new RegExp(`\\bclass\\s+${c.replace(/\?$/, '')}\\b`).test(f.text)) && /Configuration\.Provider/.test(f.text));
257
+ return { applicationClass: fqcn, appManifest, applicationCandidates, ambiguous: applicationCandidates.length > 1, file, classFound: !!file, superChain, onCreateSites, callHints, providers, startupInitializers, workManagerProvider, di };
258
+ }