@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.
@@ -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()
@@ -0,0 +1,6 @@
1
+ # nexus-mapper scripts 依赖
2
+ # 用于独立使用或分发时安装
3
+ # 在 Nexus 项目内部使用时,依赖已由 poetry 环境覆盖(tree-sitter ^0.25.2 + tree-sitter-language-pack)
4
+
5
+ tree-sitter>=0.22.0
6
+ tree-sitter-language-pack
@@ -13,10 +13,16 @@ description: 分析运行时行为、进程边界和 IPC 机制,检测"协议
13
13
 
14
14
  ---
15
15
 
16
- ## ⚠️ 强制深度思考
16
+ ## ⚠️ 深度思考要求
17
17
 
18
18
  > [!IMPORTANT]
19
- > 在执行任何分析之前,你**必须**使用 `sequential-thinking` skill,视情况组织 **3—5 个 thought** 推理。
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
  - 单步即可完成的任务