@harmonyos-arkts/d2h 0.1.11 → 0.1.13
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/.claude-plugin/marketplace.json +15 -13
- package/.claude-plugin/plugin.json +13 -7
- package/AGENTS.md +5 -5
- package/README.md +212 -212
- package/agents/android-to-hmos-00-orchestrator.md +318 -318
- package/agents/d2h.md +157 -157
- package/dist/bin/install-opencode.js +30 -30
- package/docs/environment-check.md +86 -86
- package/install-opencode.sh +160 -160
- package/opencode/agents.json +16 -16
- package/opencode/vscode-d2h-package.json +7 -7
- package/opencode/vscode-d2h-plugin.mjs +29 -29
- package/package.json +54 -54
- package/schemas/android-source-manifest.schema.json +25 -25
- package/schemas/checkpoint-provenance.schema.json +67 -67
- package/schemas/final-acceptance.schema.json +29 -29
- package/schemas/managed-evidence-index.schema.json +15 -15
- package/schemas/managed-evidence.schema.json +114 -114
- package/schemas/migration-config.schema.json +51 -51
- package/schemas/migration-report-index.schema.json +39 -39
- package/schemas/migration-report-item.schema.json +51 -51
- package/schemas/migration-report-summary.schema.json +25 -25
- package/schemas/migration-status.schema.json +202 -202
- package/schemas/preflight.schema.json +60 -60
- package/schemas/source-order-audit.schema.json +16 -16
- package/schemas/source-provenance-event.schema.json +19 -19
- package/schemas/spec-app-shard.schema.json +45 -45
- package/schemas/spec-fact-corrections-shard.schema.json +13 -13
- package/schemas/spec-features-shard.schema.json +43 -43
- package/schemas/spec-index.schema.json +26 -26
- package/schemas/spec-interactions-shard.schema.json +40 -40
- package/schemas/spec-page.schema.json +98 -98
- package/schemas/spec-pages-index.schema.json +1 -1
- package/schemas/spec-unresolved-shard.schema.json +1 -1
- package/schemas/task-envelope.schema.json +109 -109
- package/schemas/task-plan.schema.json +341 -341
- package/scripts/check-environment.mjs +320 -320
- package/skills/android2hmos_resources_convert/SKILL.md +180 -180
- package/skills/android2hmos_resources_convert/references/image-conversion-rules.md +230 -230
- package/skills/android2hmos_resources_convert/references/svg-fix-patterns.md +175 -175
- package/skills/android2hmos_resources_convert/references/xml-drawable-to-svg-rules.md +513 -513
- package/skills/arkts-app-identity/SKILL.md +238 -238
- package/skills/arkts-i18n/SKILL.md +496 -496
- package/skills/arkts-i18n/evals/evals.json +84 -84
- package/skills/arkts-i18n/references/code-examples.md +302 -302
- package/skills/arkts-i18n/references/common-pitfalls.md +391 -391
- package/skills/arkts-i18n/references/dynamic-language-switch.md +604 -604
- package/skills/arkts-i18n/references/hardcoded-string-scanner.md +348 -348
- package/skills/arkts-i18n/references/language-codes.md +104 -104
- package/skills/arkts-i18n/references/resource-file-structure.md +775 -775
- package/skills/arkts-i18n/references/static-vs-dynamic.md +242 -242
- package/skills/arkts-i18n/references/v1-compat.md +244 -244
- package/skills/arkts-i18n/scripts/audit_i18n_completeness.sh +174 -174
- package/skills/arkts-icon-sizing/SKILL.md +211 -211
- package/skills/arkts-icon-sizing/scripts/icon_audit.py +131 -131
- package/skills/arkts-icon-sizing/scripts/icon_autofix.py +88 -88
- package/skills/arkts-icon-sizing/scripts/icon_dims.py +179 -179
- package/skills/arkts-icon-sizing/scripts/icon_fix.py +119 -119
- package/skills/arkts-mvvm-architecture/SKILL.md +613 -613
- package/skills/d2h-hypium-forge/SKILL.md +309 -309
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -140
- package/skills/d2h-hypium-forge/references/official-api.md +135 -135
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -98
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -71
- package/skills/d2h-hypium-forge/scripts/_common_template.py +659 -659
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +165 -165
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -107
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -169
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -325
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -114
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -80
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -60
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -249
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -140
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -232
- package/skills/d2h-hypium-forge/scripts/probe.py +197 -197
- package/skills/d2h-hypium-forge/scripts/run_direct.py +823 -823
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -121
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -255
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -66
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -84
- package/skills/d2h-page-develop/SKILL.md +112 -112
- package/skills/d2h-page-develop/agents/openai.yaml +7 -7
- package/skills/d2h-page-spec-generate/SKILL.md +206 -206
- package/skills/d2h-page-spec-generate/scripts/build_batch_report.py +227 -227
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -128
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -76
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -34
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -96
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -61
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -89
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -314
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -123
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -280
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -524
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -489
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -69
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -13
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -2
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -369
- package/skills/d2h-utg-static-generate/SKILL.md +73 -73
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -38
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -499
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -335
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -60
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -131
- package/skills/harmonyos-migration-playbook/SKILL.md +83 -83
- package/skills/harmonyos-migration-playbook/agents/openai.yaml +7 -7
- package/skills/harmonyos-migration-playbook/references/arkts-compile.md +24 -24
- package/skills/harmonyos-migration-playbook/references/harmony-runtime.md +53 -53
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +45 -45
- package/skills/harmonyos-migration-playbook/references/protocol-e2e.md +23 -23
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +53 -53
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +38 -38
- package/skills/maintaining-migration-report/SKILL.md +144 -144
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -48
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -66
- package/skills/managing-d2h-toolchain/SKILL.md +24 -24
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -16
- package/skills/managing-d2h-toolchain/scripts/manage-toolchain.mjs +98 -98
- package/skills/managing-d2h-toolchain/toolchain.json +16 -16
- package/skills/native-library-substitution/SKILL.md +384 -384
- package/skills/native-library-substitution/references/native-library-substitution.json +56905 -56905
- package/skills/native-library-substitution/references/native-library-substitution.md +163 -163
- package/skills/preparing-migration-workspace/SKILL.md +131 -131
- 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()
|