@harmonyos-arkts/d2h 0.1.2 → 0.1.5
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/README.md +78 -44
- package/agents/android-to-hmos-00-orchestrator.md +34 -28
- package/agents/d2h.md +266 -95
- package/dist/bin/install-opencode.js +137 -43
- package/dist/src/install-opencode.js +44 -0
- package/dist/src/install-vscode.js +226 -0
- package/dist/src/toolchain.js +83 -0
- package/docs/environment-check.md +86 -0
- package/opencode/agents.json +1 -1
- package/opencode/vscode-d2h-package.json +7 -0
- package/opencode/vscode-d2h-plugin.mjs +29 -0
- package/package.json +6 -4
- package/schemas/task-envelope.schema.json +58 -4
- package/schemas/task-plan.schema.json +281 -63
- package/scripts/check-environment.mjs +321 -0
- package/skills/android2hmos_resources_convert/SKILL.md +37 -19
- package/skills/appgraph-rule-audit/SKILL.md +24 -5
- package/skills/d2h-hypium-forge/SKILL.md +252 -0
- package/skills/d2h-hypium-forge/references/functional-spec-template.md +140 -0
- package/skills/d2h-hypium-forge/references/official-api.md +135 -0
- package/skills/d2h-hypium-forge/references/pitfalls.md +98 -0
- package/skills/d2h-hypium-forge/references/testability-template.md +71 -0
- package/skills/d2h-hypium-forge/scripts/_common_template.py +614 -0
- package/skills/d2h-hypium-forge/scripts/analyze_results.py +156 -0
- package/skills/d2h-hypium-forge/scripts/assert_audit.py +107 -0
- package/skills/d2h-hypium-forge/scripts/collect_runtime_pages.py +169 -0
- package/skills/d2h-hypium-forge/scripts/forge_report.py +325 -0
- package/skills/d2h-hypium-forge/scripts/inject_resource_ids.py +114 -0
- package/skills/d2h-hypium-forge/scripts/lint_asserts.py +80 -0
- package/skills/d2h-hypium-forge/scripts/lint_locate.py +60 -0
- package/skills/d2h-hypium-forge/scripts/merge_appgraph.py +249 -0
- package/skills/d2h-hypium-forge/scripts/precondition.py +140 -0
- package/skills/d2h-hypium-forge/scripts/preinject_ids.py +232 -0
- package/skills/d2h-hypium-forge/scripts/run_direct.py +392 -0
- package/skills/d2h-hypium-forge/scripts/run_parallel.py +121 -0
- package/skills/d2h-hypium-forge/scripts/scan_features.py +255 -0
- package/skills/d2h-hypium-forge/scripts/upgrade_asserts.py +66 -0
- package/skills/d2h-hypium-forge/scripts/validate_scenarios.py +84 -0
- package/skills/d2h-utg-ai-generate/SKILL.md +128 -0
- package/skills/d2h-utg-ai-generate/references/click-paths-schema.json +76 -0
- package/skills/d2h-utg-ai-generate/references/extraction-guide.md +34 -0
- package/skills/d2h-utg-ai-generate/references/facts-contract.md +96 -0
- package/skills/d2h-utg-ai-generate/references/page-schema.json +61 -0
- package/skills/d2h-utg-ai-generate/references/utg-schema.json +89 -0
- package/skills/d2h-utg-ai-generate/scripts/build_page_json.py +314 -0
- package/skills/d2h-utg-ai-generate/scripts/build_utg.py +123 -0
- package/skills/d2h-utg-ai-generate/scripts/capture_from_page_json.ts +280 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_common.py +524 -0
- package/skills/d2h-utg-ai-generate/scripts/utg_html.py +489 -0
- package/skills/d2h-utg-ai-generate/scripts/validate_utg.py +69 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/D3-LICENSE +13 -0
- package/skills/d2h-utg-ai-generate/scripts/vendor/d3-7.9.0.min.js +2 -0
- package/skills/d2h-utg-ai-generate/tests/test_pipeline.py +369 -0
- package/skills/d2h-utg-static-generate/SKILL.md +73 -0
- package/skills/d2h-utg-static-generate/references/discoveries.md +38 -0
- package/skills/d2h-utg-static-generate/scripts/generate.py +499 -0
- package/skills/d2h-utg-static-generate/tests/test_static.py +335 -0
- package/skills/d2h-utg-static-generate/tests/verify-offline-html.py +60 -0
- package/skills/d2h-utg-static-generate/tests/verify-produced-fixture.py +131 -0
- package/skills/harmonyos-migration-playbook/SKILL.md +30 -3
- package/skills/harmonyos-migration-playbook/references/lesson-lifecycle.md +4 -4
- package/skills/harmonyos-migration-playbook/references/ui-automation.md +4 -3
- package/skills/harmonyos-migration-playbook/references/windows-environment.md +2 -2
- package/skills/maintaining-migration-report/SKILL.md +88 -99
- package/skills/maintaining-migration-report/references/final-acceptance.md +48 -0
- package/skills/maintaining-migration-report/references/source-provenance.md +66 -0
- package/skills/managing-d2h-toolchain/SKILL.md +23 -0
- package/skills/managing-d2h-toolchain/environment-requirements.json +16 -0
- package/skills/managing-d2h-toolchain/toolchain.json +16 -0
- package/skills/preparing-migration-workspace/SKILL.md +76 -69
- package/skills/preparing-migration-workspace/toolchain.json +0 -43
|
@@ -0,0 +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()
|
|
@@ -0,0 +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()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""validate_scenarios.py — 场景库质量门禁(下游唯一基础的准入校验)。
|
|
3
|
+
|
|
4
|
+
校验规则(不通过进 scenarios_blocker.md,禁止进入用例生成):
|
|
5
|
+
1. 必备字段:id / name / desc / converted / test_intent
|
|
6
|
+
2. 可观察业务效果:desc 须含效果关键词(新增/删除/…),纯导航词(进入/打开/跳转XX页)
|
|
7
|
+
且无效果词 = 拦截
|
|
8
|
+
3. 入口可定位:page_path 或前置路径字段存在(深层链路须写明)
|
|
9
|
+
4. converted=false 必须带 reason(未迁移/能力缺失/暂不测试)
|
|
10
|
+
5. 行为预期依据:source_ref / spec_ref 至少其一(默认值/联动从源码或规格推导)
|
|
11
|
+
|
|
12
|
+
用法: python3 validate_scenarios.py scenarios.json [--out scenarios_blocker.md]
|
|
13
|
+
退出码:0=全过(或仅警告);1=存在 blocker。
|
|
14
|
+
"""
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
REQUIRED = ['id', 'name', 'desc', 'converted', 'test_intent']
|
|
21
|
+
EFFECT_WORDS = ['新增', '删除', '修改', '保存', '收藏', '取消', '成功', '失败', '提示',
|
|
22
|
+
'计数', '状态', '切换', '变为', '出现', '消失', '显示', '回显', '+1', '变更为']
|
|
23
|
+
NAV_ONLY = re.compile(r'^(进入|打开|跳转|前往|查看)[^,。]{1,12}(页|面|界面)$')
|
|
24
|
+
INTENTS = ('资源渲染', '功能交互', '配置生效', '导航流转', '状态机', '数据输入')
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def check(sc):
|
|
28
|
+
bid = sc.get('id', '?')
|
|
29
|
+
errs, warns = [], []
|
|
30
|
+
for f in REQUIRED:
|
|
31
|
+
if not str(sc.get(f, '')).strip():
|
|
32
|
+
errs.append(f'缺必备字段 {f}')
|
|
33
|
+
desc = str(sc.get('desc', ''))
|
|
34
|
+
if desc:
|
|
35
|
+
if NAV_ONLY.match(desc.strip()):
|
|
36
|
+
errs.append('desc 纯导航无可观察效果("进入XX页"不是业务效果)')
|
|
37
|
+
elif not any(w in desc for w in EFFECT_WORDS):
|
|
38
|
+
errs.append(f'desc 无效果关键词(须含 {"/".join(EFFECT_WORDS[:6])}… 之一)')
|
|
39
|
+
if not (sc.get('page_path') or sc.get('precond_pages') or '前置' in desc or '路径' in desc):
|
|
40
|
+
warns.append('入口路径未标注(深层链路须在 desc 写前置路径)')
|
|
41
|
+
if sc.get('converted') is False and not str(sc.get('reason', '')).strip():
|
|
42
|
+
errs.append('converted=false 必须带 reason(未迁移/能力缺失/暂不测试)')
|
|
43
|
+
if not (sc.get('source_ref') or sc.get('spec_ref') or sc.get('evidence')):
|
|
44
|
+
warns.append('行为预期无源码/规格依据标注(默认值/联动禁直觉推断)')
|
|
45
|
+
ti = str(sc.get('test_intent', ''))
|
|
46
|
+
if ti and not any(t in ti for t in INTENTS):
|
|
47
|
+
errs.append(f'test_intent 非法 "{ti}"')
|
|
48
|
+
return bid, errs, warns
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main():
|
|
52
|
+
ap = argparse.ArgumentParser()
|
|
53
|
+
ap.add_argument('scenarios')
|
|
54
|
+
ap.add_argument('--out', default='scenarios_blocker.md')
|
|
55
|
+
args = ap.parse_args()
|
|
56
|
+
try:
|
|
57
|
+
scenes = json.load(open(args.scenarios, encoding='utf-8'))
|
|
58
|
+
except Exception as e:
|
|
59
|
+
print(f'✗ scenarios.json 解析失败: {e}')
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
if not scenes:
|
|
62
|
+
print('✗ 场景库为空——空库不允许生成用例(空跑=伪覆盖)')
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
lines = ['# 场景库 Blocker(未通过质量门禁,禁止进入用例生成)', '']
|
|
66
|
+
n_err = n_warn = 0
|
|
67
|
+
for sc in scenes:
|
|
68
|
+
bid, errs, warns = check(sc)
|
|
69
|
+
for e in errs:
|
|
70
|
+
lines.append(f'- **BLOCKER** {bid}: {e}')
|
|
71
|
+
for w in warns:
|
|
72
|
+
lines.append(f'- ⚠ {bid}: {w}')
|
|
73
|
+
n_err += len(errs)
|
|
74
|
+
n_warn += len(warns)
|
|
75
|
+
print(f'场景库校验: {len(scenes)} 场景 | {n_err} blocker / {n_warn} 警告')
|
|
76
|
+
if n_err:
|
|
77
|
+
open(args.out, 'w', encoding='utf-8').write('\n'.join(lines) + '\n')
|
|
78
|
+
print(f'→ {args.out}(blocker 场景退回宿主 agent 修复)')
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
print('✓ 全部通过(警告项自行斟酌)')
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
main()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: d2h-utg-ai-generate
|
|
3
|
+
description: Independently generate or repair a source-derived UI Transition Graph with AI-assisted code understanding. Produces schema-checked utg.json, click_paths.json, and page.json for Android View/Fragment, Jetpack Compose, Voyager, and uni-app/Vue projects; for Android runtime capture, replays the generated plan directly through ADB and captures screenshots plus UIAutomator view trees. Use when source analysis must produce a UTG and optionally collect device evidence without another skill or AI navigation service.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AI-assisted source UTG generation
|
|
7
|
+
|
|
8
|
+
Use source evidence to identify pages and transitions, then hand only structured facts to the bundled deterministic scripts. The model must not directly author the final `utg.json`, `click_paths.json`, or `page.json`.
|
|
9
|
+
|
|
10
|
+
This skill owns two explicit phases:
|
|
11
|
+
|
|
12
|
+
1. **Source phase** — runs for independent generation or repair; produces the validated graph, paths, and capture plan without requiring a device. Skip it in explicit existing-plan capture mode.
|
|
13
|
+
2. **Runtime phase** — runs when screenshot/DOM capture is requested and an unlocked Android ADB device is available. The bundled TypeScript script consumes `page.json`, replays deterministic navigation steps through ADB, and captures device evidence itself.
|
|
14
|
+
|
|
15
|
+
The source-analysis mode runs independently. The explicit existing-plan capture mode may consume a validated page.json produced by d2h-utg-static-generate; it must not repeat source analysis or overwrite that graph. Do not use AutoGLM or blind crawling. The runtime phase is targeted deterministic replay, not blind crawling: only nodes marked `capture=true` in `page.json` are requested.
|
|
16
|
+
|
|
17
|
+
## Inputs
|
|
18
|
+
|
|
19
|
+
Resolve absolute paths for:
|
|
20
|
+
|
|
21
|
+
- `source_project_dir` — application source root.
|
|
22
|
+
- `output_workspace` — a new or explicitly selected UTG workspace. In the droid2hmos migration flow, set this to `<工程名>-migration/.migration/utg/` so the UTG artifacts live in the migration workspace alongside `spec/`, `report/`, `tasks/`, `arch/`, instead of inside either source project.
|
|
23
|
+
- Optional existing `utg.json` and `click_paths.json` when the user requests repair.
|
|
24
|
+
- Optional page/module scope. Never broaden a scoped request silently.
|
|
25
|
+
- For runtime capture: Node.js 22.18+ (or 23.6+), `adb`, an installed APK on an unlocked device, optional `device_serial`, and `capture_workspace` (default `<output_workspace>/../ui_info` — a `ui_info/` sibling of the UTG workspace; in the migration flow use `<工程名>-migration/.migration/ui_info/`, sibling of `.migration/utg/`).
|
|
26
|
+
|
|
27
|
+
Write to a new workspace unless the user explicitly requested repair in place. Do not overwrite unrelated UTG or capture artifacts.
|
|
28
|
+
|
|
29
|
+
## Existing-plan capture mode
|
|
30
|
+
|
|
31
|
+
When the caller provides an existing `page.json`, skip source-generation steps 1–5. Validate the referenced graph/path/plan with `scripts/validate_utg.py --utg <utg.json> --click-paths <click_paths.json> --page-json <page.json> --project-root <source>`. Run `node scripts/capture_from_page_json.ts --page-json <page.json> --output-workspace <capture-workspace> --dry-run` first; this validates without touching a device. If capture was requested, run the same command without `--dry-run` and apply all existing device boundaries below. Do not reinterpret `capture=false` nodes as capture tasks or regenerate the plan to force them eligible. The capture plan remains read-only; runtime outputs go to its selected capture workspace.
|
|
32
|
+
|
|
33
|
+
## Workflow
|
|
34
|
+
|
|
35
|
+
1. Classify the source stack from its manifests, build files, route registries, and navigation dependencies.
|
|
36
|
+
2. Read [references/extraction-guide.md](references/extraction-guide.md) for the detected stack. Locate entry points, page declarations, navigation calls, back transitions, overlays, modes, deep links, and state guards.
|
|
37
|
+
3. Read [references/facts-contract.md](references/facts-contract.md), then write `<output_workspace>/utg-facts.json`. Every emitted node and edge must cite an existing project-relative `file:line`. Every capture-eligible node needs at least one source-grounded `expected` arrival rule. Record `graph.version_name` and `graph.build_variant` when the source makes them deterministic. Put uncertain or unresolved findings in the top-level `unresolved` array instead of turning them into graph facts.
|
|
38
|
+
4. Build the final artifacts with the deterministic generator. This writes `utg.json`, `click_paths.json`, and `page.json` atomically from the same normalized facts:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python scripts/build_utg.py \
|
|
42
|
+
--facts <output_workspace>/utg-facts.json \
|
|
43
|
+
--project-root <source_project_dir> \
|
|
44
|
+
--workspace <output_workspace>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use `--force` only when the user requested regeneration/repair or the files belong to the current incomplete run. The build also renders the self-contained `utg.html` interactive visualization; a visualization failure is non-blocking.
|
|
48
|
+
5. Validate the graph, paths, and page plan independently:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
python scripts/validate_utg.py \
|
|
52
|
+
--utg <output_workspace>/utg.json \
|
|
53
|
+
--click-paths <output_workspace>/click_paths.json \
|
|
54
|
+
--page-json <output_workspace>/page.json \
|
|
55
|
+
--project-root <source_project_dir>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
6. Fix facts, not generated JSON. Rebuild and revalidate until valid or until remaining ambiguity requires user/runtime evidence.
|
|
59
|
+
7. When runtime capture is requested, verify `node --version`, `adb devices`, that the app is installed, and that the device is unlocked. Compare the installed package version with `page.json.source.version_name`; the script stops on a mismatch. Then run:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
node scripts/capture_from_page_json.ts \
|
|
63
|
+
--page-json <output_workspace>/page.json \
|
|
64
|
+
--output-workspace <capture_workspace> \
|
|
65
|
+
[--device <device_serial>] \
|
|
66
|
+
[--adb-path <adb>] \
|
|
67
|
+
[--satisfied-state content,login] \
|
|
68
|
+
[--max-scrolls 4]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The zero-dependency TypeScript script validates the plan and its UTG/path pair, force-stops and launches the package for each selected page, then replays supported steps using only `adb shell`: `launch/default` (no-op after launch), `tap/click/open/navigate/select`, `long-press`, `back`, `home`, `swipe`, and URI deep links. It parses the UIAutomator hierarchy, promotes a semantic match to its clickable ancestor, deduplicates matches that lead to the same target, and scrolls to search for off-screen targets. Prefer an explicit `args.selector`; `nl_hint` is documentation and is never executed as a selector. A normalized coordinate requires both source evidence and the explicit `--allow-coordinate-fallback` flag.
|
|
72
|
+
8. Inspect `<capture_workspace>/capture-report.json`. A page counts as captured only when deterministic replay completed, its `expected` arrival rules passed, and its `screenshot.png`, `view.xml`, and `meta.json` were atomically promoted. Pages whose state requirements were not asserted are `blocked`, not captured. Record failed and blocked pages as manual follow-up work.
|
|
73
|
+
9. The runtime phase writes `<capture_workspace>/pages.json` directly in `page.json` order and includes only successful captures. `page.json` remains the pre-capture plan; `pages.json` is the post-capture conversion index.
|
|
74
|
+
|
|
75
|
+
Use `--dry-run` on `capture_from_page_json.ts` to validate and print selected nodes plus their required states without touching a device. Use `--only nodeA,nodeB` only for an explicitly scoped capture or retry; it must be a subset of `capture=true` nodes. Use `--satisfied-state` only for conditions verified in the current device/app state. Use `--allow-partial` only when the caller wants a zero exit code despite failed or blocked pages; their status remains visible in `capture-report.json`.
|
|
76
|
+
|
|
77
|
+
Runtime capture does not authorize modifying application source, rebuilding or reinstalling the APK, clearing app data, changing locale, changing display size/density, granting permissions, or changing device roles/settings. Stop and ask before any such expansion. If the user authorizes a temporary device mutation, record the original value first and restore it in a `finally` path; never leave a device override behind silently.
|
|
78
|
+
|
|
79
|
+
## Evidence rules
|
|
80
|
+
|
|
81
|
+
- A source fact needs a real project-relative `file:line`; the scripts reject missing files and out-of-range lines.
|
|
82
|
+
- Prefer navigation call sites for edge `trigger`, not only route declarations.
|
|
83
|
+
- Use a numeric confidence in `[0, 1]`. The builder accepts `high`, `medium`, and `low` only as a compatibility input and records their normalization.
|
|
84
|
+
- Use `guard` for reachability preconditions. Do not claim a guarded page is fresh-install reachable without evidence.
|
|
85
|
+
- Use `args.label_source="nl_hint"` only when literal UI text/content description is unavailable, and include `args.nl_hint` plus evidence.
|
|
86
|
+
- Never invent reverse/back edges merely to make the graph connected. Emit them only when source behavior supports them.
|
|
87
|
+
- Keep external destinations out of the graph unless the source project owns the destination UI. Record them in node/widget metadata or `unresolved`.
|
|
88
|
+
|
|
89
|
+
## Deterministic invariants
|
|
90
|
+
|
|
91
|
+
The bundled scripts own all final-format mechanics:
|
|
92
|
+
|
|
93
|
+
- canonical node/edge defaults, numeric confidence, and parallel-edge keys;
|
|
94
|
+
- referential integrity and root validation;
|
|
95
|
+
- in-app paths from the declared root that minimize guard requirements before hop count;
|
|
96
|
+
- deep-link fallback only for nodes not reachable through normal in-app edges;
|
|
97
|
+
- `depth`, `clicks`, `labels`, `kind`, `path_text`, and `unreachable`;
|
|
98
|
+
- stable capture ordering, collision-safe capture keys, per-node capture eligibility, and exact copied navigation steps in `page.json`;
|
|
99
|
+
- atomic writes and a machine-readable `generation-report.json`.
|
|
100
|
+
|
|
101
|
+
Do not hand-edit these derived fields.
|
|
102
|
+
|
|
103
|
+
## Required outputs
|
|
104
|
+
|
|
105
|
+
The completed workspace contains:
|
|
106
|
+
|
|
107
|
+
- `utg-facts.json` — AI-authored source facts and unresolved findings.
|
|
108
|
+
- `utg.json` — normalized node-link directed multigraph.
|
|
109
|
+
- `click_paths.json` — deterministic paths derived from that exact graph.
|
|
110
|
+
- `page.json` — deterministic, schema-checked runtime capture plan derived from that exact graph/path pair. It includes every UTG node, marking unreachable/container nodes with `capture=false` and a reason.
|
|
111
|
+
- `generation-report.json` — counts, normalizations, warnings, and unresolved items.
|
|
112
|
+
- `utg.html` — self-contained interactive visualization rendered by `build_utg.py`; before runtime capture, screenshot thumbnails are empty.
|
|
113
|
+
|
|
114
|
+
The schemas are [references/utg-schema.json](references/utg-schema.json), [references/click-paths-schema.json](references/click-paths-schema.json), and [references/page-schema.json](references/page-schema.json).
|
|
115
|
+
|
|
116
|
+
When runtime capture runs, it additionally produces:
|
|
117
|
+
|
|
118
|
+
- `<capture_workspace>/<capture_key>/screenshot.png`;
|
|
119
|
+
- `<capture_workspace>/<capture_key>/view.xml`;
|
|
120
|
+
- `<capture_workspace>/<capture_key>/meta.json`;
|
|
121
|
+
- `<capture_workspace>/pages.json` — verified captured pages in downstream conversion order;
|
|
122
|
+
- `<capture_workspace>/capture-report.json` — per-planned-node captured/failed/skipped status and exact artifact paths.
|
|
123
|
+
|
|
124
|
+
## Report
|
|
125
|
+
|
|
126
|
+
Report the node count by kind, edge count by mechanism, reachable/unreachable counts, `page.json` capture-planned/skipped counts, unresolved findings, validation result, provenance method (`ai-source-analysis`), and absolute output paths. Clearly state that source-derived reachability is not runtime confirmation.
|
|
127
|
+
|
|
128
|
+
If runtime capture ran, also report selected/captured/failed counts, screenshot and DOM root paths, `capture-report.json`, `pages.json`, and every failed replay that needs manual setup or a better source selector. A valid source phase with a partial runtime phase is not a total failure: preserve the valid graph and report capture limitations precisely.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://hometrans.local/schemas/click-paths-v2.json",
|
|
4
|
+
"title": "HomeTrans deterministic click paths",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["root", "paths", "unreachable"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"root": {"type": "string", "minLength": 1},
|
|
9
|
+
"paths": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": {
|
|
12
|
+
"type": "object",
|
|
13
|
+
"required": ["depth", "steps", "labels", "clicks", "kind", "path_text"],
|
|
14
|
+
"properties": {
|
|
15
|
+
"depth": {"type": "integer", "minimum": 0},
|
|
16
|
+
"steps": {
|
|
17
|
+
"type": "array",
|
|
18
|
+
"minItems": 1,
|
|
19
|
+
"items": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["action", "label", "target_id", "target_kind", "guard"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"action": {"type": "string", "minLength": 1},
|
|
24
|
+
"label": {"type": ["string", "null"]},
|
|
25
|
+
"label_source": {"type": ["string", "null"]},
|
|
26
|
+
"label_ambiguous": {"type": "boolean"},
|
|
27
|
+
"target_id": {"type": "string", "minLength": 1},
|
|
28
|
+
"target_kind": {"type": "string", "minLength": 1},
|
|
29
|
+
"guard": {"type": ["object", "null"]},
|
|
30
|
+
"mechanism": {"type": "string"},
|
|
31
|
+
"nl_hint": {"type": "string"},
|
|
32
|
+
"selector": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"text": {"type": "string", "minLength": 1},
|
|
36
|
+
"text_any": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
37
|
+
"content_desc": {"type": "string", "minLength": 1},
|
|
38
|
+
"content_desc_any": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
39
|
+
"resource_id": {"type": "string", "minLength": 1},
|
|
40
|
+
"resource_id_any": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}},
|
|
41
|
+
"normalized_point": {
|
|
42
|
+
"type": "array",
|
|
43
|
+
"prefixItems": [
|
|
44
|
+
{"type": "number", "minimum": 0, "maximum": 1},
|
|
45
|
+
{"type": "number", "minimum": 0, "maximum": 1}
|
|
46
|
+
],
|
|
47
|
+
"minItems": 2,
|
|
48
|
+
"maxItems": 2
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"additionalProperties": false
|
|
52
|
+
},
|
|
53
|
+
"direction": {"enum": ["up", "down", "left", "right"]},
|
|
54
|
+
"distance": {"type": "number", "minimum": 0.1, "maximum": 0.8},
|
|
55
|
+
"duration_ms": {"type": "integer", "minimum": 0},
|
|
56
|
+
"max_scrolls": {"type": "integer", "minimum": 0}
|
|
57
|
+
},
|
|
58
|
+
"additionalProperties": true
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"labels": {"type": "array", "items": {"type": "string"}},
|
|
62
|
+
"clicks": {"type": "integer", "minimum": 0},
|
|
63
|
+
"kind": {"type": "string", "minLength": 1},
|
|
64
|
+
"path_text": {"type": "string", "minLength": 1}
|
|
65
|
+
},
|
|
66
|
+
"additionalProperties": false
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"unreachable": {
|
|
70
|
+
"type": "array",
|
|
71
|
+
"items": {"type": "string", "minLength": 1},
|
|
72
|
+
"uniqueItems": true
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"additionalProperties": false
|
|
76
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Source extraction guide
|
|
2
|
+
|
|
3
|
+
Read only the section matching the detected stack. Record source facts rather than framework tutorials.
|
|
4
|
+
|
|
5
|
+
## Android View / Fragment
|
|
6
|
+
|
|
7
|
+
Start with merged-source equivalents of the manifest, launcher activities, navigation XML, fragment transactions, explicit/implicit intents, menus/preferences, dialog construction, and callbacks that invoke navigation. Treat an implicit intent whose destination is outside the project as an external boundary, not an owned page.
|
|
8
|
+
|
|
9
|
+
## Jetpack Compose / Navigation-Compose
|
|
10
|
+
|
|
11
|
+
Locate `NavHost`, graph start destinations, `composable`/`navigation` declarations, route registries, `navigate`, `popBackStack`, deep links, modal state controlling dialogs/sheets, and callbacks passed across composables. Resolve route templates and arguments without substituting sample runtime values. A navigation graph container may be a `nav-graph` node when it is needed to preserve source topology, but click paths should still land on actual pages through its start edge.
|
|
12
|
+
|
|
13
|
+
## Voyager or other source routers
|
|
14
|
+
|
|
15
|
+
Locate screen objects/classes, navigator creation, `push`/`replace`/`pop`, tab navigators, nested routers, and modal navigators. Use the router's stable code identity as the node ID. Do not collapse distinct state variants unless the rendered UI and reachable actions are equivalent.
|
|
16
|
+
|
|
17
|
+
## uni-app / Vue
|
|
18
|
+
|
|
19
|
+
Start with `pages.json`, subpackages, tab bar entries, `uni.navigateTo`, `redirectTo`, `reLaunch`, `switchTab`, `navigateBack`, router wrappers, and conditional component branches. Normalize source-owned route paths consistently. Treat downloaded WGT bundles and server-provided decorated-page configs as unresolved boundaries unless those inputs are supplied.
|
|
20
|
+
|
|
21
|
+
## Overlays, modes, and state
|
|
22
|
+
|
|
23
|
+
Dialogs, sheets, menus, selection modes, PiP, and meaningful empty/populated variants must be nodes when they expose distinct user actions or coverage obligations, even when they are state-driven rather than router destinations. Omit only purely decorative/transient UI, and state that reason in `unresolved`. Pair open/dismiss transitions only when each direction is supported by source behavior. Record permissions, login, content, server configuration, hardware, build flavor, and time-window constraints as guards.
|
|
24
|
+
|
|
25
|
+
## Completeness pass
|
|
26
|
+
|
|
27
|
+
Before building, compare:
|
|
28
|
+
|
|
29
|
+
- declared routes versus nodes;
|
|
30
|
+
- navigation call targets versus nodes;
|
|
31
|
+
- nodes with no incoming edge other than legitimate roots/entry points;
|
|
32
|
+
- nodes with no outgoing edge where back/dismiss behavior should exist;
|
|
33
|
+
- ambiguous or computed targets versus `unresolved`;
|
|
34
|
+
- every fact's evidence against the current source tree.
|