@harmonyos-arkts/d2h 0.1.11 → 0.1.12

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 (126) hide show
  1. package/.claude-plugin/marketplace.json +15 -13
  2. package/.claude-plugin/plugin.json +13 -7
  3. package/AGENTS.md +5 -5
  4. package/README.md +212 -212
  5. package/agents/android-to-hmos-00-orchestrator.md +318 -318
  6. package/agents/d2h.md +157 -157
  7. package/dist/bin/install-opencode.js +30 -30
  8. package/docs/environment-check.md +86 -86
  9. package/install-opencode.sh +160 -160
  10. package/opencode/agents.json +16 -16
  11. package/opencode/vscode-d2h-package.json +7 -7
  12. package/opencode/vscode-d2h-plugin.mjs +29 -29
  13. package/package.json +54 -54
  14. package/schemas/android-source-manifest.schema.json +25 -25
  15. package/schemas/checkpoint-provenance.schema.json +67 -67
  16. package/schemas/final-acceptance.schema.json +29 -29
  17. package/schemas/managed-evidence-index.schema.json +15 -15
  18. package/schemas/managed-evidence.schema.json +114 -114
  19. package/schemas/migration-config.schema.json +51 -51
  20. package/schemas/migration-report-index.schema.json +39 -39
  21. package/schemas/migration-report-item.schema.json +51 -51
  22. package/schemas/migration-report-summary.schema.json +25 -25
  23. package/schemas/migration-status.schema.json +202 -202
  24. package/schemas/preflight.schema.json +60 -60
  25. package/schemas/source-order-audit.schema.json +16 -16
  26. package/schemas/source-provenance-event.schema.json +19 -19
  27. package/schemas/spec-app-shard.schema.json +45 -45
  28. package/schemas/spec-fact-corrections-shard.schema.json +13 -13
  29. package/schemas/spec-features-shard.schema.json +43 -43
  30. package/schemas/spec-index.schema.json +26 -26
  31. package/schemas/spec-interactions-shard.schema.json +40 -40
  32. package/schemas/spec-page.schema.json +98 -98
  33. package/schemas/spec-pages-index.schema.json +1 -1
  34. package/schemas/spec-unresolved-shard.schema.json +1 -1
  35. package/schemas/task-envelope.schema.json +109 -109
  36. package/schemas/task-plan.schema.json +341 -341
  37. package/scripts/check-environment.mjs +320 -320
  38. package/skills/android2hmos_resources_convert/SKILL.md +180 -180
  39. package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
  40. package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
  41. package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
  42. package/skills/arkts-app-identity/SKILL.md +238 -238
  43. package/skills/arkts-i18n/SKILL.md +496 -496
  44. package/skills/arkts-i18n/evals/evals.json +84 -84
  45. package/skills/arkts-i18n/references/code-examples.md +302 -302
  46. package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
  47. package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
  48. package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
  49. package/skills/arkts-i18n/references/language-codes.md +104 -104
  50. package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
  51. package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
  52. package/skills/arkts-i18n/references/v1-compat.md +244 -244
  53. package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
  54. package/skills/arkts-icon-sizing/SKILL.md +211 -211
  55. package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
  56. package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
  57. package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
  58. package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
  59. package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
  60. package/skills/d2h-hypium-forge/SKILL.md +309 -309
  61. package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
  62. package/skills/d2h-hypium-forge/references/official-api.md +135 -135
  63. package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
  64. package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
  65. package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
  66. package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
  67. package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
  68. package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
  69. package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
  70. package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
  71. package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
  72. package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
  73. package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
  74. package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
  75. package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
  76. package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
  77. package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
  78. package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
  79. package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
  80. package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
  81. package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
  82. package/skills/d2h-page-develop/SKILL.md +112 -112
  83. package/skills/d2h-page-develop/agents/openai.yaml +7 -7
  84. package/skills/d2h-page-spec-generate/SKILL.md +206 -206
  85. package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
  86. package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
  87. package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
  88. package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
  89. package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
  90. package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
  91. package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
  92. package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
  93. package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
  94. package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
  95. package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
  96. package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
  97. package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
  98. package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
  99. package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
  100. package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
  101. package/skills/d2h-utg-static-generate/SKILL.md +73 -73
  102. package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
  103. package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
  104. package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
  105. package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
  106. package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
  107. package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
  108. package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
  109. package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
  110. package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
  111. package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
  112. package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
  113. package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
  114. package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
  115. package/skills/maintaining-migration-report/SKILL.md +144 -144
  116. package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
  117. package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
  118. package/skills/managing-d2h-toolchain/SKILL.md +24 -24
  119. package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
  120. package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
  121. package/skills/managing-d2h-toolchain/toolchain.json +16 -16
  122. package/skills/native-library-substitution/SKILL.md +384 -384
  123. package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
  124. package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
  125. package/skills/preparing-migration-workspace/SKILL.md +131 -131
  126. package/skills/reviewing-migration-process/SKILL.md +62 -62
