@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.
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,232 +1,232 @@
1
- #!/usr/bin/env python3
2
- """preinject_ids.py — ID 预置(铁律①的机械强制,先于脚本编写,不依赖 cases DSL)。
3
-
4
- 输入 features.json(scan_features 产物),对全部 has_id=False 的交互点批量注入
5
- `.id('tc_<page>_<n>')`(资源文案/精确模糊文本/行号三路定位,属性链内插桩永远合法),
6
- 输出 ids.json 映射(脚本编写只认此表与既有 ID)。注入后须 rebuild + uninstall&&install。
7
-
8
- 用法:
9
- python3 preinject_ids.py --features features.json [--out ids.json]
10
- """
11
- import argparse
12
- import glob
13
- import json
14
- import os
15
- import re
16
-
17
- TEXT_PAT = re.compile(
18
- r"^(\s*)(Text|Button|Toggle|Checkbox|Radio|MenuItem|Image|Row|Column)\("
19
- r"[^\n]*?['\"](.*?)['\"][^\n]*\)?\s*$")
20
-
21
-
22
- def read(p):
23
- return open(p, encoding='utf-8').read()
24
-
25
-
26
- def insert_after_component(lines, i):
27
- depth = 0
28
- started = False
29
- for k in range(i, min(len(lines), i + 40)):
30
- depth += lines[k].count('(') - lines[k].count(')')
31
- started = True
32
- if started and depth <= 0 and k > i:
33
- return k + 1
34
- if started and depth < 0:
35
- return k
36
- return min(len(lines), i + 12)
37
-
38
-
39
- def find_and_inject(page_file, target, seq):
40
- """三路定位注入:资源文案 → 精确/模糊文本 → 行号。返回 (id or None, modified_src)。"""
41
- src = read(page_file)
42
- lines = src.split('\n')
43
- tid = f"tc_{os.path.splitext(os.path.basename(page_file))[0].lower()}_{seq}"
44
-
45
- if target and re.search(rf"\.id\(\s*'{re.escape(target)}'", src):
46
- return target, src
47
-
48
- if target and 'app.string.' in src:
49
- modroot = page_file
50
- for _ in range(4):
51
- modroot = os.path.dirname(modroot)
52
- cand = os.path.join(modroot, 'resources', 'base', 'element', 'string.json')
53
- if os.path.exists(cand):
54
- resname = None
55
- try:
56
- for item in json.load(open(cand, encoding='utf-8')).get('string', []):
57
- if item.get('value') == target:
58
- resname = item.get('name')
59
- break
60
- except Exception:
61
- pass
62
- if resname:
63
- break
64
- if resname:
65
- rx = re.compile(r"[\$r]\(\s*'app\.string\." + re.escape(resname) + r"'")
66
- for i, ln in enumerate(lines):
67
- if rx.search(ln):
68
- for k in range(i, min(len(lines), i + 10)):
69
- if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
70
- indent = re.match(r'(\s*)', lines[k]).group(1)
71
- lines.insert(k, f"{indent}.id('{tid}')")
72
- return tid, '\n'.join(lines)
73
- indent = re.match(r'(\s*)', ln).group(1)
74
- lines.insert(i + 1, f"{indent} .id('{tid}')")
75
- return tid, '\n'.join(lines)
76
-
77
- if target:
78
- for i, ln in enumerate(lines):
79
- m = TEXT_PAT.match(ln)
80
- if not m:
81
- continue
82
- if m.group(3) == target or (target in m.group(3) or m.group(3) in target):
83
- seg = '\n'.join(lines[i:i + 25])
84
- if re.search(r'\.id\(', seg):
85
- idm = re.search(r"\.id\(\s*'([^']+)'", seg)
86
- if idm:
87
- return idm.group(1), src
88
- for k in range(i + 1, min(len(lines), i + 15)):
89
- if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
90
- indent = re.match(r'(\s*)', lines[k]).group(1)
91
- lines.insert(k, f"{indent}.id('{tid}')")
92
- return tid, '\n'.join(lines)
93
- j = insert_after_component(lines, i)
94
- lines.insert(j, f"{m.group(1)} .id('{tid}')")
95
- return tid, '\n'.join(lines)
96
-
97
- tc = re.match(r'line(\d+):(\w+)', target or '')
98
- if tc:
99
- i = int(tc.group(1)) - 1
100
- if 0 <= i < len(lines):
101
- for k in range(i + 1, min(len(lines), i + 9)):
102
- if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
103
- indent = re.match(r'(\s*)', lines[k]).group(1)
104
- lines.insert(k, f"{indent}.id('{tid}')")
105
- return tid, '\n'.join(lines)
106
- for k in range(i, max(0, i - 8), -1):
107
- if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
108
- indent = re.match(r'(\s*)', lines[k]).group(1)
109
- lines.insert(k + 1, f"{indent}.id('{tid}')")
110
- return tid, '\n'.join(lines)
111
- return None, src
112
-
113
-
114
- def main():
115
- ap = argparse.ArgumentParser()
116
- ap.add_argument('--features', required=True)
117
- ap.add_argument('--out', default=None)
118
- args = ap.parse_args()
119
- feats = json.load(open(args.features, encoding='utf-8'))
120
- project = feats['project_dir']
121
- out = args.out or os.path.join(os.path.dirname(args.features), 'ids.json')
122
- mapping = {}
123
- if os.path.exists(out):
124
- try:
125
- mapping = json.load(open(out, encoding='utf-8'))
126
- except Exception:
127
- mapping = {}
128
- injected = existing = missed = 0
129
- for pg in feats.get('pages', []):
130
- fpath = pg.get('file')
131
- if not fpath or not os.path.exists(fpath):
132
- continue
133
- seq = 0
134
- for it in pg.get('interactions', []):
135
- target = it.get('target')
136
- desc = it.get('desc')
137
- if not target:
138
- continue
139
- seq += 1
140
- key = f"{pg['page']}:{desc or target}"
141
- prev = mapping.get(key)
142
- if prev and re.search(rf"\.id\(\s*'{re.escape(prev)}'", read(fpath)):
143
- existing += 1
144
- continue
145
- if it.get('has_id'):
146
- existing += 1
147
- mapping[key] = target
148
- continue
149
- tid, new_src = find_and_inject(fpath, target, seq)
150
- if not tid:
151
- tid2, new_src2 = find_and_inject(fpath, f"line{it.get('line')}:{it.get('type')}", seq)
152
- tid, new_src = tid2 or tid, new_src2 if tid2 else new_src
153
- if tid:
154
- disk = read(fpath)
155
- if new_src != disk:
156
- open(fpath, 'w', encoding='utf-8').write(new_src) # 逐次落盘(页尾统一写会丢同页其余注入)
157
- injected += 1
158
- else:
159
- existing += 1
160
- mapping[key] = tid
161
- else:
162
- missed += 1
163
- for res_id in re.findall(r"\.id\('([a-z][a-z0-9_]+)'\)", '\n'.join(
164
- read(p) for p in glob.glob(os.path.join(project, '**', '*.ets'), recursive=True)
165
- if 'oh_modules' not in p))[:0]:
166
- pass
167
- # 连续 .id() 消除(TD-006):注入点组件已有语义 id 时,删除本轮新插的机械 id,
168
- # mapping 改指向已有 id——运行时链式 .id() 后者覆盖前者,机械 id 失效且污染源码
169
- import glob as _g
170
- fixed = 0
171
- for pg in feats.get('pages', []):
172
- fp = pg.get('file')
173
- if not fp or not os.path.exists(fp):
174
- continue
175
- try:
176
- src = open(fp, encoding='utf-8').read()
177
- except Exception:
178
- continue
179
- new_src = src
180
- for m in re.finditer(r"\.id\('(tc_[^']+)'\)\s*\.id\('([^']+)'\)", src):
181
- dead, alive = m.group(1), m.group(2)
182
- new_src = new_src.replace(f".id('{dead}')\n", '', 1).replace(f".id('{dead}')", '', 1)
183
- for k, v in list(mapping.items()):
184
- if v == dead:
185
- mapping[k] = alive
186
- fixed += 1
187
- if new_src != src:
188
- open(fp, 'w', encoding='utf-8').write(new_src)
189
- # ID 稳定性门禁(TD-013):旧 ids.json 被用例引用的 id 若在新轮消失 → BLOCKER 阻断,
190
- # 禁止静默让用例回退 text 定位(run 期间 id 漂移的机制化防线)
191
- old_out = out + '.prev'
192
- if os.path.exists(out):
193
- import shutil as _sh
194
- _sh.copy(out, old_out)
195
- try:
196
- old_map = json.load(open(old_out, encoding='utf-8'))
197
- except Exception:
198
- old_map = {}
199
- gone = {k: v for k, v in old_map.items() if v not in set(mapping.values())
200
- and v not in {m for m in mapping.values()}}
201
- if gone:
202
- print(f'✗ BLOCKER:上一轮 {len(gone)} 个 id 本轮消失(用例可能引用,禁静默回退 text):')
203
- for k, v in list(gone.items())[:6]:
204
- print(f' {k} → {v}')
205
- print(' 处置:核对注入冲突/页面改动,或显式重命名并同步全部用例后重跑。')
206
- json.dump(mapping, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
207
- print(f'ID 预置完成: 新注入 {injected} / 已有 {existing} / 未定位 {missed}'
208
- f' / 连续id消除 {fixed} → {out}')
209
-
210
- # 语法快速自检:检测连续 .id() 调用(最常见注入冲突)
211
- import re as _re
212
- _warn = 0
213
- for pg in feats.get('pages', []):
214
- fp = pg.get('file')
215
- if not fp or not os.path.exists(fp):
216
- continue
217
- try:
218
- src = open(fp, encoding='utf-8').read()
219
- for m in _re.finditer(r"\.id\(\s*'[^']+'\s*\)\s*\.id\(", src):
220
- print(f' ⚠️ 语法风险: {os.path.basename(fp)} 存在连续 .id() 调用')
221
- _warn += 1
222
- except Exception:
223
- pass
224
- if _warn:
225
- print(f'⚠️ 发现 {_warn} 处语法风险——建议先跑构建验证再使用 ids.json')
226
- else:
227
- print('✓ 语法快速检查通过')
228
- print('⚠️ 注入后必须 rebuild + uninstall&&install 再执行脚本')
229
-
230
-
231
- if __name__ == '__main__':
232
- main()
1
+ #!/usr/bin/env python3
2
+ """preinject_ids.py — ID 预置(铁律①的机械强制,先于脚本编写,不依赖 cases DSL)。
3
+
4
+ 输入 features.json(scan_features 产物),对全部 has_id=False 的交互点批量注入
5
+ `.id('tc_<page>_<n>')`(资源文案/精确模糊文本/行号三路定位,属性链内插桩永远合法),
6
+ 输出 ids.json 映射(脚本编写只认此表与既有 ID)。注入后须 rebuild + uninstall&&install。
7
+
8
+ 用法:
9
+ python3 preinject_ids.py --features features.json [--out ids.json]
10
+ """
11
+ import argparse
12
+ import glob
13
+ import json
14
+ import os
15
+ import re
16
+
17
+ TEXT_PAT = re.compile(
18
+ r"^(\s*)(Text|Button|Toggle|Checkbox|Radio|MenuItem|Image|Row|Column)\("
19
+ r"[^\n]*?['\"](.*?)['\"][^\n]*\)?\s*$")
20
+
21
+
22
+ def read(p):
23
+ return open(p, encoding='utf-8').read()
24
+
25
+
26
+ def insert_after_component(lines, i):
27
+ depth = 0
28
+ started = False
29
+ for k in range(i, min(len(lines), i + 40)):
30
+ depth += lines[k].count('(') - lines[k].count(')')
31
+ started = True
32
+ if started and depth <= 0 and k > i:
33
+ return k + 1
34
+ if started and depth < 0:
35
+ return k
36
+ return min(len(lines), i + 12)
37
+
38
+
39
+ def find_and_inject(page_file, target, seq):
40
+ """三路定位注入:资源文案 → 精确/模糊文本 → 行号。返回 (id or None, modified_src)。"""
41
+ src = read(page_file)
42
+ lines = src.split('\n')
43
+ tid = f"tc_{os.path.splitext(os.path.basename(page_file))[0].lower()}_{seq}"
44
+
45
+ if target and re.search(rf"\.id\(\s*'{re.escape(target)}'", src):
46
+ return target, src
47
+
48
+ if target and 'app.string.' in src:
49
+ modroot = page_file
50
+ for _ in range(4):
51
+ modroot = os.path.dirname(modroot)
52
+ cand = os.path.join(modroot, 'resources', 'base', 'element', 'string.json')
53
+ if os.path.exists(cand):
54
+ resname = None
55
+ try:
56
+ for item in json.load(open(cand, encoding='utf-8')).get('string', []):
57
+ if item.get('value') == target:
58
+ resname = item.get('name')
59
+ break
60
+ except Exception:
61
+ pass
62
+ if resname:
63
+ break
64
+ if resname:
65
+ rx = re.compile(r"[\$r]\(\s*'app\.string\." + re.escape(resname) + r"'")
66
+ for i, ln in enumerate(lines):
67
+ if rx.search(ln):
68
+ for k in range(i, min(len(lines), i + 10)):
69
+ if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
70
+ indent = re.match(r'(\s*)', lines[k]).group(1)
71
+ lines.insert(k, f"{indent}.id('{tid}')")
72
+ return tid, '\n'.join(lines)
73
+ indent = re.match(r'(\s*)', ln).group(1)
74
+ lines.insert(i + 1, f"{indent} .id('{tid}')")
75
+ return tid, '\n'.join(lines)
76
+
77
+ if target:
78
+ for i, ln in enumerate(lines):
79
+ m = TEXT_PAT.match(ln)
80
+ if not m:
81
+ continue
82
+ if m.group(3) == target or (target in m.group(3) or m.group(3) in target):
83
+ seg = '\n'.join(lines[i:i + 25])
84
+ if re.search(r'\.id\(', seg):
85
+ idm = re.search(r"\.id\(\s*'([^']+)'", seg)
86
+ if idm:
87
+ return idm.group(1), src
88
+ for k in range(i + 1, min(len(lines), i + 15)):
89
+ if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
90
+ indent = re.match(r'(\s*)', lines[k]).group(1)
91
+ lines.insert(k, f"{indent}.id('{tid}')")
92
+ return tid, '\n'.join(lines)
93
+ j = insert_after_component(lines, i)
94
+ lines.insert(j, f"{m.group(1)} .id('{tid}')")
95
+ return tid, '\n'.join(lines)
96
+
97
+ tc = re.match(r'line(\d+):(\w+)', target or '')
98
+ if tc:
99
+ i = int(tc.group(1)) - 1
100
+ if 0 <= i < len(lines):
101
+ for k in range(i + 1, min(len(lines), i + 9)):
102
+ if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
103
+ indent = re.match(r'(\s*)', lines[k]).group(1)
104
+ lines.insert(k, f"{indent}.id('{tid}')")
105
+ return tid, '\n'.join(lines)
106
+ for k in range(i, max(0, i - 8), -1):
107
+ if re.match(r'^\s*\.[a-zA-Z]', lines[k]):
108
+ indent = re.match(r'(\s*)', lines[k]).group(1)
109
+ lines.insert(k + 1, f"{indent}.id('{tid}')")
110
+ return tid, '\n'.join(lines)
111
+ return None, src
112
+
113
+
114
+ def main():
115
+ ap = argparse.ArgumentParser()
116
+ ap.add_argument('--features', required=True)
117
+ ap.add_argument('--out', default=None)
118
+ args = ap.parse_args()
119
+ feats = json.load(open(args.features, encoding='utf-8'))
120
+ project = feats['project_dir']
121
+ out = args.out or os.path.join(os.path.dirname(args.features), 'ids.json')
122
+ mapping = {}
123
+ if os.path.exists(out):
124
+ try:
125
+ mapping = json.load(open(out, encoding='utf-8'))
126
+ except Exception:
127
+ mapping = {}
128
+ injected = existing = missed = 0
129
+ for pg in feats.get('pages', []):
130
+ fpath = pg.get('file')
131
+ if not fpath or not os.path.exists(fpath):
132
+ continue
133
+ seq = 0
134
+ for it in pg.get('interactions', []):
135
+ target = it.get('target')
136
+ desc = it.get('desc')
137
+ if not target:
138
+ continue
139
+ seq += 1
140
+ key = f"{pg['page']}:{desc or target}"
141
+ prev = mapping.get(key)
142
+ if prev and re.search(rf"\.id\(\s*'{re.escape(prev)}'", read(fpath)):
143
+ existing += 1
144
+ continue
145
+ if it.get('has_id'):
146
+ existing += 1
147
+ mapping[key] = target
148
+ continue
149
+ tid, new_src = find_and_inject(fpath, target, seq)
150
+ if not tid:
151
+ tid2, new_src2 = find_and_inject(fpath, f"line{it.get('line')}:{it.get('type')}", seq)
152
+ tid, new_src = tid2 or tid, new_src2 if tid2 else new_src
153
+ if tid:
154
+ disk = read(fpath)
155
+ if new_src != disk:
156
+ open(fpath, 'w', encoding='utf-8').write(new_src) # 逐次落盘(页尾统一写会丢同页其余注入)
157
+ injected += 1
158
+ else:
159
+ existing += 1
160
+ mapping[key] = tid
161
+ else:
162
+ missed += 1
163
+ for res_id in re.findall(r"\.id\('([a-z][a-z0-9_]+)'\)", '\n'.join(
164
+ read(p) for p in glob.glob(os.path.join(project, '**', '*.ets'), recursive=True)
165
+ if 'oh_modules' not in p))[:0]:
166
+ pass
167
+ # 连续 .id() 消除(TD-006):注入点组件已有语义 id 时,删除本轮新插的机械 id,
168
+ # mapping 改指向已有 id——运行时链式 .id() 后者覆盖前者,机械 id 失效且污染源码
169
+ import glob as _g
170
+ fixed = 0
171
+ for pg in feats.get('pages', []):
172
+ fp = pg.get('file')
173
+ if not fp or not os.path.exists(fp):
174
+ continue
175
+ try:
176
+ src = open(fp, encoding='utf-8').read()
177
+ except Exception:
178
+ continue
179
+ new_src = src
180
+ for m in re.finditer(r"\.id\('(tc_[^']+)'\)\s*\.id\('([^']+)'\)", src):
181
+ dead, alive = m.group(1), m.group(2)
182
+ new_src = new_src.replace(f".id('{dead}')\n", '', 1).replace(f".id('{dead}')", '', 1)
183
+ for k, v in list(mapping.items()):
184
+ if v == dead:
185
+ mapping[k] = alive
186
+ fixed += 1
187
+ if new_src != src:
188
+ open(fp, 'w', encoding='utf-8').write(new_src)
189
+ # ID 稳定性门禁(TD-013):旧 ids.json 被用例引用的 id 若在新轮消失 → BLOCKER 阻断,
190
+ # 禁止静默让用例回退 text 定位(run 期间 id 漂移的机制化防线)
191
+ old_out = out + '.prev'
192
+ if os.path.exists(out):
193
+ import shutil as _sh
194
+ _sh.copy(out, old_out)
195
+ try:
196
+ old_map = json.load(open(old_out, encoding='utf-8'))
197
+ except Exception:
198
+ old_map = {}
199
+ gone = {k: v for k, v in old_map.items() if v not in set(mapping.values())
200
+ and v not in {m for m in mapping.values()}}
201
+ if gone:
202
+ print(f'✗ BLOCKER:上一轮 {len(gone)} 个 id 本轮消失(用例可能引用,禁静默回退 text):')
203
+ for k, v in list(gone.items())[:6]:
204
+ print(f' {k} → {v}')
205
+ print(' 处置:核对注入冲突/页面改动,或显式重命名并同步全部用例后重跑。')
206
+ json.dump(mapping, open(out, 'w', encoding='utf-8'), ensure_ascii=False, indent=1)
207
+ print(f'ID 预置完成: 新注入 {injected} / 已有 {existing} / 未定位 {missed}'
208
+ f' / 连续id消除 {fixed} → {out}')
209
+
210
+ # 语法快速自检:检测连续 .id() 调用(最常见注入冲突)
211
+ import re as _re
212
+ _warn = 0
213
+ for pg in feats.get('pages', []):
214
+ fp = pg.get('file')
215
+ if not fp or not os.path.exists(fp):
216
+ continue
217
+ try:
218
+ src = open(fp, encoding='utf-8').read()
219
+ for m in _re.finditer(r"\.id\(\s*'[^']+'\s*\)\s*\.id\(", src):
220
+ print(f' ⚠️ 语法风险: {os.path.basename(fp)} 存在连续 .id() 调用')
221
+ _warn += 1
222
+ except Exception:
223
+ pass
224
+ if _warn:
225
+ print(f'⚠️ 发现 {_warn} 处语法风险——建议先跑构建验证再使用 ids.json')
226
+ else:
227
+ print('✓ 语法快速检查通过')
228
+ print('⚠️ 注入后必须 rebuild + uninstall&&install 再执行脚本')
229
+
230
+
231
+ if __name__ == '__main__':
232
+ main()