@harmonyos-arkts/d2h 0.1.10 → 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,255 +1,255 @@
1
- #!/usr/bin/env python3
2
- """
3
- scan_features.py — 扫描鸿蒙工程,产出功能场景矩阵(features.json + features.md)。
4
-
5
- 场景矩阵模型(覆盖度评估的基础):
6
- 页面(路由可达) × 交互点(可操作控件/事件) × 状态(数据/条件分支)
7
-
8
- 输出 features.json:
9
- {
10
- "project_dir": "...",
11
- "bundle": "hm.xxx",
12
- "pages": [
13
- {
14
- "page": "Index", "path": "pages/Index", "file": "...",
15
- "nav_in": ["入口"] , # 如何到达(入口/router.pushUrl 链)
16
- "interactions": [ # 交互点(含已有 ID)
17
- {"kind": "click", "target": "id_or_text", "desc": "设置按钮", "has_id": true}
18
- ],
19
- "states": [ # 可区分状态(条件渲染/数据驱动)
20
- {"name": "列表为空", "evidence": "if (!items.length)", "line": 42}
21
- ],
22
- "data_sources": ["mock", "http", "preferences"]
23
- }
24
- ],
25
- "permissions": ["ohos.permission.INTERNET", ...], # 需预授/确认的权限
26
- "has_login": true|false, # 登录页检测
27
- "nav_graph": {"pages/Index": ["pages/Settings", ...]} # 静态跳转图
28
- }
29
-
30
- 用法: scan_features.py <harmony_project_dir> [--out features.json]
31
- """
32
- import argparse
33
- import json
34
- import os
35
- import re
36
- import sys
37
-
38
- # 登录页特征
39
- LOGIN_HINTS = ['登录', 'login', 'Login', 'signin', 'SignIn', '账号', '密码', 'password', '手机号']
40
- # 数据源特征
41
- DATA_HINTS = {
42
- 'http': re.compile(r'https?://|@ohos\.net\.http|axios|rcp'),
43
- 'preferences': re.compile(r'@ohos\.data\.preferences|preferences\.getPreferences'),
44
- 'mock': re.compile(r'\bmock\b|MockData|EMOJI_DATA|SAMPLE_', re.I),
45
- 'db': re.compile(r'@ohos\.data\.relationalStore|relationalStore'),
46
- }
47
- # 交互控件(组件级)
48
- INTERACTIVE_COMPONENTS = ('Button', 'TextInput', 'TextArea', 'Toggle', 'Slider',
49
- 'Radio', 'Checkbox', 'Select', 'Search', 'Stepper',
50
- 'Rating', 'DatePicker', 'TextPicker', 'TimePicker',
51
- 'Image', 'Hyperlink', 'Navigator')
52
- # 非组件关键词(生命周期/builder/容器等误判源)
53
- NON_COMPONENT = re.compile(r'^(onPageShow|onBackPress|aboutToAppear|aboutToDisappear|'
54
- r'\w*[Bb]uilder|TabContent|ForEach|LazyForEach|Repeat|'
55
- r'Navigation|NavDestination|if|else|while|for|struct|class|build)\b')
56
- # 风险权限(需用户确认/无法预授)
57
- RISKY_PERMS = {'ohos.permission.USER_AUTH', 'ohos.permission.MANAGE_FINGERPRINT'}
58
-
59
-
60
- def read(p):
61
- try:
62
- return open(p, encoding='utf-8', errors='ignore').read()
63
- except OSError:
64
- return ''
65
-
66
-
67
- def find_pages(root):
68
- """定位入口模块与页面集。"""
69
- # 找含 UIAbility+loadContent 的模块(entry 或自定义)
70
- modules = []
71
- for cand in sorted(os.listdir(root)):
72
- mdir = os.path.join(root, cand)
73
- if os.path.isdir(mdir) and os.path.isdir(os.path.join(mdir, 'src', 'main')):
74
- modules.append(cand)
75
- elif os.path.isdir(mdir) and cand not in ('oh_modules', '.hypium_forge', 'AppScope', 'hvigor', 'build', 'doc'):
76
- # 二级模块容器(features/commons 下可能有独立模块)
77
- for sub in sorted(os.listdir(mdir)):
78
- sdir = os.path.join(mdir, sub)
79
- if os.path.isdir(os.path.join(sdir, 'src', 'main')):
80
- modules.append(f'{cand}/{sub}')
81
-
82
- pages = {}
83
- nav_graph = {}
84
- entry_module, entry_page = None, None
85
- for m in modules:
86
- mdir = os.path.join(root, m, 'src', 'main')
87
- # Ability 的 loadContent
88
- for dp, _d, files in os.walk(os.path.join(mdir, 'ets')):
89
- for f in files:
90
- if f.endswith('.ets'):
91
- src = read(os.path.join(dp, f))
92
- mm = re.search(r"loadContent\(\s*'([^']+)'", src)
93
- if mm and entry_page is None:
94
- entry_module, entry_page = m, mm.group(1)
95
- # 路由表(可能多个)
96
- for dp, _d, files in os.walk(os.path.join(mdir, 'resources')):
97
- for f in files:
98
- if f.endswith('_pages.json'):
99
- try:
100
- routes = json.load(open(os.path.join(dp, f))).get('src', [])
101
- except (OSError, json.JSONDecodeError):
102
- routes = []
103
- for r in routes:
104
- pages.setdefault((m, r), None)
105
- # @Entry 页面兜底(覆盖路由表留下的 None 值)
106
- ets_root = os.path.join(mdir, 'ets')
107
- for dp, _d, files in os.walk(ets_root):
108
- for f in files:
109
- if f.endswith('.ets') and '@Entry' in read(os.path.join(dp, f)):
110
- rel = os.path.relpath(os.path.join(dp, f), ets_root)[:-4]
111
- if pages.get((m, rel)) is None:
112
- pages[(m, rel)] = os.path.join(dp, f)
113
- return modules, pages, entry_module, entry_page
114
-
115
-
116
- def scan_page(file_path):
117
- """单页扫描:交互点/状态/数据源/跳转。"""
118
- src = read(file_path)
119
- lines = src.split('\n')
120
- interactions, states, navs = [], [], []
121
- data_sources = [k for k, rx in DATA_HINTS.items() if rx.search(src)]
122
-
123
- # 组件构造 + 邻近 .id()/文本
124
- for i, ln in enumerate(lines):
125
- m = re.match(r'^\s*(\w+)\s*(?:\(\s*(?:{[^}]*})?\s*\)?\s*(?:\{\s*)?)$', ln) or \
126
- re.match(r'^\s*(Button|Text|Image|Toggle|TextInput)\s*\(', ln)
127
- if not m or NON_COMPONENT.match(m.group(1)):
128
- continue
129
- comp = m.group(1)
130
- # 向后 15 行找 .id / 文本内容 / onClick
131
- after = '\n'.join(lines[i:min(len(lines), i + 15)])
132
- idm = re.search(r"\.id\(\s*'([^']+)'", after)
133
- txtm = re.search(r"(?:Text|Button)\s*\(\s*(?:this\.)?[\w.]*?'([^']{1,24})'", '\n'.join(lines[max(0,i-1):i+6]))
134
- onclick = bool(re.search(r'\.on[A-Z]\w*\(', after))
135
- if comp in INTERACTIVE_COMPONENTS or onclick:
136
- target = idm.group(1) if idm else (txtm.group(1) if txtm else None)
137
- if comp == 'Image' and not onclick:
138
- continue
139
- # 纯容器按行号定位且无文本无 id —— 噪声,剔除
140
- if not target and comp in ('Column', 'Row', 'Stack', 'Flex', 'ListItem', 'List', 'Grid', 'Refresh', 'TabContent'):
141
- continue
142
- interactions.append({
143
- 'kind': 'click' if comp != 'TextInput' else 'input',
144
- 'target': target or f'line{i+1}:{comp}',
145
- 'desc': (txtm.group(1) if txtm else comp),
146
- 'has_id': bool(idm), 'line': i + 1, 'type': comp
147
- })
148
- # router 跳转
149
- for nm in re.finditer(r"router\.(?:pushUrl|replaceUrl)\(\s*{\s*url:\s*'([^']+)'", after):
150
- navs.append(nm.group(1))
151
-
152
- # 条件状态(if 分支)
153
- for i, ln in enumerate(lines):
154
- if re.match(r'^\s*if\s*\(', ln):
155
- cond = ln.strip()
156
- states.append({'name': cond[:60], 'evidence': cond, 'line': i + 1})
157
- return interactions, states[:20], navs, data_sources
158
-
159
-
160
- def main():
161
- ap = argparse.ArgumentParser()
162
- ap.add_argument('harmony')
163
- ap.add_argument('--out', default=None)
164
- args = ap.parse_args()
165
- root = args.harmony
166
-
167
- app_json5 = os.path.join(root, 'AppScope', 'app.json5')
168
- bundle = ''
169
- m = re.search(r'"bundleName"\s*:\s*"([^"]+)"', read(app_json5))
170
- if m:
171
- bundle = m.group(1)
172
-
173
- modules, pages, entry_module, entry_page = find_pages(root)
174
- if not pages:
175
- print('错误: 未找到任何页面(路由表或 @Entry)', file=sys.stderr)
176
- sys.exit(1)
177
-
178
- # 权限(各模块 module.json5)
179
- perms = []
180
- for mdir in modules:
181
- mj = os.path.join(root, mdir, 'src', 'main', 'module.json5')
182
- perms += re.findall(r'"name"\s*:\s*"(ohos\.permission\.[\w.]+)"', read(mj))
183
- perms = sorted(set(perms))
184
-
185
- # 登录检测
186
- has_login = False
187
- for (mname, route), f in pages.items():
188
- src = read(f) if f else ''
189
- if any(h in src for h in LOGIN_HINTS):
190
- # 有登录字样的页面
191
- if re.search(r'登录|login|Login', src):
192
- has_login = True
193
-
194
- result = {'project_dir': os.path.abspath(root), 'bundle': bundle,
195
- 'entry': {'module': entry_module, 'page': entry_page},
196
- 'pages': [], 'permissions': perms, 'has_login': has_login,
197
- 'nav_graph': {}}
198
-
199
- n_int = n_state = 0
200
- for (mname, route), f in sorted(pages.items()):
201
- if not f:
202
- continue
203
- interactions, states, navs, data_sources = scan_page(f)
204
- result['pages'].append({
205
- 'page': os.path.basename(route), 'path': route, 'module': mname,
206
- 'file': os.path.abspath(f),
207
- 'nav_in': ['entry'] if route == entry_page else ['router:见 nav_graph'],
208
- 'interactions': interactions, 'states': states,
209
- 'data_sources': data_sources
210
- })
211
- result['nav_graph'][route] = sorted(set(navs))
212
- n_int += len(interactions)
213
- n_state += len(states)
214
-
215
- # ── 深扫:多模块架构的真实 UI 在 features/commons 的 @Component 组件里 ──
216
- seen_files = {p['file'] for p in result['pages']}
217
- for m in modules:
218
- ets_root = os.path.join(root, m, 'src', 'main', 'ets')
219
- if not os.path.isdir(ets_root):
220
- continue
221
- for dp, _d, files in os.walk(ets_root):
222
- for f in files:
223
- if not f.endswith('.ets'):
224
- continue
225
- fp = os.path.join(dp, f)
226
- if fp in seen_files:
227
- continue
228
- src = read(fp)
229
- # 含 build() 的组件(@Component/@ComponentV2 struct)才算 UI 单元
230
- if '@Component' in src and re.search(r'\bbuild\s*\(\s*\)', src):
231
- interactions, states, navs, data_sources = scan_page(fp)
232
- if not interactions and not states:
233
- continue
234
- rel = os.path.relpath(fp, os.path.join(root, m, 'src', 'main', 'ets'))[:-4]
235
- result['pages'].append({
236
- 'page': os.path.splitext(f)[0], 'path': f'comp:{m}/{rel}',
237
- 'module': m, 'file': os.path.abspath(fp),
238
- 'nav_in': ['builder/comp'], 'interactions': interactions,
239
- 'states': states, 'data_sources': data_sources
240
- })
241
- n_int += len(interactions)
242
- n_state += len(states)
243
-
244
- payload = json.dumps(result, indent=2, ensure_ascii=False)
245
- if args.out:
246
- os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
247
- open(args.out, 'w', encoding='utf-8').write(payload)
248
- print(f'扫描完成: {len(result["pages"])} 页面 / {n_int} 交互点 / {n_state} 状态 / '
249
- f'{len(perms)} 权限 / 登录页={"有" if has_login else "无"} → {args.out}')
250
- else:
251
- print(payload)
252
-
253
-
254
- if __name__ == '__main__':
255
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ scan_features.py — 扫描鸿蒙工程,产出功能场景矩阵(features.json + features.md)。
4
+
5
+ 场景矩阵模型(覆盖度评估的基础):
6
+ 页面(路由可达) × 交互点(可操作控件/事件) × 状态(数据/条件分支)
7
+
8
+ 输出 features.json:
9
+ {
10
+ "project_dir": "...",
11
+ "bundle": "hm.xxx",
12
+ "pages": [
13
+ {
14
+ "page": "Index", "path": "pages/Index", "file": "...",
15
+ "nav_in": ["入口"] , # 如何到达(入口/router.pushUrl 链)
16
+ "interactions": [ # 交互点(含已有 ID)
17
+ {"kind": "click", "target": "id_or_text", "desc": "设置按钮", "has_id": true}
18
+ ],
19
+ "states": [ # 可区分状态(条件渲染/数据驱动)
20
+ {"name": "列表为空", "evidence": "if (!items.length)", "line": 42}
21
+ ],
22
+ "data_sources": ["mock", "http", "preferences"]
23
+ }
24
+ ],
25
+ "permissions": ["ohos.permission.INTERNET", ...], # 需预授/确认的权限
26
+ "has_login": true|false, # 登录页检测
27
+ "nav_graph": {"pages/Index": ["pages/Settings", ...]} # 静态跳转图
28
+ }
29
+
30
+ 用法: scan_features.py <harmony_project_dir> [--out features.json]
31
+ """
32
+ import argparse
33
+ import json
34
+ import os
35
+ import re
36
+ import sys
37
+
38
+ # 登录页特征
39
+ LOGIN_HINTS = ['登录', 'login', 'Login', 'signin', 'SignIn', '账号', '密码', 'password', '手机号']
40
+ # 数据源特征
41
+ DATA_HINTS = {
42
+ 'http': re.compile(r'https?://|@ohos\.net\.http|axios|rcp'),
43
+ 'preferences': re.compile(r'@ohos\.data\.preferences|preferences\.getPreferences'),
44
+ 'mock': re.compile(r'\bmock\b|MockData|EMOJI_DATA|SAMPLE_', re.I),
45
+ 'db': re.compile(r'@ohos\.data\.relationalStore|relationalStore'),
46
+ }
47
+ # 交互控件(组件级)
48
+ INTERACTIVE_COMPONENTS = ('Button', 'TextInput', 'TextArea', 'Toggle', 'Slider',
49
+ 'Radio', 'Checkbox', 'Select', 'Search', 'Stepper',
50
+ 'Rating', 'DatePicker', 'TextPicker', 'TimePicker',
51
+ 'Image', 'Hyperlink', 'Navigator')
52
+ # 非组件关键词(生命周期/builder/容器等误判源)
53
+ NON_COMPONENT = re.compile(r'^(onPageShow|onBackPress|aboutToAppear|aboutToDisappear|'
54
+ r'\w*[Bb]uilder|TabContent|ForEach|LazyForEach|Repeat|'
55
+ r'Navigation|NavDestination|if|else|while|for|struct|class|build)\b')
56
+ # 风险权限(需用户确认/无法预授)
57
+ RISKY_PERMS = {'ohos.permission.USER_AUTH', 'ohos.permission.MANAGE_FINGERPRINT'}
58
+
59
+
60
+ def read(p):
61
+ try:
62
+ return open(p, encoding='utf-8', errors='ignore').read()
63
+ except OSError:
64
+ return ''
65
+
66
+
67
+ def find_pages(root):
68
+ """定位入口模块与页面集。"""
69
+ # 找含 UIAbility+loadContent 的模块(entry 或自定义)
70
+ modules = []
71
+ for cand in sorted(os.listdir(root)):
72
+ mdir = os.path.join(root, cand)
73
+ if os.path.isdir(mdir) and os.path.isdir(os.path.join(mdir, 'src', 'main')):
74
+ modules.append(cand)
75
+ elif os.path.isdir(mdir) and cand not in ('oh_modules', '.hypium_forge', 'AppScope', 'hvigor', 'build', 'doc'):
76
+ # 二级模块容器(features/commons 下可能有独立模块)
77
+ for sub in sorted(os.listdir(mdir)):
78
+ sdir = os.path.join(mdir, sub)
79
+ if os.path.isdir(os.path.join(sdir, 'src', 'main')):
80
+ modules.append(f'{cand}/{sub}')
81
+
82
+ pages = {}
83
+ nav_graph = {}
84
+ entry_module, entry_page = None, None
85
+ for m in modules:
86
+ mdir = os.path.join(root, m, 'src', 'main')
87
+ # Ability 的 loadContent
88
+ for dp, _d, files in os.walk(os.path.join(mdir, 'ets')):
89
+ for f in files:
90
+ if f.endswith('.ets'):
91
+ src = read(os.path.join(dp, f))
92
+ mm = re.search(r"loadContent\(\s*'([^']+)'", src)
93
+ if mm and entry_page is None:
94
+ entry_module, entry_page = m, mm.group(1)
95
+ # 路由表(可能多个)
96
+ for dp, _d, files in os.walk(os.path.join(mdir, 'resources')):
97
+ for f in files:
98
+ if f.endswith('_pages.json'):
99
+ try:
100
+ routes = json.load(open(os.path.join(dp, f))).get('src', [])
101
+ except (OSError, json.JSONDecodeError):
102
+ routes = []
103
+ for r in routes:
104
+ pages.setdefault((m, r), None)
105
+ # @Entry 页面兜底(覆盖路由表留下的 None 值)
106
+ ets_root = os.path.join(mdir, 'ets')
107
+ for dp, _d, files in os.walk(ets_root):
108
+ for f in files:
109
+ if f.endswith('.ets') and '@Entry' in read(os.path.join(dp, f)):
110
+ rel = os.path.relpath(os.path.join(dp, f), ets_root)[:-4]
111
+ if pages.get((m, rel)) is None:
112
+ pages[(m, rel)] = os.path.join(dp, f)
113
+ return modules, pages, entry_module, entry_page
114
+
115
+
116
+ def scan_page(file_path):
117
+ """单页扫描:交互点/状态/数据源/跳转。"""
118
+ src = read(file_path)
119
+ lines = src.split('\n')
120
+ interactions, states, navs = [], [], []
121
+ data_sources = [k for k, rx in DATA_HINTS.items() if rx.search(src)]
122
+
123
+ # 组件构造 + 邻近 .id()/文本
124
+ for i, ln in enumerate(lines):
125
+ m = re.match(r'^\s*(\w+)\s*(?:\(\s*(?:{[^}]*})?\s*\)?\s*(?:\{\s*)?)$', ln) or \
126
+ re.match(r'^\s*(Button|Text|Image|Toggle|TextInput)\s*\(', ln)
127
+ if not m or NON_COMPONENT.match(m.group(1)):
128
+ continue
129
+ comp = m.group(1)
130
+ # 向后 15 行找 .id / 文本内容 / onClick
131
+ after = '\n'.join(lines[i:min(len(lines), i + 15)])
132
+ idm = re.search(r"\.id\(\s*'([^']+)'", after)
133
+ txtm = re.search(r"(?:Text|Button)\s*\(\s*(?:this\.)?[\w.]*?'([^']{1,24})'", '\n'.join(lines[max(0,i-1):i+6]))
134
+ onclick = bool(re.search(r'\.on[A-Z]\w*\(', after))
135
+ if comp in INTERACTIVE_COMPONENTS or onclick:
136
+ target = idm.group(1) if idm else (txtm.group(1) if txtm else None)
137
+ if comp == 'Image' and not onclick:
138
+ continue
139
+ # 纯容器按行号定位且无文本无 id —— 噪声,剔除
140
+ if not target and comp in ('Column', 'Row', 'Stack', 'Flex', 'ListItem', 'List', 'Grid', 'Refresh', 'TabContent'):
141
+ continue
142
+ interactions.append({
143
+ 'kind': 'click' if comp != 'TextInput' else 'input',
144
+ 'target': target or f'line{i+1}:{comp}',
145
+ 'desc': (txtm.group(1) if txtm else comp),
146
+ 'has_id': bool(idm), 'line': i + 1, 'type': comp
147
+ })
148
+ # router 跳转
149
+ for nm in re.finditer(r"router\.(?:pushUrl|replaceUrl)\(\s*{\s*url:\s*'([^']+)'", after):
150
+ navs.append(nm.group(1))
151
+
152
+ # 条件状态(if 分支)
153
+ for i, ln in enumerate(lines):
154
+ if re.match(r'^\s*if\s*\(', ln):
155
+ cond = ln.strip()
156
+ states.append({'name': cond[:60], 'evidence': cond, 'line': i + 1})
157
+ return interactions, states[:20], navs, data_sources
158
+
159
+
160
+ def main():
161
+ ap = argparse.ArgumentParser()
162
+ ap.add_argument('harmony')
163
+ ap.add_argument('--out', default=None)
164
+ args = ap.parse_args()
165
+ root = args.harmony
166
+
167
+ app_json5 = os.path.join(root, 'AppScope', 'app.json5')
168
+ bundle = ''
169
+ m = re.search(r'"bundleName"\s*:\s*"([^"]+)"', read(app_json5))
170
+ if m:
171
+ bundle = m.group(1)
172
+
173
+ modules, pages, entry_module, entry_page = find_pages(root)
174
+ if not pages:
175
+ print('错误: 未找到任何页面(路由表或 @Entry)', file=sys.stderr)
176
+ sys.exit(1)
177
+
178
+ # 权限(各模块 module.json5)
179
+ perms = []
180
+ for mdir in modules:
181
+ mj = os.path.join(root, mdir, 'src', 'main', 'module.json5')
182
+ perms += re.findall(r'"name"\s*:\s*"(ohos\.permission\.[\w.]+)"', read(mj))
183
+ perms = sorted(set(perms))
184
+
185
+ # 登录检测
186
+ has_login = False
187
+ for (mname, route), f in pages.items():
188
+ src = read(f) if f else ''
189
+ if any(h in src for h in LOGIN_HINTS):
190
+ # 有登录字样的页面
191
+ if re.search(r'登录|login|Login', src):
192
+ has_login = True
193
+
194
+ result = {'project_dir': os.path.abspath(root), 'bundle': bundle,
195
+ 'entry': {'module': entry_module, 'page': entry_page},
196
+ 'pages': [], 'permissions': perms, 'has_login': has_login,
197
+ 'nav_graph': {}}
198
+
199
+ n_int = n_state = 0
200
+ for (mname, route), f in sorted(pages.items()):
201
+ if not f:
202
+ continue
203
+ interactions, states, navs, data_sources = scan_page(f)
204
+ result['pages'].append({
205
+ 'page': os.path.basename(route), 'path': route, 'module': mname,
206
+ 'file': os.path.abspath(f),
207
+ 'nav_in': ['entry'] if route == entry_page else ['router:见 nav_graph'],
208
+ 'interactions': interactions, 'states': states,
209
+ 'data_sources': data_sources
210
+ })
211
+ result['nav_graph'][route] = sorted(set(navs))
212
+ n_int += len(interactions)
213
+ n_state += len(states)
214
+
215
+ # ── 深扫:多模块架构的真实 UI 在 features/commons 的 @Component 组件里 ──
216
+ seen_files = {p['file'] for p in result['pages']}
217
+ for m in modules:
218
+ ets_root = os.path.join(root, m, 'src', 'main', 'ets')
219
+ if not os.path.isdir(ets_root):
220
+ continue
221
+ for dp, _d, files in os.walk(ets_root):
222
+ for f in files:
223
+ if not f.endswith('.ets'):
224
+ continue
225
+ fp = os.path.join(dp, f)
226
+ if fp in seen_files:
227
+ continue
228
+ src = read(fp)
229
+ # 含 build() 的组件(@Component/@ComponentV2 struct)才算 UI 单元
230
+ if '@Component' in src and re.search(r'\bbuild\s*\(\s*\)', src):
231
+ interactions, states, navs, data_sources = scan_page(fp)
232
+ if not interactions and not states:
233
+ continue
234
+ rel = os.path.relpath(fp, os.path.join(root, m, 'src', 'main', 'ets'))[:-4]
235
+ result['pages'].append({
236
+ 'page': os.path.splitext(f)[0], 'path': f'comp:{m}/{rel}',
237
+ 'module': m, 'file': os.path.abspath(fp),
238
+ 'nav_in': ['builder/comp'], 'interactions': interactions,
239
+ 'states': states, 'data_sources': data_sources
240
+ })
241
+ n_int += len(interactions)
242
+ n_state += len(states)
243
+
244
+ payload = json.dumps(result, indent=2, ensure_ascii=False)
245
+ if args.out:
246
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
247
+ open(args.out, 'w', encoding='utf-8').write(payload)
248
+ print(f'扫描完成: {len(result["pages"])} 页面 / {n_int} 交互点 / {n_state} 状态 / '
249
+ f'{len(perms)} 权限 / 登录页={"有" if has_login else "无"} → {args.out}')
250
+ else:
251
+ print(payload)
252
+
253
+
254
+ if __name__ == '__main__':
255
+ main()
@@ -1,66 +1,66 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """upgrade_asserts.py — 存量用例断言批量升级(EXEC-041)。
4
-
5
- 目标:把「仅 exist」的用例批量升级为值级断言,让存量测试具备发现能力。
6
-
7
- 升级规则(保守机械,不造新值——只把已有期望变强):
8
- R1 expect:"exist" + 文本型 target(非 snake_case id)→ expect:"equals"(同 target)
9
- 理由:equals 是 EQUALS 精确匹配,能抓 CONTAINS 命中的前缀污染(❄️Gallery 类)
10
- R2 expect:"exist" + key 型 target → 保留(页面锚点合法),但在用例末尾若
11
- 仍无值级断言,追加 count 断言(target=该 key,value=1,唯一性)
12
- R3 absent/checked 等已是值级,不动
13
- 每次升级在 note 里追加留痕(防驯化审计)。
14
-
15
- 用法: python3 upgrade_asserts.py [--cases cases.json] [--dry-run]
16
- """
17
- import argparse
18
- import json
19
- import re
20
-
21
- ID_LIKE = re.compile(r'^(tc_[a-z0-9_]+|[a-z][a-z0-9]*(_[a-z0-9]+)+|line\d+:\w+)$')
22
-
23
-
24
- def upgrade(cases, dry=False):
25
- changed = 0
26
- for c in cases:
27
- has_value = any(s.get('expect') in
28
- ('equals', 'absent', 'text', 'text_contains', 'count',
29
- 'below', 'checked', 'unchecked', 'enabled', 'disabled', 'toast')
30
- for s in c.get('steps', []))
31
- for s in c.get('steps', []):
32
- if s.get('expect') != 'exist':
33
- continue
34
- t = str(s.get('target', ''))
35
- if t and not ID_LIKE.match(t):
36
- # R1: 文本期望 → 精确
37
- s['expect'] = 'equals'
38
- c.setdefault('note', '')
39
- c['note'] += f"[断言升级R1 exist→equals: {t[:20]}]"
40
- changed += 1
41
- elif t and ID_LIKE.match(t):
42
- # R2: key 锚点保留,仅当全例无值级时补 count
43
- if not has_value and s is c['steps'][-1]:
44
- c['steps'].append({'action': '断言升级R2:控件唯一性', 'target': t,
45
- 'expect': 'count', 'value': '1'})
46
- c['note'] = (c.get('note', '') + f"[断言升级R2 补count: {t[:20]}]")
47
- changed += 1
48
- return changed
49
-
50
-
51
- def main():
52
- ap = argparse.ArgumentParser()
53
- ap.add_argument('--cases', default='cases.json')
54
- ap.add_argument('--dry-run', action='store_true')
55
- args = ap.parse_args()
56
- cases = json.load(open(args.cases, encoding='utf-8'))
57
- n = upgrade(cases, args.dry_run)
58
- if args.dry_run:
59
- print(f'[dry-run] 将升级 {n} 处')
60
- else:
61
- json.dump(cases, open(args.cases, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
62
- print(f'[written] 升级 {n} 处 → {args.cases}')
63
-
64
-
65
- if __name__ == '__main__':
66
- main()
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """upgrade_asserts.py — 存量用例断言批量升级(EXEC-041)。
4
+
5
+ 目标:把「仅 exist」的用例批量升级为值级断言,让存量测试具备发现能力。
6
+
7
+ 升级规则(保守机械,不造新值——只把已有期望变强):
8
+ R1 expect:"exist" + 文本型 target(非 snake_case id)→ expect:"equals"(同 target)
9
+ 理由:equals 是 EQUALS 精确匹配,能抓 CONTAINS 命中的前缀污染(❄️Gallery 类)
10
+ R2 expect:"exist" + key 型 target → 保留(页面锚点合法),但在用例末尾若
11
+ 仍无值级断言,追加 count 断言(target=该 key,value=1,唯一性)
12
+ R3 absent/checked 等已是值级,不动
13
+ 每次升级在 note 里追加留痕(防驯化审计)。
14
+
15
+ 用法: python3 upgrade_asserts.py [--cases cases.json] [--dry-run]
16
+ """
17
+ import argparse
18
+ import json
19
+ import re
20
+
21
+ ID_LIKE = re.compile(r'^(tc_[a-z0-9_]+|[a-z][a-z0-9]*(_[a-z0-9]+)+|line\d+:\w+)$')
22
+
23
+
24
+ def upgrade(cases, dry=False):
25
+ changed = 0
26
+ for c in cases:
27
+ has_value = any(s.get('expect') in
28
+ ('equals', 'absent', 'text', 'text_contains', 'count',
29
+ 'below', 'checked', 'unchecked', 'enabled', 'disabled', 'toast')
30
+ for s in c.get('steps', []))
31
+ for s in c.get('steps', []):
32
+ if s.get('expect') != 'exist':
33
+ continue
34
+ t = str(s.get('target', ''))
35
+ if t and not ID_LIKE.match(t):
36
+ # R1: 文本期望 → 精确
37
+ s['expect'] = 'equals'
38
+ c.setdefault('note', '')
39
+ c['note'] += f"[断言升级R1 exist→equals: {t[:20]}]"
40
+ changed += 1
41
+ elif t and ID_LIKE.match(t):
42
+ # R2: key 锚点保留,仅当全例无值级时补 count
43
+ if not has_value and s is c['steps'][-1]:
44
+ c['steps'].append({'action': '断言升级R2:控件唯一性', 'target': t,
45
+ 'expect': 'count', 'value': '1'})
46
+ c['note'] = (c.get('note', '') + f"[断言升级R2 补count: {t[:20]}]")
47
+ changed += 1
48
+ return changed
49
+
50
+
51
+ def main():
52
+ ap = argparse.ArgumentParser()
53
+ ap.add_argument('--cases', default='cases.json')
54
+ ap.add_argument('--dry-run', action='store_true')
55
+ args = ap.parse_args()
56
+ cases = json.load(open(args.cases, encoding='utf-8'))
57
+ n = upgrade(cases, args.dry_run)
58
+ if args.dry_run:
59
+ print(f'[dry-run] 将升级 {n} 处')
60
+ else:
61
+ json.dump(cases, open(args.cases, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
62
+ print(f'[written] 升级 {n} 处 → {args.cases}')
63
+
64
+
65
+ if __name__ == '__main__':
66
+ main()