@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,114 +1,114 @@
1
- #!/usr/bin/env python3
2
- """
3
- inject_resource_ids.py — 资源化文案工程的 ID 注入器(DFT-010 场景)。
4
-
5
- 流程:target 文本 → string.json 资源名 → 全工程搜 $r('app.string.X') 引用点
6
- → 组件属性链内注入 .id('tc_<file>_<hash>')(幂等,已有 tc_ id 复用)
7
-
8
- 输出 injected_ids.json(target → id 映射,供脚本生成器消费)
9
- 用法: inject_resource_ids.py <harmony_root> <targets_json文件或逗号分隔> [--cases cases.json]
10
- """
11
- import argparse
12
- import json
13
- import os
14
- import re
15
- import hashlib
16
-
17
-
18
- def build_res_map(root):
19
- res_map = {}
20
- for dp, _d, files in os.walk(root):
21
- if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
22
- continue
23
- for f in files:
24
- if f == 'string.json':
25
- try:
26
- for item in json.load(open(os.path.join(dp, f), encoding='utf-8')).get('string', []):
27
- res_map.setdefault(item['value'], item['name'])
28
- except Exception:
29
- pass
30
- return res_map
31
-
32
-
33
- def collect_src(root):
34
- out = []
35
- for dp, _d, files in os.walk(root):
36
- if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
37
- continue
38
- for f in files:
39
- if f.endswith('.ets'):
40
- out.append(os.path.join(dp, f))
41
- return out
42
-
43
-
44
- ID_RE = re.compile(r"\.id\('tc_[a-z0-9_]+'\)")
45
-
46
-
47
- def inject_in_file(sf, rx):
48
- """在 sf 中找 rx 引用行,属性链内注入 id;返回 (tid or None)。幂等。"""
49
- src = open(sf, encoding='utf-8', errors='ignore').read()
50
- lines = src.split('\n')
51
- for i, ln in enumerate(lines):
52
- if rx.search(ln):
53
- seg = '\n'.join(lines[i:i + 12])
54
- m = ID_RE.search(seg)
55
- if m:
56
- return m.group(0)[5:-2] # 已有 → 复用
57
- for k in range(i, min(len(lines), i + 10)):
58
- if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
59
- indent = re.match(r'(\s*)', lines[k]).group(1)
60
- tid = 'tc_' + os.path.splitext(os.path.basename(sf))[0].lower() + '_' + \
61
- hashlib.md5(ln.encode()).hexdigest()[:6]
62
- lines.insert(k, f"{indent}.id('{tid}')")
63
- open(sf, 'w', encoding='utf-8').write('\n'.join(lines))
64
- return tid
65
- return None
66
-
67
-
68
- def main():
69
- ap = argparse.ArgumentParser()
70
- ap.add_argument('root')
71
- ap.add_argument('--cases', help='forge 格式 cases.json(取 target)')
72
- ap.add_argument('--targets', help='逗号分隔 target 列表(二选一)')
73
- ap.add_argument('--out', default=None)
74
- args = ap.parse_args()
75
-
76
- targets = []
77
- if args.cases:
78
- for c in json.load(open(args.cases, encoding='utf-8')):
79
- for s in c.get('steps', []):
80
- t = s.get('target')
81
- if t and not t.startswith('请输入') and t != '数量':
82
- targets.append(t)
83
- else:
84
- targets = args.targets.split(',')
85
- targets = list(dict.fromkeys(targets))
86
-
87
- res_map = build_res_map(args.root)
88
- src_files = collect_src(args.root)
89
-
90
- injected, unresolved = {}, []
91
- for t in targets:
92
- resname = res_map.get(t)
93
- tid = None
94
- if resname:
95
- rx = re.compile(r"\$r\(\s*['\"]app\.string\." + re.escape(resname) + r"['\"]")
96
- for sf in src_files:
97
- tid = inject_in_file(sf, rx)
98
- if tid:
99
- injected[t] = tid
100
- break
101
- if not tid:
102
- unresolved.append(t)
103
-
104
- out = args.out or os.path.join(args.root, '.hypium_forge', 'injected_ids.json')
105
- json.dump(injected, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
106
- print(f'资源命中注入: {len(injected)} / 未解析: {len(unresolved)} → {out}')
107
- for t, tid in sorted(injected.items()):
108
- print(f' + {t} → {tid}')
109
- if unresolved:
110
- print('未解析(动态文案/图标按钮,脚本走文本/兜底定位):', unresolved[:15])
111
-
112
-
113
- if __name__ == '__main__':
114
- main()
1
+ #!/usr/bin/env python3
2
+ """
3
+ inject_resource_ids.py — 资源化文案工程的 ID 注入器(DFT-010 场景)。
4
+
5
+ 流程:target 文本 → string.json 资源名 → 全工程搜 $r('app.string.X') 引用点
6
+ → 组件属性链内注入 .id('tc_<file>_<hash>')(幂等,已有 tc_ id 复用)
7
+
8
+ 输出 injected_ids.json(target → id 映射,供脚本生成器消费)
9
+ 用法: inject_resource_ids.py <harmony_root> <targets_json文件或逗号分隔> [--cases cases.json]
10
+ """
11
+ import argparse
12
+ import json
13
+ import os
14
+ import re
15
+ import hashlib
16
+
17
+
18
+ def build_res_map(root):
19
+ res_map = {}
20
+ for dp, _d, files in os.walk(root):
21
+ if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
22
+ continue
23
+ for f in files:
24
+ if f == 'string.json':
25
+ try:
26
+ for item in json.load(open(os.path.join(dp, f), encoding='utf-8')).get('string', []):
27
+ res_map.setdefault(item['value'], item['name'])
28
+ except Exception:
29
+ pass
30
+ return res_map
31
+
32
+
33
+ def collect_src(root):
34
+ out = []
35
+ for dp, _d, files in os.walk(root):
36
+ if 'oh_modules' in dp or '/build' in dp or '.testdesign' in dp or '.hypium_forge' in dp:
37
+ continue
38
+ for f in files:
39
+ if f.endswith('.ets'):
40
+ out.append(os.path.join(dp, f))
41
+ return out
42
+
43
+
44
+ ID_RE = re.compile(r"\.id\('tc_[a-z0-9_]+'\)")
45
+
46
+
47
+ def inject_in_file(sf, rx):
48
+ """在 sf 中找 rx 引用行,属性链内注入 id;返回 (tid or None)。幂等。"""
49
+ src = open(sf, encoding='utf-8', errors='ignore').read()
50
+ lines = src.split('\n')
51
+ for i, ln in enumerate(lines):
52
+ if rx.search(ln):
53
+ seg = '\n'.join(lines[i:i + 12])
54
+ m = ID_RE.search(seg)
55
+ if m:
56
+ return m.group(0)[5:-2] # 已有 → 复用
57
+ for k in range(i, min(len(lines), i + 10)):
58
+ if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
59
+ indent = re.match(r'(\s*)', lines[k]).group(1)
60
+ tid = 'tc_' + os.path.splitext(os.path.basename(sf))[0].lower() + '_' + \
61
+ hashlib.md5(ln.encode()).hexdigest()[:6]
62
+ lines.insert(k, f"{indent}.id('{tid}')")
63
+ open(sf, 'w', encoding='utf-8').write('\n'.join(lines))
64
+ return tid
65
+ return None
66
+
67
+
68
+ def main():
69
+ ap = argparse.ArgumentParser()
70
+ ap.add_argument('root')
71
+ ap.add_argument('--cases', help='forge 格式 cases.json(取 target)')
72
+ ap.add_argument('--targets', help='逗号分隔 target 列表(二选一)')
73
+ ap.add_argument('--out', default=None)
74
+ args = ap.parse_args()
75
+
76
+ targets = []
77
+ if args.cases:
78
+ for c in json.load(open(args.cases, encoding='utf-8')):
79
+ for s in c.get('steps', []):
80
+ t = s.get('target')
81
+ if t and not t.startswith('请输入') and t != '数量':
82
+ targets.append(t)
83
+ else:
84
+ targets = args.targets.split(',')
85
+ targets = list(dict.fromkeys(targets))
86
+
87
+ res_map = build_res_map(args.root)
88
+ src_files = collect_src(args.root)
89
+
90
+ injected, unresolved = {}, []
91
+ for t in targets:
92
+ resname = res_map.get(t)
93
+ tid = None
94
+ if resname:
95
+ rx = re.compile(r"\$r\(\s*['\"]app\.string\." + re.escape(resname) + r"['\"]")
96
+ for sf in src_files:
97
+ tid = inject_in_file(sf, rx)
98
+ if tid:
99
+ injected[t] = tid
100
+ break
101
+ if not tid:
102
+ unresolved.append(t)
103
+
104
+ out = args.out or os.path.join(args.root, '.hypium_forge', 'injected_ids.json')
105
+ json.dump(injected, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
106
+ print(f'资源命中注入: {len(injected)} / 未解析: {len(unresolved)} → {out}')
107
+ for t, tid in sorted(injected.items()):
108
+ print(f' + {t} → {tid}')
109
+ if unresolved:
110
+ print('未解析(动态文案/图标按钮,脚本走文本/兜底定位):', unresolved[:15])
111
+
112
+
113
+ if __name__ == '__main__':
114
+ main()
@@ -1,80 +1,80 @@
1
- #!/usr/bin/env python3
2
- """lint_asserts.py — 断言强度门禁(白盒直写形态,2026-09-29 none 臂对照实证落地)。
3
-
4
- 背景:none 臂(无 skill)LLM 凭通用能力达成 34% 强断言(hilog 防污染/dump Toast 轮询/
5
- 剪贴板),B 线(抄模板)仅 15%——模板能力边界效应。本门禁把下限固化:
6
- - 强断言步骤占比 ≥ 30%(默认阈值),低于即 FAIL
7
- - vacuous 用例点名(纯点击链、无任何值级断言)——对应 assert_audit.py 语义(.py 版)
8
-
9
- 值级断言原语:checked/unchecked/enabled/equals/text_equals/text_absent/toast_text_seen/
10
- hilog_tail(关键字)/pasteboard/剪贴板/_dump_layout 断言性使用/text_contains/count
11
- 用法: python3 lint_asserts.py --dir testcases [--threshold 30]
12
- """
13
- import argparse
14
- import glob
15
- import os
16
- import re
17
- import sys
18
-
19
- STRONG_CALLS = re.compile(
20
- r'\b(checked|unchecked|enabled\(|disabled|text_equals|text_absent|toast_text_seen|'
21
- r'hilog_clear|hilog_tail|pasteboard|剪贴板|text_contains|find_all_components\s*\(\s*BY\.)'
22
- r'[^)]*\)|\bchecked\s*\(\s*True\)|assert\s')
23
- STEP_PAT = re.compile(r'^\s*(?:Step\(|step_shot\(|#)', re.M)
24
- ACTION_PAT = re.compile(r'\b(id_click|text_click|click_text|click_tab|id_input|d\.touch|press_back|swipe|scroll_click)\b')
25
-
26
-
27
- STRONG_STEP_WORDS = ('断言', '校验', 'equals', 'checked', 'absent', 'toast', 'hilog',
28
- '剪贴板', 'pasteboard', '包含', '等于', '消失', '翻转为', '已开启', '已关闭',
29
- '一致性', '业务数据', '关键字', 'dump')
30
-
31
-
32
- VALUE_CALLS = re.compile(
33
- r'\b(text_equals|text_absent|toast_text_seen|hilog_clear|hilog_tail|pasteboard|'
34
- r'find_all_components|checked\s*\(|asserts?\s*\.|\.assert)')
35
-
36
-
37
- def audit_file(path):
38
- """双轨审计(两种白盒写法都覆盖):
39
- 轨1 Step 结构:Step 描述含值级断言语义词 = 强步骤;
40
- 轨2 无 Step(内联代码):值级断言调用数 / (值级调用+动作调用) 为强度。"""
41
- src = open(path, encoding='utf-8').read()
42
- steps_all = re.findall(r'Step\(\s*"([^"]+)"', src)
43
- value_calls = len(VALUE_CALLS.findall(src))
44
- if steps_all:
45
- strong = sum(1 for t in steps_all if any(w in t.lower() for w in STRONG_STEP_WORDS))
46
- return len(steps_all), 0, strong, strong > 0 or value_calls > 0
47
- actions = len(ACTION_PAT.findall(src))
48
- return value_calls + actions, 0, value_calls, value_calls > 0
49
-
50
-
51
- def main():
52
- ap = argparse.ArgumentParser()
53
- ap.add_argument('--dir', required=True)
54
- ap.add_argument('--threshold', type=int, default=30)
55
- args = ap.parse_args()
56
- total_steps = total_strong = vacuous = n = 0
57
- vacuous_list = []
58
- for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
59
- steps, marks, strong_steps, has_value = audit_file(p)
60
- n += 1
61
- total_steps += steps
62
- total_strong += (strong_steps or marks)
63
- if not has_value:
64
- vacuous += 1
65
- vacuous_list.append(os.path.basename(p))
66
- pct = round(total_strong / total_steps * 100, 1) if total_steps else 0
67
- print(f'断言强度: 强断言 {total_strong}/{total_steps} 步 = {pct}%(下限 {args.threshold}%)')
68
- if vacuous_list:
69
- print(f'⚠ vacuous 用例(无任何值级断言){vacuous} 个: {", ".join(vacuous_list[:8])}')
70
- if n and pct < args.threshold:
71
- print(f'✗ FAIL:断言强度 {pct}% < {args.threshold}%——按场景目的补值级断言(最佳区间 40~70%)'
72
- f'(Toast→toast_text_seen;系统事件→hilog 轮询+用例前 hilog_clear;'
73
- f'状态→checked;文案→text_equals;消失→text_absent)后重检')
74
- sys.exit(1)
75
- print('✓ PASS')
76
- sys.exit(0)
77
-
78
-
79
- if __name__ == '__main__':
80
- main()
1
+ #!/usr/bin/env python3
2
+ """lint_asserts.py — 断言强度门禁(白盒直写形态,2026-09-29 none 臂对照实证落地)。
3
+
4
+ 背景:none 臂(无 skill)LLM 凭通用能力达成 34% 强断言(hilog 防污染/dump Toast 轮询/
5
+ 剪贴板),B 线(抄模板)仅 15%——模板能力边界效应。本门禁把下限固化:
6
+ - 强断言步骤占比 ≥ 30%(默认阈值),低于即 FAIL
7
+ - vacuous 用例点名(纯点击链、无任何值级断言)——对应 assert_audit.py 语义(.py 版)
8
+
9
+ 值级断言原语:checked/unchecked/enabled/equals/text_equals/text_absent/toast_text_seen/
10
+ hilog_tail(关键字)/pasteboard/剪贴板/_dump_layout 断言性使用/text_contains/count
11
+ 用法: python3 lint_asserts.py --dir testcases [--threshold 30]
12
+ """
13
+ import argparse
14
+ import glob
15
+ import os
16
+ import re
17
+ import sys
18
+
19
+ STRONG_CALLS = re.compile(
20
+ r'\b(checked|unchecked|enabled\(|disabled|text_equals|text_absent|toast_text_seen|'
21
+ r'hilog_clear|hilog_tail|pasteboard|剪贴板|text_contains|find_all_components\s*\(\s*BY\.)'
22
+ r'[^)]*\)|\bchecked\s*\(\s*True\)|assert\s')
23
+ STEP_PAT = re.compile(r'^\s*(?:Step\(|step_shot\(|#)', re.M)
24
+ ACTION_PAT = re.compile(r'\b(id_click|text_click|click_text|click_tab|id_input|d\.touch|press_back|swipe|scroll_click)\b')
25
+
26
+
27
+ STRONG_STEP_WORDS = ('断言', '校验', 'equals', 'checked', 'absent', 'toast', 'hilog',
28
+ '剪贴板', 'pasteboard', '包含', '等于', '消失', '翻转为', '已开启', '已关闭',
29
+ '一致性', '业务数据', '关键字', 'dump')
30
+
31
+
32
+ VALUE_CALLS = re.compile(
33
+ r'\b(text_equals|text_absent|toast_text_seen|hilog_clear|hilog_tail|pasteboard|'
34
+ r'find_all_components|checked\s*\(|asserts?\s*\.|\.assert)')
35
+
36
+
37
+ def audit_file(path):
38
+ """双轨审计(两种白盒写法都覆盖):
39
+ 轨1 Step 结构:Step 描述含值级断言语义词 = 强步骤;
40
+ 轨2 无 Step(内联代码):值级断言调用数 / (值级调用+动作调用) 为强度。"""
41
+ src = open(path, encoding='utf-8').read()
42
+ steps_all = re.findall(r'Step\(\s*"([^"]+)"', src)
43
+ value_calls = len(VALUE_CALLS.findall(src))
44
+ if steps_all:
45
+ strong = sum(1 for t in steps_all if any(w in t.lower() for w in STRONG_STEP_WORDS))
46
+ return len(steps_all), 0, strong, strong > 0 or value_calls > 0
47
+ actions = len(ACTION_PAT.findall(src))
48
+ return value_calls + actions, 0, value_calls, value_calls > 0
49
+
50
+
51
+ def main():
52
+ ap = argparse.ArgumentParser()
53
+ ap.add_argument('--dir', required=True)
54
+ ap.add_argument('--threshold', type=int, default=30)
55
+ args = ap.parse_args()
56
+ total_steps = total_strong = vacuous = n = 0
57
+ vacuous_list = []
58
+ for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
59
+ steps, marks, strong_steps, has_value = audit_file(p)
60
+ n += 1
61
+ total_steps += steps
62
+ total_strong += (strong_steps or marks)
63
+ if not has_value:
64
+ vacuous += 1
65
+ vacuous_list.append(os.path.basename(p))
66
+ pct = round(total_strong / total_steps * 100, 1) if total_steps else 0
67
+ print(f'断言强度: 强断言 {total_strong}/{total_steps} 步 = {pct}%(下限 {args.threshold}%)')
68
+ if vacuous_list:
69
+ print(f'⚠ vacuous 用例(无任何值级断言){vacuous} 个: {", ".join(vacuous_list[:8])}')
70
+ if n and pct < args.threshold:
71
+ print(f'✗ FAIL:断言强度 {pct}% < {args.threshold}%——按场景目的补值级断言(最佳区间 40~70%)'
72
+ f'(Toast→toast_text_seen;系统事件→hilog 轮询+用例前 hilog_clear;'
73
+ f'状态→checked;文案→text_equals;消失→text_absent)后重检')
74
+ sys.exit(1)
75
+ print('✓ PASS')
76
+ sys.exit(0)
77
+
78
+
79
+ if __name__ == '__main__':
80
+ main()
@@ -1,60 +1,60 @@
1
- #!/usr/bin/env python3
2
- """lint_locate.py — 脚本定位方式自检门禁(铁律①的执行期强制)。
3
-
4
- 统计 testcases/*.py 的定位调用(id_click 等 BY.key 原语 vs text_click/click_text),
5
- text 定位占比超阈值(默认 20%)即 FAIL 并列出改写建议(ids.json 有映射优先)。
6
- 写完脚本后必须先过本门禁再执行。
7
-
8
- 用法: python3 lint_locate.py --dir testcases [--ids ids.json] [--threshold 20]
9
- """
10
- import argparse
11
- import glob
12
- import json
13
- import os
14
- import re
15
- import sys
16
-
17
- KEY_PRIMS = r'\bid_click\(|BY\.key\('
18
- TEXT_PRIMS = r'\btext_click\(|\bclick_text\(|BY\.text\((?![^)]*CONTAINS)|find_component\(\s*BY\.text'
19
-
20
-
21
- def main():
22
- ap = argparse.ArgumentParser()
23
- ap.add_argument('--dir', required=True)
24
- ap.add_argument('--ids', default=None)
25
- ap.add_argument('--threshold', type=int, default=20)
26
- args = ap.parse_args()
27
- ids = {}
28
- if args.ids and os.path.exists(args.ids):
29
- ids = json.load(open(args.ids, encoding='utf-8'))
30
- total_key = total_text = 0
31
- offenders = []
32
- for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
33
- src = open(p, encoding='utf-8').read()
34
- k = len(re.findall(KEY_PRIMS, src))
35
- t = len(re.findall(TEXT_PRIMS, src))
36
- total_key += k
37
- total_text += t
38
- if t > k:
39
- hint = ''
40
- m = re.findall(r'(?:text_click|click_text)\(\s*d\s*,\s*[\'"]([^\'"]+)[\'"]', src)
41
- mapped = [f"{x} → {ids[x]}" for x in m if x in ids][:3]
42
- if mapped:
43
- hint = ' | ids.json 有映射: ' + '; '.join(mapped)
44
- offenders.append(f"{os.path.basename(p)}: key={k} text={t}{hint}")
45
- total = total_key + total_text
46
- pct = round(total_key / total * 100) if total else 100
47
- print(f"定位统计: BY.key={total_key} / text={total_text} → key 占比 {pct}%(阈值 ≥{100 - args.threshold}%)")
48
- if offenders:
49
- print(f"text 主导的脚本 {len(offenders)} 个:")
50
- for o in offenders[:15]:
51
- print(' -', o)
52
- if total and pct < 100 - args.threshold:
53
- print(f"✗ FAIL:text 定位占比 {100 - pct}% > {args.threshold}% —— 须把 ids.json 有映射的 text_click 改写为 id_click 后重过门禁")
54
- sys.exit(1)
55
- print('✓ PASS')
56
- sys.exit(0)
57
-
58
-
59
- if __name__ == '__main__':
60
- main()
1
+ #!/usr/bin/env python3
2
+ """lint_locate.py — 脚本定位方式自检门禁(铁律①的执行期强制)。
3
+
4
+ 统计 testcases/*.py 的定位调用(id_click 等 BY.key 原语 vs text_click/click_text),
5
+ text 定位占比超阈值(默认 20%)即 FAIL 并列出改写建议(ids.json 有映射优先)。
6
+ 写完脚本后必须先过本门禁再执行。
7
+
8
+ 用法: python3 lint_locate.py --dir testcases [--ids ids.json] [--threshold 20]
9
+ """
10
+ import argparse
11
+ import glob
12
+ import json
13
+ import os
14
+ import re
15
+ import sys
16
+
17
+ KEY_PRIMS = r'\bid_click\(|BY\.key\('
18
+ TEXT_PRIMS = r'\btext_click\(|\bclick_text\(|BY\.text\((?![^)]*CONTAINS)|find_component\(\s*BY\.text'
19
+
20
+
21
+ def main():
22
+ ap = argparse.ArgumentParser()
23
+ ap.add_argument('--dir', required=True)
24
+ ap.add_argument('--ids', default=None)
25
+ ap.add_argument('--threshold', type=int, default=20)
26
+ args = ap.parse_args()
27
+ ids = {}
28
+ if args.ids and os.path.exists(args.ids):
29
+ ids = json.load(open(args.ids, encoding='utf-8'))
30
+ total_key = total_text = 0
31
+ offenders = []
32
+ for p in sorted(glob.glob(os.path.join(args.dir, 'TC*.py'))):
33
+ src = open(p, encoding='utf-8').read()
34
+ k = len(re.findall(KEY_PRIMS, src))
35
+ t = len(re.findall(TEXT_PRIMS, src))
36
+ total_key += k
37
+ total_text += t
38
+ if t > k:
39
+ hint = ''
40
+ m = re.findall(r'(?:text_click|click_text)\(\s*d\s*,\s*[\'"]([^\'"]+)[\'"]', src)
41
+ mapped = [f"{x} → {ids[x]}" for x in m if x in ids][:3]
42
+ if mapped:
43
+ hint = ' | ids.json 有映射: ' + '; '.join(mapped)
44
+ offenders.append(f"{os.path.basename(p)}: key={k} text={t}{hint}")
45
+ total = total_key + total_text
46
+ pct = round(total_key / total * 100) if total else 100
47
+ print(f"定位统计: BY.key={total_key} / text={total_text} → key 占比 {pct}%(阈值 ≥{100 - args.threshold}%)")
48
+ if offenders:
49
+ print(f"text 主导的脚本 {len(offenders)} 个:")
50
+ for o in offenders[:15]:
51
+ print(' -', o)
52
+ if total and pct < 100 - args.threshold:
53
+ print(f"✗ FAIL:text 定位占比 {100 - pct}% > {args.threshold}% —— 须把 ids.json 有映射的 text_click 改写为 id_click 后重过门禁")
54
+ sys.exit(1)
55
+ print('✓ PASS')
56
+ sys.exit(0)
57
+
58
+
59
+ if __name__ == '__main__':
60
+ main()