@@ -1,249 +1,249 @@
1
- #!/usr/bin/env python3
2
- """
3
- merge_appgraph.py — AppGraph(analyze android) facts × forge scan_features 语义合并。
4
-
5
- 定位:scan_features 只见鸿蒙侧静态树(.id/行号/if 分支),缺行为语义;
6
- AppGraph facts 是安卓权威行为账本(控件/入口/事件/handler/callTrace/导航边/trigger 分类),
7
- 但无鸿蒙侧定位事实。本脚本按 token 集合 join(answerFeature ↔ feature_answer_switch)
8
- 把 Android 语义注入 features.json,产出同名 schema 的增强版,供 evaluate_coverage /
9
- 用例设计(handler 名直接揭示联动语义)/ agent 融合(ref 体系对齐迁移报告)消费。
10
-
11
- 用法:
12
- merge_appgraph.py --features features.json --facts <android>/.migration/spec/facts \
13
- [--out features.merged.json]
14
- 无 AppGraph facts 时退出码 2(调用方回退纯 scan_features)。
15
- """
16
- import argparse
17
- import glob
18
- import json
19
- import os
20
- import re
21
-
22
-
23
- def tokens(s):
24
- """camelCase + snake_case 统一切词:answerFeature 与 feature_answer → {answer, feature}。"""
25
- parts = [p.lower() for p in re.findall(r'[A-Z]?[a-z0-9]+', str(s or ''))]
26
- return set(parts) - {'menu', 'click', 'button', 'entry', 'the', 'id', 'app'}
27
-
28
-
29
- def norm_name(s):
30
- return re.sub(r'[^a-z0-9]', '', str(s or '').lower())
31
-
32
-
33
- def join_key_candidates(name):
34
- t = tokens(name)
35
- return t, norm_name(name)
36
-
37
-
38
- def match(names, pool, event=None):
39
- """names: 候选语义名(carrier/label/event 提取);规则:norm 相等 > token 全等 >
40
- android ⊂ forge id(容许 _switch 类小后缀);checked 类事件优先 *_switch 目标。"""
41
- for name in names:
42
- if not name:
43
- continue
44
- ct, cn = tokens(name), norm_name(name)
45
- if not ct and not cn:
46
- continue
47
- for ft in sorted(pool):
48
- if cn and norm_name(ft) == cn:
49
- return ft
50
- cands = []
51
- for ft in sorted(pool):
52
- ftt = tokens(ft)
53
- if not ftt or not ct:
54
- continue
55
- if ct == ftt:
56
- cands.append((0, ft))
57
- elif ct < ftt and len(ftt) <= len(ct) + 2:
58
- cands.append((1, ft))
59
- if not cands:
60
- continue
61
- cands.sort()
62
- if event and str(event).startswith('checked'):
63
- for _, ft in cands:
64
- if 'switch' in ft or 'toggle' in ft:
65
- return ft
66
- return cands[0][1]
67
- return None
68
-
69
-
70
- def event_tokens(event):
71
- """menu-click:privacy_policy → ['privacy_policy'];checked-change → []。"""
72
- m = re.match(r'^menu-click:(\w+)$', str(event or ''))
73
- return [m.group(1)] if m else []
74
-
75
-
76
- def harvest_ids(project_dir):
77
- """收割工程内可静态定位的 ID:.id('x') 字面量 + 组件参数 *Id: 'x'(如 rowId)。
78
- 模板拼接 .id(`${rowId}_switch`) 无法静态解析,由 prop 值 + 已知后缀兜底。"""
79
- pool = {}
80
- root = os.path.join(project_dir or '.', '')
81
- for path in glob.glob(os.path.join(root, '**', '*.ets'), recursive=True):
82
- if os.sep + 'ohosTest' in path or os.sep + '.hypium_forge' in path:
83
- continue
84
- try:
85
- src = open(path, encoding='utf-8').read()
86
- except Exception:
87
- continue
88
- for m in re.finditer(r"\.id\(\s*['\"`]([\w-]+)['\"`]\s*\)", src):
89
- line = src[:m.start()].count('\n') + 1
90
- pool.setdefault(m.group(1), {'file': os.path.relpath(path, root), 'line': line})
91
- for m in re.finditer(r"\b\w*[Ii]d\s*:\s*['\"]([\w-]+)['\"]", src):
92
- for cand in (m.group(1), f"{m.group(1)}_switch"):
93
- pool.setdefault(cand, {'file': os.path.relpath(path, root), 'line': src[:m.start()].count('\n') + 1})
94
- return pool
95
-
96
-
97
- def load_appgraph(facts_dir):
98
- controls, entries, nav = [], [], []
99
- inter_path = os.path.join(facts_dir, 'interactions.json')
100
- if os.path.isdir(facts_dir):
101
- pages = sorted(glob.glob(os.path.join(facts_dir, 'pages', '*.json')))
102
- else:
103
- pages = []
104
- for p in pages:
105
- if os.path.basename(p) == 'index.json' or os.path.basename(p) == 'unassigned.json':
106
- continue
107
- try:
108
- d = json.load(open(p, encoding='utf-8'))
109
- except Exception:
110
- continue
111
- controls += d.get('controls', []) or []
112
- entries += d.get('entries', []) or []
113
- if os.path.exists(inter_path):
114
- try:
115
- nav = json.load(open(inter_path, encoding='utf-8')).get('interactions', []) or []
116
- except Exception:
117
- nav = []
118
- return controls, entries, nav
119
-
120
-
121
- def main():
122
- ap = argparse.ArgumentParser()
123
- ap.add_argument('--features', required=True)
124
- ap.add_argument('--facts', required=True)
125
- ap.add_argument('--out', default=None)
126
- args = ap.parse_args()
127
-
128
- if not os.path.isdir(args.facts):
129
- print(f'[x] AppGraph facts 不存在: {args.facts}')
130
- raise SystemExit(2)
131
- feats = json.load(open(args.features, encoding='utf-8'))
132
- controls, entries, nav = load_appgraph(args.facts)
133
- if not entries and not controls:
134
- print('[x] facts 为空,回退 scan_features')
135
- raise SystemExit(2)
136
-
137
- forge_targets = []
138
- for pg in feats.get('pages', []):
139
- for it in pg.get('interactions', []):
140
- forge_targets.append(it.get('target'))
141
- id_pool = harvest_ids(feats.get('project_dir', '.'))
142
- pool = set(forge_targets) | set(id_pool.keys())
143
-
144
- android_entries = []
145
- for e in entries:
146
- rec = {
147
- 'name': e.get('carrierName') or e.get('displayLabel'),
148
- 'event': e.get('event'),
149
- 'handlers': e.get('handlerNames', []) or [],
150
- 'trigger': e.get('triggerCategory'),
151
- 'ref': e.get('entryRef'),
152
- 'label': e.get('displayLabel'),
153
- 'android_evidence': (e.get('sourceEvidence') or [{}])[0].get('file', ''),
154
- }
155
- android_entries.append(rec)
156
-
157
- android_controls = []
158
- for c in controls:
159
- android_controls.append({
160
- 'name': c.get('name'),
161
- 'ref': c.get('controlRef'),
162
- 'evidence': (c.get('sourceEvidence') or [{}])[0].get('file', ''),
163
- })
164
-
165
- matched, unmatched = {}, []
166
- for ae in android_entries:
167
- if ae['trigger'] != 'user':
168
- continue
169
- cand = [ae['name'], ae['label']] + event_tokens(ae['event'])
170
- ft = match(cand, pool, ae['event'])
171
- if ft:
172
- ae['forge_target'] = ft
173
- matched.setdefault(ft, []).append(ae)
174
- else:
175
- unmatched.append(ae)
176
-
177
- nav_edges = [{'from': n.get('fromRef'), 'to': n.get('toRef'), 'relation': n.get('relation')}
178
- for n in nav]
179
-
180
- enriched_pages = []
181
- for pg in feats.get('pages', []):
182
- for it in pg.get('interactions', []):
183
- hits = matched.get(it.get('target'), [])
184
- if hits:
185
- h = hits[0]
186
- it['android_event'] = h['event']
187
- it['android_handlers'] = h['handlers']
188
- it['android_ref'] = h['ref']
189
- it['android_evidence'] = h['android_evidence']
190
- enriched_pages.append(pg)
191
-
192
- entry_path = next((p['path'] for p in enriched_pages if enriched_pages and p['path'] == feats.get('entry', {}).get('page')),
193
- enriched_pages[0]['path'] if enriched_pages else None)
194
- entry_idx = next((i for i, p in enumerate(enriched_pages) if p['path'] == entry_path), 0)
195
-
196
- def owner_unit(h, loc):
197
- """归属:checked-change → 拥有 lineN:Toggle 的组件单元(并移除被具体化的 lineN);
198
- 其余 → 入口页。"""
199
- if str(h['event']).startswith('checked'):
200
- for i, p in enumerate(enriched_pages):
201
- for it in p.get('interactions', []):
202
- if str(it.get('target', '')).startswith('line') and it.get('type') == 'Toggle':
203
- p['interactions'] = [x for x in p['interactions'] if x is not it]
204
- return i
205
- return entry_idx
206
-
207
- for ft, hits in matched.items():
208
- if any(ft == it.get('target') for pg in enriched_pages for it in pg.get('interactions', [])):
209
- continue
210
- loc = id_pool.get(ft, {})
211
- h = hits[0]
212
- enriched_pages[owner_unit(h, loc)]['interactions'].append({
213
- 'kind': 'click' if 'click' in str(h['event']) else 'toggle',
214
- 'target': ft,
215
- 'desc': f"android:{h['name']}({h['event']})",
216
- 'has_id': ft in id_pool,
217
- 'line': loc.get('line', 0),
218
- 'type': 'IdRef',
219
- 'android_event': h['event'],
220
- 'android_handlers': h['handlers'],
221
- 'android_ref': h['ref'],
222
- 'android_evidence': h['android_evidence'],
223
- 'android_file': loc.get('file', ''),
224
- })
225
-
226
- feats['pages'] = enriched_pages
227
- feats['android'] = {
228
- 'controls': android_controls,
229
- 'entries_user': [e for e in android_entries if e['trigger'] == 'user'],
230
- 'entries_system': [e for e in android_entries if e['trigger'] != 'user'],
231
- 'nav_edges': nav_edges,
232
- 'unmatched_user_entries': unmatched,
233
- 'source': args.facts,
234
- }
235
-
236
- out = args.out or args.features.replace('.json', '.merged.json')
237
- json.dump(feats, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
238
- print(f'合并完成: controls={len(android_controls)} user入口={len(feats["android"]["entries_user"])} '
239
- f'系统入口={len(feats["android"]["entries_system"])} join命中={len(matched)}/{len(matched)+len(unmatched)} '
240
- f'导航边={len(nav_edges)} → {out}')
241
- for ft, hits in sorted(matched.items()):
242
- for h in hits:
243
- print(f' [+] {h["name"]}({h["event"]}) → {ft} handlers={h["handlers"]}')
244
- for u in unmatched:
245
- print(f' [?] 未join: {u["name"]} ({u["event"]}) handlers={u["handlers"]}')
246
-
247
-
248
- if __name__ == '__main__':
249
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ merge_appgraph.py — AppGraph(analyze android) facts × forge scan_features 语义合并。
4
+
5
+ 定位:scan_features 只见鸿蒙侧静态树(.id/行号/if 分支),缺行为语义;
6
+ AppGraph facts 是安卓权威行为账本(控件/入口/事件/handler/callTrace/导航边/trigger 分类),
7
+ 但无鸿蒙侧定位事实。本脚本按 token 集合 join(answerFeature ↔ feature_answer_switch)
8
+ 把 Android 语义注入 features.json,产出同名 schema 的增强版,供 evaluate_coverage /
9
+ 用例设计(handler 名直接揭示联动语义)/ agent 融合(ref 体系对齐迁移报告)消费。
10
+
11
+ 用法:
12
+ merge_appgraph.py --features features.json --facts <android>/.migration/spec/facts \
13
+ [--out features.merged.json]
14
+ 无 AppGraph facts 时退出码 2(调用方回退纯 scan_features)。
15
+ """
16
+ import argparse
17
+ import glob
18
+ import json
19
+ import os
20
+ import re
21
+
22
+
23
+ def tokens(s):
24
+ """camelCase + snake_case 统一切词:answerFeature 与 feature_answer → {answer, feature}。"""
25
+ parts = [p.lower() for p in re.findall(r'[A-Z]?[a-z0-9]+', str(s or ''))]
26
+ return set(parts) - {'menu', 'click', 'button', 'entry', 'the', 'id', 'app'}
27
+
28
+
29
+ def norm_name(s):
30
+ return re.sub(r'[^a-z0-9]', '', str(s or '').lower())
31
+
32
+
33
+ def join_key_candidates(name):
34
+ t = tokens(name)
35
+ return t, norm_name(name)
36
+
37
+
38
+ def match(names, pool, event=None):
39
+ """names: 候选语义名(carrier/label/event 提取);规则:norm 相等 > token 全等 >
40
+ android ⊂ forge id(容许 _switch 类小后缀);checked 类事件优先 *_switch 目标。"""
41
+ for name in names:
42
+ if not name:
43
+ continue
44
+ ct, cn = tokens(name), norm_name(name)
45
+ if not ct and not cn:
46
+ continue
47
+ for ft in sorted(pool):
48
+ if cn and norm_name(ft) == cn:
49
+ return ft
50
+ cands = []
51
+ for ft in sorted(pool):
52
+ ftt = tokens(ft)
53
+ if not ftt or not ct:
54
+ continue
55
+ if ct == ftt:
56
+ cands.append((0, ft))
57
+ elif ct < ftt and len(ftt) <= len(ct) + 2:
58
+ cands.append((1, ft))
59
+ if not cands:
60
+ continue
61
+ cands.sort()
62
+ if event and str(event).startswith('checked'):
63
+ for _, ft in cands:
64
+ if 'switch' in ft or 'toggle' in ft:
65
+ return ft
66
+ return cands[0][1]
67
+ return None
68
+
69
+
70
+ def event_tokens(event):
71
+ """menu-click:privacy_policy → ['privacy_policy'];checked-change → []。"""
72
+ m = re.match(r'^menu-click:(\w+)$', str(event or ''))
73
+ return [m.group(1)] if m else []
74
+
75
+
76
+ def harvest_ids(project_dir):
77
+ """收割工程内可静态定位的 ID:.id('x') 字面量 + 组件参数 *Id: 'x'(如 rowId)。
78
+ 模板拼接 .id(`${rowId}_switch`) 无法静态解析,由 prop 值 + 已知后缀兜底。"""
79
+ pool = {}
80
+ root = os.path.join(project_dir or '.', '')
81
+ for path in glob.glob(os.path.join(root, '**', '*.ets'), recursive=True):
82
+ if os.sep + 'ohosTest' in path or os.sep + '.hypium_forge' in path:
83
+ continue
84
+ try:
85
+ src = open(path, encoding='utf-8').read()
86
+ except Exception:
87
+ continue
88
+ for m in re.finditer(r"\.id\(\s*['\"`]([\w-]+)['\"`]\s*\)", src):
89
+ line = src[:m.start()].count('\n') + 1
90
+ pool.setdefault(m.group(1), {'file': os.path.relpath(path, root), 'line': line})
91
+ for m in re.finditer(r"\b\w*[Ii]d\s*:\s*['\"]([\w-]+)['\"]", src):
92
+ for cand in (m.group(1), f"{m.group(1)}_switch"):
93
+ pool.setdefault(cand, {'file': os.path.relpath(path, root), 'line': src[:m.start()].count('\n') + 1})
94
+ return pool
95
+
96
+
97
+ def load_appgraph(facts_dir):
98
+ controls, entries, nav = [], [], []
99
+ inter_path = os.path.join(facts_dir, 'interactions.json')
100
+ if os.path.isdir(facts_dir):
101
+ pages = sorted(glob.glob(os.path.join(facts_dir, 'pages', '*.json')))
102
+ else:
103
+ pages = []
104
+ for p in pages:
105
+ if os.path.basename(p) == 'index.json' or os.path.basename(p) == 'unassigned.json':
106
+ continue
107
+ try:
108
+ d = json.load(open(p, encoding='utf-8'))
109
+ except Exception:
110
+ continue
111
+ controls += d.get('controls', []) or []
112
+ entries += d.get('entries', []) or []
113
+ if os.path.exists(inter_path):
114
+ try:
115
+ nav = json.load(open(inter_path, encoding='utf-8')).get('interactions', []) or []
116
+ except Exception:
117
+ nav = []
118
+ return controls, entries, nav
119
+
120
+
121
+ def main():
122
+ ap = argparse.ArgumentParser()
123
+ ap.add_argument('--features', required=True)
124
+ ap.add_argument('--facts', required=True)
125
+ ap.add_argument('--out', default=None)
126
+ args = ap.parse_args()
127
+
128
+ if not os.path.isdir(args.facts):
129
+ print(f'[x] AppGraph facts 不存在: {args.facts}')
130
+ raise SystemExit(2)
131
+ feats = json.load(open(args.features, encoding='utf-8'))
132
+ controls, entries, nav = load_appgraph(args.facts)
133
+ if not entries and not controls:
134
+ print('[x] facts 为空,回退 scan_features')
135
+ raise SystemExit(2)
136
+
137
+ forge_targets = []
138
+ for pg in feats.get('pages', []):
139
+ for it in pg.get('interactions', []):
140
+ forge_targets.append(it.get('target'))
141
+ id_pool = harvest_ids(feats.get('project_dir', '.'))
142
+ pool = set(forge_targets) | set(id_pool.keys())
143
+
144
+ android_entries = []
145
+ for e in entries:
146
+ rec = {
147
+ 'name': e.get('carrierName') or e.get('displayLabel'),
148
+ 'event': e.get('event'),
149
+ 'handlers': e.get('handlerNames', []) or [],
150
+ 'trigger': e.get('triggerCategory'),
151
+ 'ref': e.get('entryRef'),
152
+ 'label': e.get('displayLabel'),
153
+ 'android_evidence': (e.get('sourceEvidence') or [{}])[0].get('file', ''),
154
+ }
155
+ android_entries.append(rec)
156
+
157
+ android_controls = []
158
+ for c in controls:
159
+ android_controls.append({
160
+ 'name': c.get('name'),
161
+ 'ref': c.get('controlRef'),
162
+ 'evidence': (c.get('sourceEvidence') or [{}])[0].get('file', ''),
163
+ })
164
+
165
+ matched, unmatched = {}, []
166
+ for ae in android_entries:
167
+ if ae['trigger'] != 'user':
168
+ continue
169
+ cand = [ae['name'], ae['label']] + event_tokens(ae['event'])
170
+ ft = match(cand, pool, ae['event'])
171
+ if ft:
172
+ ae['forge_target'] = ft
173
+ matched.setdefault(ft, []).append(ae)
174
+ else:
175
+ unmatched.append(ae)
176
+
177
+ nav_edges = [{'from': n.get('fromRef'), 'to': n.get('toRef'), 'relation': n.get('relation')}
178
+ for n in nav]
179
+
180
+ enriched_pages = []
181
+ for pg in feats.get('pages', []):
182
+ for it in pg.get('interactions', []):
183
+ hits = matched.get(it.get('target'), [])
184
+ if hits:
185
+ h = hits[0]
186
+ it['android_event'] = h['event']
187
+ it['android_handlers'] = h['handlers']
188
+ it['android_ref'] = h['ref']
189
+ it['android_evidence'] = h['android_evidence']
190
+ enriched_pages.append(pg)
191
+
192
+ entry_path = next((p['path'] for p in enriched_pages if enriched_pages and p['path'] == feats.get('entry', {}).get('page')),
193
+ enriched_pages[0]['path'] if enriched_pages else None)
194
+ entry_idx = next((i for i, p in enumerate(enriched_pages) if p['path'] == entry_path), 0)
195
+
196
+ def owner_unit(h, loc):
197
+ """归属:checked-change → 拥有 lineN:Toggle 的组件单元(并移除被具体化的 lineN);
198
+ 其余 → 入口页。"""
199
+ if str(h['event']).startswith('checked'):
200
+ for i, p in enumerate(enriched_pages):
201
+ for it in p.get('interactions', []):
202
+ if str(it.get('target', '')).startswith('line') and it.get('type') == 'Toggle':
203
+ p['interactions'] = [x for x in p['interactions'] if x is not it]
204
+ return i
205
+ return entry_idx
206
+
207
+ for ft, hits in matched.items():
208
+ if any(ft == it.get('target') for pg in enriched_pages for it in pg.get('interactions', [])):
209
+ continue
210
+ loc = id_pool.get(ft, {})
211
+ h = hits[0]
212
+ enriched_pages[owner_unit(h, loc)]['interactions'].append({
213
+ 'kind': 'click' if 'click' in str(h['event']) else 'toggle',
214
+ 'target': ft,
215
+ 'desc': f"android:{h['name']}({h['event']})",
216
+ 'has_id': ft in id_pool,
217
+ 'line': loc.get('line', 0),
218
+ 'type': 'IdRef',
219
+ 'android_event': h['event'],
220
+ 'android_handlers': h['handlers'],
221
+ 'android_ref': h['ref'],
222
+ 'android_evidence': h['android_evidence'],
223
+ 'android_file': loc.get('file', ''),
224
+ })
225
+
226
+ feats['pages'] = enriched_pages
227
+ feats['android'] = {
228
+ 'controls': android_controls,
229
+ 'entries_user': [e for e in android_entries if e['trigger'] == 'user'],
230
+ 'entries_system': [e for e in android_entries if e['trigger'] != 'user'],
231
+ 'nav_edges': nav_edges,
232
+ 'unmatched_user_entries': unmatched,
233
+ 'source': args.facts,
234
+ }
235
+
236
+ out = args.out or args.features.replace('.json', '.merged.json')
237
+ json.dump(feats, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
238
+ print(f'合并完成: controls={len(android_controls)} user入口={len(feats["android"]["entries_user"])} '
239
+ f'系统入口={len(feats["android"]["entries_system"])} join命中={len(matched)}/{len(matched)+len(unmatched)} '
240
+ f'导航边={len(nav_edges)} → {out}')
241
+ for ft, hits in sorted(matched.items()):
242
+ for h in hits:
243
+ print(f' [+] {h["name"]}({h["event"]}) → {ft} handlers={h["handlers"]}')
244
+ for u in unmatched:
245
+ print(f' [?] 未join: {u["name"]} ({u["event"]}) handlers={u["handlers"]}')
246
+
247
+
248
+ if __name__ == '__main__':
249
+ main()