@haaaiawd/anws 2.0.4 → 2.1.0
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/package.json +1 -1
- package/templates/.agents/skills/nexus-query/SKILL.md +114 -0
- package/templates/.agents/skills/nexus-query/scripts/extract_ast.py +706 -0
- package/templates/.agents/skills/nexus-query/scripts/git_detective.py +194 -0
- package/templates/.agents/skills/nexus-query/scripts/languages.json +127 -0
- package/templates/.agents/skills/nexus-query/scripts/query_graph.py +556 -0
- package/templates/.agents/skills/nexus-query/scripts/requirements.txt +6 -0
- package/templates/.agents/skills/runtime-inspector/SKILL.md +8 -2
- package/templates/.agents/skills/sequential-thinking/SKILL.md +44 -7
- package/templates/.agents/skills/task-planner/SKILL.md +63 -1
- package/templates/.agents/skills/task-planner/references/TASK_TEMPLATE.md +86 -37
- package/templates/.agents/workflows/blueprint.md +40 -42
- package/templates/.agents/workflows/challenge.md +7 -6
- package/templates/.agents/workflows/change.md +52 -39
- package/templates/.agents/workflows/design-system.md +14 -5
- package/templates/.agents/workflows/explore.md +76 -8
- package/templates/.agents/workflows/forge.md +6 -1
- package/templates/.agents/workflows/genesis.md +59 -14
- package/templates/.agents/workflows/probe.md +105 -35
- package/templates/.agents/workflows/quickstart.md +2 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
query_graph.py — AST 按需查询工具
|
|
4
|
+
|
|
5
|
+
读取 extract_ast.py 产出的 ast_nodes.json,提供多种查询模式,
|
|
6
|
+
输出 agent 易消费的精简文本。
|
|
7
|
+
|
|
8
|
+
用途:
|
|
9
|
+
- PROBE 流程中辅助 REASON/OBJECT/EMIT 阶段生成认知文件
|
|
10
|
+
- 开发中做 bug 调查、修改影响评估、重构分析
|
|
11
|
+
|
|
12
|
+
用法:
|
|
13
|
+
python query_graph.py <ast_nodes.json> --file <path>
|
|
14
|
+
python query_graph.py <ast_nodes.json> --who-imports <module_or_path>
|
|
15
|
+
python query_graph.py <ast_nodes.json> --impact <path>
|
|
16
|
+
python query_graph.py <ast_nodes.json> --hub-analysis [--top N]
|
|
17
|
+
python query_graph.py <ast_nodes.json> --summary
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import sys
|
|
21
|
+
import json
|
|
22
|
+
import argparse
|
|
23
|
+
from pathlib import Path, PurePosixPath
|
|
24
|
+
from collections import defaultdict
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitStats:
|
|
28
|
+
"""git_stats.json 的查询辅助。可选加载,不影响核心 AST 查询。"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, data: dict):
|
|
31
|
+
self.period_days: int = data.get('analysis_period_days', 90)
|
|
32
|
+
self.hotspots: dict[str, dict] = {} # path → {changes, risk}
|
|
33
|
+
for h in data.get('hotspots', []):
|
|
34
|
+
self.hotspots[h['path']] = h
|
|
35
|
+
self.coupling: dict[str, list[dict]] = defaultdict(list) # path → [{peer, co_changes, score}]
|
|
36
|
+
for c in data.get('coupling_pairs', []):
|
|
37
|
+
self.coupling[c['file_a']].append({
|
|
38
|
+
'peer': c['file_b'], 'co_changes': c['co_changes'],
|
|
39
|
+
'score': c['coupling_score'],
|
|
40
|
+
})
|
|
41
|
+
self.coupling[c['file_b']].append({
|
|
42
|
+
'peer': c['file_a'], 'co_changes': c['co_changes'],
|
|
43
|
+
'score': c['coupling_score'],
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
def file_risk(self, path: str) -> dict | None:
|
|
47
|
+
return self.hotspots.get(path)
|
|
48
|
+
|
|
49
|
+
def file_coupling(self, path: str) -> list[dict]:
|
|
50
|
+
return sorted(self.coupling.get(path, []), key=lambda x: x['score'], reverse=True)
|
|
51
|
+
|
|
52
|
+
RISK_ICON = {'high': '🔴', 'medium': '🟡', 'low': '🟢'}
|
|
53
|
+
|
|
54
|
+
def format_risk_block(self, path: str) -> list[str]:
|
|
55
|
+
"""为一个文件生成 git 风险 + 耦合的文本行(空列表表示无数据)。"""
|
|
56
|
+
lines: list[str] = []
|
|
57
|
+
risk = self.file_risk(path)
|
|
58
|
+
if risk:
|
|
59
|
+
icon = self.RISK_ICON.get(risk['risk'], '⚪')
|
|
60
|
+
lines.append(f"Git risk: {icon} {risk['risk']} ({risk['changes']} changes in {self.period_days} days)")
|
|
61
|
+
coupling = self.file_coupling(path)
|
|
62
|
+
if coupling:
|
|
63
|
+
lines.append("Coupled files (co-change):")
|
|
64
|
+
for c in coupling[:5]:
|
|
65
|
+
lines.append(f" - {c['peer']} (coupling: {c['score']:.2f}, {c['co_changes']} co-changes)")
|
|
66
|
+
return lines
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ASTGraph:
|
|
70
|
+
"""内存中的 AST 图索引,支持多种查询模式。"""
|
|
71
|
+
|
|
72
|
+
SOURCE_ROOT_MARKERS = (
|
|
73
|
+
('src',),
|
|
74
|
+
('backend', 'src'),
|
|
75
|
+
('frontend', 'src'),
|
|
76
|
+
('client', 'src'),
|
|
77
|
+
('src', 'main', 'python'),
|
|
78
|
+
('src', 'test', 'python'),
|
|
79
|
+
('src', 'main', 'java'),
|
|
80
|
+
('src', 'test', 'java'),
|
|
81
|
+
('src', 'main', 'kotlin'),
|
|
82
|
+
('src', 'test', 'kotlin'),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def __init__(self, data: dict, git_stats: GitStats | None = None):
|
|
86
|
+
self.data = data
|
|
87
|
+
self.nodes: list[dict] = data.get('nodes', [])
|
|
88
|
+
self.edges: list[dict] = data.get('edges', [])
|
|
89
|
+
self.stats: dict = data.get('stats', {})
|
|
90
|
+
self.languages: list[str] = data.get('languages', [])
|
|
91
|
+
self.git: GitStats | None = git_stats
|
|
92
|
+
|
|
93
|
+
# 索引
|
|
94
|
+
self.nodes_by_id: dict[str, dict] = {}
|
|
95
|
+
self.nodes_by_path: dict[str, list[dict]] = defaultdict(list)
|
|
96
|
+
self.modules_by_path: dict[str, dict] = {}
|
|
97
|
+
self.imports_forward: dict[str, set[str]] = defaultdict(set)
|
|
98
|
+
self.imports_reverse: dict[str, set[str]] = defaultdict(set)
|
|
99
|
+
self.internal_imports_forward: dict[str, set[str]] = defaultdict(set)
|
|
100
|
+
self.internal_imports_reverse: dict[str, set[str]] = defaultdict(set)
|
|
101
|
+
self.contains_children: dict[str, list[dict]] = defaultdict(list)
|
|
102
|
+
self.path_to_module_id: dict[str, str] = {}
|
|
103
|
+
self.alias_to_module_ids: dict[str, set[str]] = defaultdict(set)
|
|
104
|
+
|
|
105
|
+
self._build_index()
|
|
106
|
+
|
|
107
|
+
def _build_index(self) -> None:
|
|
108
|
+
for node in self.nodes:
|
|
109
|
+
nid = node['id']
|
|
110
|
+
self.nodes_by_id[nid] = node
|
|
111
|
+
path = node.get('path', '')
|
|
112
|
+
if path:
|
|
113
|
+
self.nodes_by_path[path].append(node)
|
|
114
|
+
if node['type'] == 'Module' and path:
|
|
115
|
+
self.modules_by_path[path] = node
|
|
116
|
+
self.path_to_module_id[path] = nid
|
|
117
|
+
for alias in self._module_aliases(nid, path):
|
|
118
|
+
self.alias_to_module_ids[alias].add(nid)
|
|
119
|
+
|
|
120
|
+
for edge in self.edges:
|
|
121
|
+
src, tgt, etype = edge['source'], edge['target'], edge['type']
|
|
122
|
+
if etype == 'imports':
|
|
123
|
+
self.imports_forward[src].add(tgt)
|
|
124
|
+
self.imports_reverse[tgt].add(src)
|
|
125
|
+
elif etype == 'contains':
|
|
126
|
+
child = self.nodes_by_id.get(tgt)
|
|
127
|
+
if child:
|
|
128
|
+
self.contains_children[src].append(child)
|
|
129
|
+
|
|
130
|
+
module_ids = {n['id'] for n in self.nodes if n['type'] == 'Module'}
|
|
131
|
+
for source, targets in self.imports_forward.items():
|
|
132
|
+
if source not in module_ids:
|
|
133
|
+
continue
|
|
134
|
+
for target in targets:
|
|
135
|
+
resolved = self.resolve_import_target(target)
|
|
136
|
+
if resolved and resolved in module_ids and resolved != source:
|
|
137
|
+
self.internal_imports_forward[source].add(resolved)
|
|
138
|
+
self.internal_imports_reverse[resolved].add(source)
|
|
139
|
+
|
|
140
|
+
def _module_aliases(self, module_id: str, path: str) -> set[str]:
|
|
141
|
+
aliases = {module_id}
|
|
142
|
+
parts = list(PurePosixPath(path.replace('\\', '/')).parts)
|
|
143
|
+
if not parts:
|
|
144
|
+
return aliases
|
|
145
|
+
|
|
146
|
+
stem = PurePosixPath(parts[-1]).stem
|
|
147
|
+
normalized_parts = parts[:-1] if stem == '__init__' else parts[:-1] + [stem]
|
|
148
|
+
|
|
149
|
+
for marker in self.SOURCE_ROOT_MARKERS:
|
|
150
|
+
if tuple(normalized_parts[:len(marker)]) == marker and len(normalized_parts) > len(marker):
|
|
151
|
+
aliases.add('.'.join(normalized_parts[len(marker):]))
|
|
152
|
+
|
|
153
|
+
for idx, part in enumerate(normalized_parts):
|
|
154
|
+
if part == 'src' and idx + 1 < len(normalized_parts):
|
|
155
|
+
aliases.add('.'.join(normalized_parts[idx + 1:]))
|
|
156
|
+
|
|
157
|
+
return {alias for alias in aliases if alias}
|
|
158
|
+
|
|
159
|
+
def resolve_import_target(self, target: str) -> str | None:
|
|
160
|
+
if target in self.nodes_by_id and self.nodes_by_id[target]['type'] == 'Module':
|
|
161
|
+
return target
|
|
162
|
+
|
|
163
|
+
direct = self.alias_to_module_ids.get(target)
|
|
164
|
+
if direct and len(direct) == 1:
|
|
165
|
+
return next(iter(direct))
|
|
166
|
+
|
|
167
|
+
parts = target.split('.')
|
|
168
|
+
while len(parts) > 1:
|
|
169
|
+
parts = parts[:-1]
|
|
170
|
+
candidate = '.'.join(parts)
|
|
171
|
+
matches = self.alias_to_module_ids.get(candidate)
|
|
172
|
+
if matches and len(matches) == 1:
|
|
173
|
+
return next(iter(matches))
|
|
174
|
+
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def _classify_imports(self, imports: set[str]) -> tuple[list[tuple[str, str]], list[str]]:
|
|
178
|
+
internal: list[tuple[str, str]] = []
|
|
179
|
+
external: list[str] = []
|
|
180
|
+
for imp in sorted(imports):
|
|
181
|
+
resolved = self.resolve_import_target(imp)
|
|
182
|
+
if resolved:
|
|
183
|
+
internal.append((imp, resolved))
|
|
184
|
+
else:
|
|
185
|
+
external.append(imp)
|
|
186
|
+
return internal, external
|
|
187
|
+
|
|
188
|
+
def resolve_to_module_id(self, query: str) -> str | None:
|
|
189
|
+
"""将文件路径或 module id 统一解析为 module id。"""
|
|
190
|
+
# 尝试直接作为 module id
|
|
191
|
+
if query in self.nodes_by_id and self.nodes_by_id[query]['type'] == 'Module':
|
|
192
|
+
return query
|
|
193
|
+
# 尝试作为文件路径(兼容 \\ 和 /)
|
|
194
|
+
normalized = query.replace('\\', '/')
|
|
195
|
+
if normalized in self.path_to_module_id:
|
|
196
|
+
return self.path_to_module_id[normalized]
|
|
197
|
+
# 模糊匹配:去掉开头的 repo 相对路径前缀
|
|
198
|
+
for path, mid in self.path_to_module_id.items():
|
|
199
|
+
if path.endswith(normalized) or normalized.endswith(path):
|
|
200
|
+
return mid
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def resolve_to_path(self, module_id: str) -> str | None:
|
|
204
|
+
"""将 module id 解析为文件路径。"""
|
|
205
|
+
node = self.nodes_by_id.get(module_id)
|
|
206
|
+
if node:
|
|
207
|
+
return node.get('path')
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
# ── 查询模式实现 ──────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
def query_file(self, file_query: str) -> str:
|
|
213
|
+
"""--file: 查看某个文件的完整结构和 import 清单。"""
|
|
214
|
+
mid = self.resolve_to_module_id(file_query)
|
|
215
|
+
if not mid:
|
|
216
|
+
return f"[NOT FOUND] No module matching '{file_query}'"
|
|
217
|
+
|
|
218
|
+
module_node = self.nodes_by_id[mid]
|
|
219
|
+
path = module_node.get('path', mid)
|
|
220
|
+
lines = module_node.get('lines', '?')
|
|
221
|
+
lang = module_node.get('lang', '?')
|
|
222
|
+
|
|
223
|
+
out = [f"=== {path} ==="]
|
|
224
|
+
out.append(f"Module: {mid} ({lines} lines, {lang})")
|
|
225
|
+
out.append("")
|
|
226
|
+
|
|
227
|
+
# 类和函数
|
|
228
|
+
classes = [n for n in self.contains_children.get(mid, []) if n['type'] == 'Class']
|
|
229
|
+
top_funcs = [n for n in self.contains_children.get(mid, []) if n['type'] == 'Function']
|
|
230
|
+
|
|
231
|
+
if classes:
|
|
232
|
+
out.append("Classes:")
|
|
233
|
+
for cls in classes:
|
|
234
|
+
sl = cls.get('start_line', '?')
|
|
235
|
+
el = cls.get('end_line', '?')
|
|
236
|
+
out.append(f" {cls['label']} (L{sl}-L{el})")
|
|
237
|
+
methods = [n for n in self.contains_children.get(cls['id'], []) if n['type'] == 'Function']
|
|
238
|
+
for i, m in enumerate(methods):
|
|
239
|
+
prefix = "└─" if i == len(methods) - 1 else "├─"
|
|
240
|
+
ml = m.get('start_line', '?')
|
|
241
|
+
me = m.get('end_line', '?')
|
|
242
|
+
out.append(f" {prefix} {m['label']} (L{ml}-L{me})")
|
|
243
|
+
out.append("")
|
|
244
|
+
|
|
245
|
+
if top_funcs:
|
|
246
|
+
out.append("Top-level Functions:")
|
|
247
|
+
for f in top_funcs:
|
|
248
|
+
sl = f.get('start_line', '?')
|
|
249
|
+
el = f.get('end_line', '?')
|
|
250
|
+
out.append(f" {f['label']} (L{sl}-L{el})")
|
|
251
|
+
out.append("")
|
|
252
|
+
|
|
253
|
+
# Imports
|
|
254
|
+
imports = sorted(self.imports_forward.get(mid, set()))
|
|
255
|
+
if imports:
|
|
256
|
+
internal, external = self._classify_imports(set(imports))
|
|
257
|
+
out.append("Imports:")
|
|
258
|
+
for raw_imp, resolved_imp in internal:
|
|
259
|
+
imp_path = self.resolve_to_path(resolved_imp)
|
|
260
|
+
suffix = f" ({imp_path})" if imp_path else ""
|
|
261
|
+
if raw_imp == resolved_imp:
|
|
262
|
+
out.append(f" → {raw_imp}{suffix}")
|
|
263
|
+
else:
|
|
264
|
+
out.append(f" → {raw_imp} [resolved: {resolved_imp}{suffix}]")
|
|
265
|
+
for imp in external:
|
|
266
|
+
out.append(f" → {imp} (external)")
|
|
267
|
+
out.append("")
|
|
268
|
+
|
|
269
|
+
if not classes and not top_funcs and not imports:
|
|
270
|
+
out.append("(no classes, functions, or imports detected)")
|
|
271
|
+
|
|
272
|
+
# Git stats (可选)
|
|
273
|
+
if self.git:
|
|
274
|
+
git_lines = self.git.format_risk_block(path)
|
|
275
|
+
if git_lines:
|
|
276
|
+
out.append("Git:")
|
|
277
|
+
out.extend(f" {l}" for l in git_lines)
|
|
278
|
+
out.append("")
|
|
279
|
+
|
|
280
|
+
return "\n".join(out)
|
|
281
|
+
|
|
282
|
+
def query_who_imports(self, module_query: str) -> str:
|
|
283
|
+
"""--who-imports: 反向依赖查询。"""
|
|
284
|
+
mid = self.resolve_to_module_id(module_query)
|
|
285
|
+
|
|
286
|
+
# 也尝试直接在 imports_reverse 中查找(处理外部包名等)
|
|
287
|
+
if not mid:
|
|
288
|
+
# 可能是部分匹配(如 'flask' 在 imports target 中)
|
|
289
|
+
matches = set()
|
|
290
|
+
normalized = module_query.replace('\\', '/')
|
|
291
|
+
for target, sources in self.imports_reverse.items():
|
|
292
|
+
if target == normalized or target == module_query:
|
|
293
|
+
matches.update(sources)
|
|
294
|
+
if matches:
|
|
295
|
+
return self._format_who_imports(module_query, matches)
|
|
296
|
+
return f"[NOT FOUND] No module matching '{module_query}'"
|
|
297
|
+
|
|
298
|
+
importers: set[str] = set(self.internal_imports_reverse.get(mid, set()))
|
|
299
|
+
|
|
300
|
+
return self._format_who_imports(mid, importers)
|
|
301
|
+
|
|
302
|
+
def _format_who_imports(self, query: str, importers: set[str]) -> str:
|
|
303
|
+
out = [f"=== Who imports {query}? ==="]
|
|
304
|
+
if not importers:
|
|
305
|
+
out.append("Not imported by any module in the project.")
|
|
306
|
+
return "\n".join(out)
|
|
307
|
+
|
|
308
|
+
out.append(f"Imported by {len(importers)} module(s):")
|
|
309
|
+
for imp in sorted(importers):
|
|
310
|
+
imp_path = self.resolve_to_path(imp)
|
|
311
|
+
suffix = f" ({imp_path})" if imp_path else ""
|
|
312
|
+
out.append(f" ← {imp}{suffix}")
|
|
313
|
+
return "\n".join(out)
|
|
314
|
+
|
|
315
|
+
def query_impact(self, file_query: str) -> str:
|
|
316
|
+
"""--impact: 影响半径分析(上下游依赖一览)。"""
|
|
317
|
+
mid = self.resolve_to_module_id(file_query)
|
|
318
|
+
if not mid:
|
|
319
|
+
return f"[NOT FOUND] No module matching '{file_query}'"
|
|
320
|
+
|
|
321
|
+
module_node = self.nodes_by_id[mid]
|
|
322
|
+
path = module_node.get('path', mid)
|
|
323
|
+
|
|
324
|
+
out = [f"=== Impact radius: {path} ===", ""]
|
|
325
|
+
|
|
326
|
+
# 上游:本文件 import 了谁
|
|
327
|
+
forward = sorted(self.imports_forward.get(mid, set()))
|
|
328
|
+
internal_forward, external_forward = self._classify_imports(set(forward))
|
|
329
|
+
|
|
330
|
+
out.append("Depends on (this file imports):")
|
|
331
|
+
if internal_forward:
|
|
332
|
+
for raw_dep, resolved_dep in internal_forward:
|
|
333
|
+
dep_path = self.resolve_to_path(resolved_dep)
|
|
334
|
+
suffix = f" ({dep_path})" if dep_path else ""
|
|
335
|
+
if raw_dep == resolved_dep:
|
|
336
|
+
out.append(f" → {raw_dep}{suffix}")
|
|
337
|
+
else:
|
|
338
|
+
out.append(f" → {raw_dep} [resolved: {resolved_dep}{suffix}]")
|
|
339
|
+
if external_forward:
|
|
340
|
+
for dep in external_forward:
|
|
341
|
+
out.append(f" → {dep} (external)")
|
|
342
|
+
if not forward:
|
|
343
|
+
out.append(" (none)")
|
|
344
|
+
out.append("")
|
|
345
|
+
|
|
346
|
+
# 下游:谁 import 了本文件
|
|
347
|
+
importers: set[str] = set(self.internal_imports_reverse.get(mid, set()))
|
|
348
|
+
|
|
349
|
+
out.append("Depended by (other files import this):")
|
|
350
|
+
if importers:
|
|
351
|
+
for imp in sorted(importers):
|
|
352
|
+
imp_path = self.resolve_to_path(imp)
|
|
353
|
+
suffix = f" ({imp_path})" if imp_path else ""
|
|
354
|
+
out.append(f" ← {imp}{suffix}")
|
|
355
|
+
else:
|
|
356
|
+
out.append(" (none)")
|
|
357
|
+
out.append("")
|
|
358
|
+
|
|
359
|
+
downstream_count = len(importers)
|
|
360
|
+
upstream_count = len({resolved for _raw, resolved in internal_forward})
|
|
361
|
+
out.append(
|
|
362
|
+
f"Impact summary: {upstream_count} upstream dependencies, "
|
|
363
|
+
f"{downstream_count} downstream dependents"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# Git stats (可选)
|
|
367
|
+
if self.git:
|
|
368
|
+
out.append("")
|
|
369
|
+
git_lines = self.git.format_risk_block(path)
|
|
370
|
+
if git_lines:
|
|
371
|
+
out.extend(git_lines)
|
|
372
|
+
|
|
373
|
+
return "\n".join(out)
|
|
374
|
+
|
|
375
|
+
def query_hub_analysis(self, top_n: int = 10) -> str:
|
|
376
|
+
"""--hub-analysis: 高扇入/高扇出核心节点识别。"""
|
|
377
|
+
fan_in = {target: len(sources) for target, sources in self.internal_imports_reverse.items()}
|
|
378
|
+
fan_out = {source: len(targets) for source, targets in self.internal_imports_forward.items()}
|
|
379
|
+
|
|
380
|
+
out = ["=== Hub Analysis ===", ""]
|
|
381
|
+
|
|
382
|
+
# Top fan-in
|
|
383
|
+
top_fan_in = sorted(fan_in.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
|
384
|
+
out.append("Top fan-in (most imported by others):")
|
|
385
|
+
if top_fan_in:
|
|
386
|
+
for i, (mid, count) in enumerate(top_fan_in, 1):
|
|
387
|
+
path = self.resolve_to_path(mid) or ""
|
|
388
|
+
out.append(f" {i}. {mid} — imported by {count} module(s) [{path}]")
|
|
389
|
+
else:
|
|
390
|
+
out.append(" (no internal import relationships found)")
|
|
391
|
+
out.append("")
|
|
392
|
+
|
|
393
|
+
# Top fan-out
|
|
394
|
+
top_fan_out = sorted(fan_out.items(), key=lambda x: x[1], reverse=True)[:top_n]
|
|
395
|
+
out.append("Top fan-out (imports most others):")
|
|
396
|
+
if top_fan_out:
|
|
397
|
+
for i, (mid, count) in enumerate(top_fan_out, 1):
|
|
398
|
+
path = self.resolve_to_path(mid) or ""
|
|
399
|
+
out.append(f" {i}. {mid} — imports {count} internal module(s) [{path}]")
|
|
400
|
+
else:
|
|
401
|
+
out.append(" (no internal import relationships found)")
|
|
402
|
+
|
|
403
|
+
return "\n".join(out)
|
|
404
|
+
|
|
405
|
+
def query_summary(self) -> str:
|
|
406
|
+
"""--summary: 按顶层目录聚合的结构摘要。"""
|
|
407
|
+
# 按第一级或第二级目录聚合
|
|
408
|
+
dir_stats: dict[str, dict] = defaultdict(
|
|
409
|
+
lambda: {'modules': 0, 'classes': 0, 'functions': 0, 'lines': 0,
|
|
410
|
+
'class_names': [], 'import_dirs': set()}
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# 决定聚合粒度:取 path 的前 2 级目录
|
|
414
|
+
def _dir_key(path: str) -> str:
|
|
415
|
+
parts = path.split('/')
|
|
416
|
+
if len(parts) <= 2:
|
|
417
|
+
return parts[0] + '/'
|
|
418
|
+
return '/'.join(parts[:2]) + '/'
|
|
419
|
+
|
|
420
|
+
for node in self.nodes:
|
|
421
|
+
path = node.get('path', '')
|
|
422
|
+
if not path:
|
|
423
|
+
continue
|
|
424
|
+
dk = _dir_key(path)
|
|
425
|
+
ntype = node['type']
|
|
426
|
+
if ntype == 'Module':
|
|
427
|
+
dir_stats[dk]['modules'] += 1
|
|
428
|
+
dir_stats[dk]['lines'] += node.get('lines', 0)
|
|
429
|
+
elif ntype == 'Class':
|
|
430
|
+
dir_stats[dk]['classes'] += 1
|
|
431
|
+
dir_stats[dk]['class_names'].append(node['label'])
|
|
432
|
+
elif ntype == 'Function':
|
|
433
|
+
dir_stats[dk]['functions'] += 1
|
|
434
|
+
|
|
435
|
+
# 收集每个目录的 import 来源目录
|
|
436
|
+
for mid, targets in self.imports_forward.items():
|
|
437
|
+
src_node = self.nodes_by_id.get(mid)
|
|
438
|
+
if not src_node or src_node['type'] != 'Module':
|
|
439
|
+
continue
|
|
440
|
+
src_path = src_node.get('path', '')
|
|
441
|
+
if not src_path:
|
|
442
|
+
continue
|
|
443
|
+
src_dk = _dir_key(src_path)
|
|
444
|
+
for t in targets:
|
|
445
|
+
t_node = self.nodes_by_id.get(t)
|
|
446
|
+
if t_node and t_node.get('path'):
|
|
447
|
+
t_dk = _dir_key(t_node['path'])
|
|
448
|
+
if t_dk != src_dk:
|
|
449
|
+
dir_stats[src_dk]['import_dirs'].add(t_dk.rstrip('/'))
|
|
450
|
+
|
|
451
|
+
out = ["=== Directory Summary ===", ""]
|
|
452
|
+
|
|
453
|
+
for dk in sorted(dir_stats.keys()):
|
|
454
|
+
s = dir_stats[dk]
|
|
455
|
+
out.append(
|
|
456
|
+
f"{dk} ({s['modules']} modules, {s['classes']} classes, "
|
|
457
|
+
f"{s['functions']} functions, {s['lines']} lines)"
|
|
458
|
+
)
|
|
459
|
+
if s['class_names']:
|
|
460
|
+
# 最多显示 8 个
|
|
461
|
+
names = s['class_names'][:8]
|
|
462
|
+
suffix = f" ... +{len(s['class_names']) - 8}" if len(s['class_names']) > 8 else ""
|
|
463
|
+
out.append(f" Key classes: {', '.join(names)}{suffix}")
|
|
464
|
+
import_dirs = sorted(s['import_dirs'])
|
|
465
|
+
if import_dirs:
|
|
466
|
+
out.append(f" Key imports from: {', '.join(import_dirs)}")
|
|
467
|
+
else:
|
|
468
|
+
out.append(f" Key imports from: (none / external only)")
|
|
469
|
+
out.append("")
|
|
470
|
+
|
|
471
|
+
if not dir_stats:
|
|
472
|
+
out.append("(no modules found in ast_nodes.json)")
|
|
473
|
+
|
|
474
|
+
return "\n".join(out)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def main() -> None:
|
|
478
|
+
parser = argparse.ArgumentParser(
|
|
479
|
+
description='Query AST graph from ast_nodes.json',
|
|
480
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
481
|
+
epilog="""
|
|
482
|
+
Examples:
|
|
483
|
+
%(prog)s ast_nodes.json --file src/server/handler.py
|
|
484
|
+
%(prog)s ast_nodes.json --who-imports src.server.handler
|
|
485
|
+
%(prog)s ast_nodes.json --impact src/server/handler.py
|
|
486
|
+
%(prog)s ast_nodes.json --hub-analysis --top 10
|
|
487
|
+
%(prog)s ast_nodes.json --summary
|
|
488
|
+
""",
|
|
489
|
+
)
|
|
490
|
+
parser.add_argument('ast_json', help='Path to ast_nodes.json')
|
|
491
|
+
parser.add_argument('--file', dest='file_query', help='Show structure and imports of a file')
|
|
492
|
+
parser.add_argument('--who-imports', dest='who_imports', help='Find modules that import the given module')
|
|
493
|
+
parser.add_argument('--impact', dest='impact_query', help='Show impact radius (deps + dependents)')
|
|
494
|
+
parser.add_argument('--hub-analysis', action='store_true', help='Show top fan-in/fan-out modules')
|
|
495
|
+
parser.add_argument('--summary', action='store_true', help='Show per-directory structural summary')
|
|
496
|
+
parser.add_argument('--top', type=int, default=10, help='Number of results for hub-analysis (default: 10)')
|
|
497
|
+
parser.add_argument('--git-stats', dest='git_stats_path', metavar='GIT_STATS_JSON',
|
|
498
|
+
help='Optional git_stats.json to enrich --file and --impact with risk/coupling data')
|
|
499
|
+
|
|
500
|
+
args = parser.parse_args()
|
|
501
|
+
|
|
502
|
+
# 检查至少有一个查询模式
|
|
503
|
+
has_query = any([args.file_query, args.who_imports, args.impact_query,
|
|
504
|
+
args.hub_analysis, args.summary])
|
|
505
|
+
if not has_query:
|
|
506
|
+
parser.print_help()
|
|
507
|
+
sys.exit(1)
|
|
508
|
+
|
|
509
|
+
# 加载 JSON
|
|
510
|
+
ast_path = Path(args.ast_json)
|
|
511
|
+
if not ast_path.exists():
|
|
512
|
+
sys.stderr.write(f"[ERROR] File not found: {ast_path}\n")
|
|
513
|
+
sys.exit(1)
|
|
514
|
+
|
|
515
|
+
try:
|
|
516
|
+
raw_text = ast_path.read_text(encoding='utf-8')
|
|
517
|
+
# 跳过可能混入的 stderr 行(如 [WARNING]),定位到第一个 '{' 开始解析
|
|
518
|
+
json_start = raw_text.find('{')
|
|
519
|
+
if json_start < 0:
|
|
520
|
+
sys.stderr.write(f"[ERROR] No JSON object found in {ast_path}\n")
|
|
521
|
+
sys.exit(1)
|
|
522
|
+
data = json.loads(raw_text[json_start:])
|
|
523
|
+
except json.JSONDecodeError as e:
|
|
524
|
+
sys.stderr.write(f"[ERROR] Invalid JSON: {e}\n")
|
|
525
|
+
sys.exit(1)
|
|
526
|
+
|
|
527
|
+
# 可选加载 git stats
|
|
528
|
+
git_stats: GitStats | None = None
|
|
529
|
+
if args.git_stats_path:
|
|
530
|
+
gs_path = Path(args.git_stats_path)
|
|
531
|
+
if not gs_path.exists():
|
|
532
|
+
sys.stderr.write(f"[WARNING] git_stats file not found: {gs_path}, ignoring\n")
|
|
533
|
+
else:
|
|
534
|
+
try:
|
|
535
|
+
gs_data = json.loads(gs_path.read_text(encoding='utf-8'))
|
|
536
|
+
git_stats = GitStats(gs_data)
|
|
537
|
+
except (json.JSONDecodeError, KeyError) as e:
|
|
538
|
+
sys.stderr.write(f"[WARNING] git_stats parse error: {e}, ignoring\n")
|
|
539
|
+
|
|
540
|
+
graph = ASTGraph(data, git_stats=git_stats)
|
|
541
|
+
|
|
542
|
+
# 执行查询
|
|
543
|
+
if args.file_query:
|
|
544
|
+
print(graph.query_file(args.file_query))
|
|
545
|
+
elif args.who_imports:
|
|
546
|
+
print(graph.query_who_imports(args.who_imports))
|
|
547
|
+
elif args.impact_query:
|
|
548
|
+
print(graph.query_impact(args.impact_query))
|
|
549
|
+
elif args.hub_analysis:
|
|
550
|
+
print(graph.query_hub_analysis(args.top))
|
|
551
|
+
elif args.summary:
|
|
552
|
+
print(graph.query_summary())
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
if __name__ == '__main__':
|
|
556
|
+
main()
|
|
@@ -13,10 +13,16 @@ description: 分析运行时行为、进程边界和 IPC 机制,检测"协议
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
-
## ⚠️
|
|
16
|
+
## ⚠️ 深度思考要求
|
|
17
17
|
|
|
18
18
|
> [!IMPORTANT]
|
|
19
|
-
>
|
|
19
|
+
> **运行时分析需要深度思考,思考方式基于模型能力和任务复杂度。**
|
|
20
|
+
>
|
|
21
|
+
> **核心判断规则**:
|
|
22
|
+
> - **无 CoT 模型** → **必须调用** `sequential-thinking` CLI
|
|
23
|
+
> - **有 CoT 模型 + 简单项目**(单进程、通信清晰)→ 用思考引导问题组织自然 CoT
|
|
24
|
+
> - **有 CoT 模型 + 复杂项目**(多进程、需要修正前提)→ 调用 `sequential-thinking` CLI
|
|
25
|
+
>
|
|
20
26
|
> 思考内容例如:
|
|
21
27
|
> 1. "这个项目有多少个入口点(`main` 函数)?它们是一个进程还是多个?"
|
|
22
28
|
> 2. "进程之间用什么通信?Pipe?HTTP?共享数据库?"
|
|
@@ -30,15 +30,52 @@ license: MIT
|
|
|
30
30
|
|
|
31
31
|
## When to Use
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
### 核心判断规则
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
- 初始范围或方法不明确,需要先拆问题、再形成方法
|
|
37
|
-
- 需要在有限候选方案之间做比较,而不是无限发散
|
|
38
|
-
- 需要回看已有判断、识别漏洞、证据不足与隐含假设
|
|
39
|
-
- 需要留下可回放、可导出的推理轨迹
|
|
35
|
+
> **模型能力决定基线,任务复杂度决定是否升级。**
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
| 模型能力 | 简单任务 | 复杂任务 |
|
|
38
|
+
|---------|:-------:|:-------:|
|
|
39
|
+
| **有思维链(CoT)** | 自然 CoT 即可 | 调用 ST CLI |
|
|
40
|
+
| **无思维链** | **必须调用 ST CLI** | **必须调用 ST CLI** |
|
|
41
|
+
|
|
42
|
+
### 判断口诀
|
|
43
|
+
|
|
44
|
+
> **"无 CoT → 必须用 ST**
|
|
45
|
+
> **有 CoT → 复杂才用 ST"**
|
|
46
|
+
|
|
47
|
+
### 必须调用 CLI 的场景
|
|
48
|
+
|
|
49
|
+
| 场景 | 判断标准 | 为什么需要 CLI |
|
|
50
|
+
|------|---------|---------------|
|
|
51
|
+
| **无 CoT 模型** | 当前模型不支持思维链输出 | 必须依赖外部工具组织推理 |
|
|
52
|
+
| **修正前提** | 推理过程中发现前面判断错了,需要回头修正 | CLI 会话保留历史,支持回看修正 |
|
|
53
|
+
| **多方案比较** | 需要在 2+ 个候选方案之间做权衡决策 | `branch` 模式专为分支比较设计 |
|
|
54
|
+
| **可回放轨迹** | 需要留下可审计、可复现的推理过程 | CLI 支持生成 replay 文档 |
|
|
55
|
+
| **复杂收敛** | 问题需要 > 5 步才能收敛到结论 | 强制步数限制防止无限发散 |
|
|
56
|
+
|
|
57
|
+
### 有 CoT 时可直接用自然 CoT 的场景
|
|
58
|
+
|
|
59
|
+
| 场景 | 判断标准 | 为什么不需要 CLI |
|
|
60
|
+
|------|---------|-----------------|
|
|
61
|
+
| **单向推理** | 不需要回头修正,线性推进 | 模型自然输出即可 |
|
|
62
|
+
| **简单分析** | 问题边界清晰、步骤 < 5 | 不需要复杂工具辅助 |
|
|
63
|
+
| **快速决策** | 只需结论,不需要可回放轨迹 | CoT 足够表达推理 |
|
|
64
|
+
| **探索性思考** | 还在发散阶段,不确定是否需要收敛 | 先用 CoT 探索,再决定是否用 CLI |
|
|
65
|
+
|
|
66
|
+
### 决策树
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
flowchart TD
|
|
70
|
+
A[需要多步推理?] -->|否| Z[直接回答]
|
|
71
|
+
A -->|是| B{模型有 CoT?}
|
|
72
|
+
B -->|否| CLI[必须调用 ST CLI]
|
|
73
|
+
B -->|是| C{任务复杂?}
|
|
74
|
+
C -->|简单: 步骤 < 5, 无修正| CoT[自然 CoT]
|
|
75
|
+
C -->|复杂: 修正/比较/回放| CLI
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 不适用场景
|
|
42
79
|
|
|
43
80
|
- 简单事实查询
|
|
44
81
|
- 单步即可完成的任务
|