@haaaiawd/anws 2.2.6 → 2.4.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.
Files changed (94) hide show
  1. package/README.md +1 -1
  2. package/bin/cli.js +52 -22
  3. package/lib/diff.js +5 -2
  4. package/lib/init.js +217 -96
  5. package/lib/install-state.js +18 -3
  6. package/lib/manifest.js +510 -213
  7. package/lib/prompt.js +68 -0
  8. package/lib/resources/index.js +36 -2
  9. package/lib/update.js +12 -6
  10. package/package.json +48 -47
  11. package/templates/.agents/skills/anws-system/SKILL.md +108 -108
  12. package/templates/.agents/skills/code-reviewer/SKILL.md +170 -103
  13. package/templates/.agents/skills/concept-modeler/SKILL.md +230 -179
  14. package/templates/.agents/skills/craft-authoring/SKILL.md +112 -49
  15. package/templates/.agents/skills/craft-authoring/references/BUNDLE_POLICY.md +61 -0
  16. package/templates/.agents/skills/craft-authoring/references/PROMPT_QUALITY_RUBRIC.md +99 -0
  17. package/templates/.agents/skills/craft-authoring/references/SCORECARD_TEMPLATE.md +64 -0
  18. package/templates/.agents/skills/design-reviewer/SKILL.md +265 -190
  19. package/templates/.agents/skills/e2e-testing-guide/SKILL.md +246 -135
  20. package/templates/.agents/skills/nexus-mapper/SKILL.md +321 -321
  21. package/templates/.agents/skills/output-contract/SKILL.md +37 -0
  22. package/templates/.agents/skills/report-template/SKILL.md +92 -92
  23. package/templates/.agents/skills/sequential-thinking/SKILL.md +222 -225
  24. package/templates/.agents/skills/spec-writer/SKILL.md +75 -30
  25. package/templates/.agents/skills/system-architect/SKILL.md +538 -678
  26. package/templates/.agents/skills/system-designer/SKILL.md +601 -601
  27. package/templates/.agents/skills/task-planner/SKILL.md +1 -2
  28. package/templates/.agents/skills/task-reviewer/SKILL.md +428 -388
  29. package/templates/.agents/skills/tech-evaluator/SKILL.md +252 -144
  30. package/templates/.agents/workflows/blueprint.md +166 -43
  31. package/templates/.agents/workflows/challenge.md +331 -497
  32. package/templates/.agents/workflows/change.md +182 -339
  33. package/templates/.agents/workflows/craft.md +159 -236
  34. package/templates/.agents/workflows/design-system.md +202 -674
  35. package/templates/.agents/workflows/explore.md +187 -399
  36. package/templates/.agents/workflows/forge.md +650 -550
  37. package/templates/.agents/workflows/genesis.md +439 -351
  38. package/templates/.agents/workflows/probe.md +219 -241
  39. package/templates/.agents/workflows/quickstart.md +302 -123
  40. package/templates/.agents/workflows/upgrade.md +145 -182
  41. package/templates_en/.agents/skills/anws-system/SKILL.md +108 -0
  42. package/templates_en/.agents/skills/code-reviewer/SKILL.md +170 -0
  43. package/templates_en/.agents/skills/concept-modeler/SKILL.md +230 -0
  44. package/templates_en/.agents/skills/craft-authoring/SKILL.md +179 -0
  45. package/templates_en/.agents/skills/craft-authoring/references/BUNDLE_POLICY.md +60 -0
  46. package/templates_en/.agents/skills/craft-authoring/references/PROMPT_QUALITY_RUBRIC.md +92 -0
  47. package/templates_en/.agents/skills/craft-authoring/references/SCORECARD_TEMPLATE.md +52 -0
  48. package/templates_en/.agents/skills/design-reviewer/SKILL.md +265 -0
  49. package/templates_en/.agents/skills/e2e-testing-guide/SKILL.md +246 -0
  50. package/templates_en/.agents/skills/nexus-mapper/SKILL.md +306 -0
  51. package/templates_en/.agents/skills/nexus-mapper/references/language-customization.md +167 -0
  52. package/templates_en/.agents/skills/nexus-mapper/references/output-schema.md +311 -0
  53. package/templates_en/.agents/skills/nexus-mapper/references/probe-protocol.md +246 -0
  54. package/templates_en/.agents/skills/nexus-mapper/scripts/extract_ast.py +706 -0
  55. package/templates_en/.agents/skills/nexus-mapper/scripts/git_detective.py +194 -0
  56. package/templates_en/.agents/skills/nexus-mapper/scripts/languages.json +127 -0
  57. package/templates_en/.agents/skills/nexus-mapper/scripts/query_graph.py +556 -0
  58. package/templates_en/.agents/skills/nexus-mapper/scripts/requirements.txt +6 -0
  59. package/templates_en/.agents/skills/nexus-query/SKILL.md +114 -0
  60. package/templates_en/.agents/skills/nexus-query/scripts/extract_ast.py +706 -0
  61. package/templates_en/.agents/skills/nexus-query/scripts/git_detective.py +194 -0
  62. package/templates_en/.agents/skills/nexus-query/scripts/languages.json +127 -0
  63. package/templates_en/.agents/skills/nexus-query/scripts/query_graph.py +556 -0
  64. package/templates_en/.agents/skills/nexus-query/scripts/requirements.txt +6 -0
  65. package/templates_en/.agents/skills/output-contract/SKILL.md +37 -0
  66. package/templates_en/.agents/skills/report-template/SKILL.md +85 -0
  67. package/templates_en/.agents/skills/report-template/references/REPORT_TEMPLATE.md +100 -0
  68. package/templates_en/.agents/skills/runtime-inspector/SKILL.md +101 -0
  69. package/templates_en/.agents/skills/sequential-thinking/SKILL.md +214 -0
  70. package/templates_en/.agents/skills/spec-writer/SKILL.md +153 -0
  71. package/templates_en/.agents/skills/spec-writer/references/prd_template.md +177 -0
  72. package/templates_en/.agents/skills/system-architect/SKILL.md +538 -0
  73. package/templates_en/.agents/skills/system-architect/references/rfc_template.md +59 -0
  74. package/templates_en/.agents/skills/system-designer/SKILL.md +534 -0
  75. package/templates_en/.agents/skills/system-designer/references/system-design-detail-template.md +187 -0
  76. package/templates_en/.agents/skills/system-designer/references/system-design-template.md +605 -0
  77. package/templates_en/.agents/skills/task-planner/SKILL.md +251 -0
  78. package/templates_en/.agents/skills/task-planner/references/TASK_TEMPLATE_05A.md +109 -0
  79. package/templates_en/.agents/skills/task-planner/references/TASK_TEMPLATE_05B.md +176 -0
  80. package/templates_en/.agents/skills/task-reviewer/SKILL.md +428 -0
  81. package/templates_en/.agents/skills/tech-evaluator/SKILL.md +252 -0
  82. package/templates_en/.agents/skills/tech-evaluator/references/ADR_TEMPLATE.md +78 -0
  83. package/templates_en/.agents/workflows/blueprint.md +200 -0
  84. package/templates_en/.agents/workflows/challenge.md +331 -0
  85. package/templates_en/.agents/workflows/change.md +182 -0
  86. package/templates_en/.agents/workflows/craft.md +159 -0
  87. package/templates_en/.agents/workflows/design-system.md +202 -0
  88. package/templates_en/.agents/workflows/explore.md +187 -0
  89. package/templates_en/.agents/workflows/forge.md +651 -0
  90. package/templates_en/.agents/workflows/genesis.md +439 -0
  91. package/templates_en/.agents/workflows/probe.md +219 -0
  92. package/templates_en/.agents/workflows/quickstart.md +303 -0
  93. package/templates_en/.agents/workflows/upgrade.md +145 -0
  94. package/templates_en/AGENTS.md +149 -0
@@ -0,0 +1,556 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ query_graph.py — On-demand AST query tool
4
+
5
+ Read ast_nodes.json produced by extract_ast.py and provide multiple query modes,
6
+ output concise text that is easy for agents to consume.
7
+
8
+ Purpose:
9
+ - Assist REASON/OBJECT/EMIT stages in the PROBE workflow to generate cognition files
10
+ - Perform bug investigation, change impact analysis, and refactor analysis during development
11
+
12
+ Usage:
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
+ """Query helper for git_stats.json. Optional loading, no impact on core AST queries."""
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
+ """Generate git risk + coupling text lines for a file (empty list means no data)."""
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
+ """In-memory AST graph index supporting multiple query modes."""
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
+ # Indexes
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
+ """Resolve file path or module id to module id."""
190
+ # Try direct module id lookup
191
+ if query in self.nodes_by_id and self.nodes_by_id[query]['type'] == 'Module':
192
+ return query
193
+ # Try as file path (compatible with \\ and /)
194
+ normalized = query.replace('\\', '/')
195
+ if normalized in self.path_to_module_id:
196
+ return self.path_to_module_id[normalized]
197
+ # Fuzzy match: remove leading repo-relative path prefix
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
+ """Resolve module id to file path."""
205
+ node = self.nodes_by_id.get(module_id)
206
+ if node:
207
+ return node.get('path')
208
+ return None
209
+
210
+ # ── Query mode implementation ──────────────────────────────────────────────
211
+
212
+ def query_file(self, file_query: str) -> str:
213
+ """--file: Show full structure and import list for a file."""
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
+ # Classes and functions
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 (optional)
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: Reverse dependency query."""
284
+ mid = self.resolve_to_module_id(module_query)
285
+
286
+ # Also try direct lookup in imports_reverse (for external package names, etc.)
287
+ if not mid:
288
+ # Could be a partial match (e.g. 'flask' in import targets)
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: Impact radius analysis (upstream/downstream dependencies)."""
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
+ # Upstream: what this file imports
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
+ # Downstream: who imports this file
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 (optional)
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: Identify core nodes with high fan-in/fan-out."""
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: Structural summary aggregated by top-level directories."""
407
+ # Aggregate by first-level or second-level directory
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
+ # Aggregation granularity: use first 2 path levels
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
+ # Collect import source directories for each directory
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
+ # Show at most 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
+ # Ensure at least one query mode is provided
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
+ # Load 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
+ # Skip possible mixed-in stderr lines (e.g. [WARNING]); parse from first '{'
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
+ # Optionally load 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
+ # Execute query
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 dependencies
2
+ # For standalone use or distribution installation
3
+ # When used inside the Nexus project, dependencies are already covered by the poetry environment (tree-sitter ^0.25.2 + tree-sitter-language-pack)
4
+
5
+ tree-sitter>=0.22.0
6
+ tree-sitter-language-pack
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: output-contract
3
+ description: Load when persisting reports in this template bundle, running parallel child sessions, or aligning outputs across workflows. Holds shared on-disk spec and delegation loop; unrelated to craft-authoring (/craft scaffolds).
4
+ ---
5
+
6
+ # Output contract and collaboration loop
7
+
8
+ > If another skill or workflow repeats these bullets, replace with **one line pointing here** and keep only role- or step-specific deltas.
9
+
10
+ ## When to load
11
+
12
+ - Authoring architecture / review / exploration Markdown that will land in the repo.
13
+ - Parent session splits parallel children or path-sliced collaboration.
14
+ - Verifying traceability, duplicate storytelling, or single-writer rules.
15
+
16
+ ## Shared spec contract
17
+
18
+ - **Precise**: Verifiable statements must cite sources or `path:line` / section anchors; no unsourced strong claims.
19
+ - **Evidence-backed**: Findings / evidence / recommendations must trace back to concrete inputs or retrieval results.
20
+ - **Non-repetitive**: State each fact once; no duplicate narrative in summary vs body.
21
+ - **No filler**: Ban vague boilerplate (“needs optimization”, “should watch”) without a clear subject.
22
+
23
+ Normative blocks (CRITICAL, severity tables, gates, upstream/downstream contracts) **must not be deleted**; tightening targets execution chatter and repetitive storytelling only.
24
+
25
+ ## Delegation loop (parent ↔ child)
26
+
27
+ 1. **Parent session**: `TARGET_DIR`, round, final paths, merge order, **sole write** to normalized report paths.
28
+ 2. **Child session**: Bounded slice; same evidence rules as parent.
29
+ 3. **Handoff**: Child returns mergeable table structure + one-line verdict; parent dedupes, checks against spec, writes `07_*` / explore reports, then runs workflow **completion** at the end.
30
+
31
+ Without delegation: single session runs the full workflow; parent still owns final persistence and consistency.
32
+
33
+ ## Parallelism and paths
34
+
35
+ - At most **one active writer per managed path (or glob)** per batch; no repo-wide format passes outside authorized globs.
36
+ - Child scopes must be **disjoint** by directory or topic; parent arbitrates conflicts.
37
+ - Checklist phrasing aligned with `/genesis` lives in **`genesis.md`** “Sub-agent orchestration and handoff checklist” (same section title in EN trees).
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: report-template
3
+ description: Synthesize all Probe-stage analysis (nexus-mapper, runtime-inspector) into a decision-ready system risk report.
4
+ ---
5
+
6
+ # The Synthesizer's Manual
7
+
8
+ > "Data is not information. Information is not knowledge. Knowledge is not wisdom." -- T.S. Eliot
9
+
10
+ Your goal is to convert raw analysis into **wisdom architects can act on**.
11
+
12
+ ---
13
+
14
+ ## Mandatory Self-Correction
15
+
16
+ > [!IMPORTANT]
17
+ > Before generating the report, you **must** self-check:
18
+ > 1. "Do build boundaries from nexus-mapper align with IPC boundaries from runtime-inspector?"
19
+ > 2. "Do high-coupling file pairs from nexus-mapper cross build boundaries?"
20
+ > 3. "Are missing components identified by nexus-mapper related to discovered risks?"
21
+ > 4. "Is this report complete enough?"
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ 1. **Read template (MANDATORY)**: Read `references/REPORT_TEMPLATE.md`. Your report **must** match this structure exactly.
28
+ 2. **Synthesize all findings**: Merge outputs from:
29
+ * `nexus-mapper` -> Build Roots, Topology, Coupling Pairs, Hotspots, Entities, Missing Components
30
+ * `runtime-inspector` -> IPC Surfaces, Contract Status
31
+ 3. **Draft report**: Organize with explicit logical connections.
32
+ 4. **Publish (CRITICAL)**: You **must** create `.anws/v{N}/00_PROBE_REPORT.md` and write the full report. **Do not** only print in chat. Ensure `.anws/v{N}/` exists.
33
+
34
+ ---
35
+
36
+ ## Completion Checklist
37
+
38
+ Before moving to next phase, verify:
39
+ - [ ] Output file created: `.anws/v{N}/00_PROBE_REPORT.md`
40
+ - [ ] Includes: System Fingerprint, Component Map, Risk Matrix, Feature Landing Guide
41
+ - [ ] User has confirmed findings
42
+
43
+ ---
44
+
45
+ ## Synthesis Ritual
46
+
47
+ ### 1. Executive Summary
48
+ * **Elevator pitch**: Describe system health in 30 seconds.
49
+ * **Focus points**: technical debt, key risks, reliability.
50
+
51
+ ### 2. Dark Matter Detection
52
+ * Do not only list what exists. **List what is missing**.
53
+ * Checklist: logs? error handling? CI/CD? secret management? version handshake?
54
+
55
+ ### 3. Cross-Verification
56
+ * **nexus-mapper** says "Workspace managed uniformly"?
57
+ * **nexus-mapper** says "High coupling crosses build roots"?
58
+ * **Conclusion**: detect hidden logical coupling -> **refactor target**.
59
+
60
+ ### 4. Human Checkpoint
61
+ * Force user confirmation: "Is this report complete?"
62
+ * **Do not enter Blueprint before this report is signed off**.
63
+
64
+ ---
65
+
66
+ ## Master Rules
67
+
68
+ 1. **No hallucination**: Every claim must link to source files.
69
+ 2. **Brutal honesty**: Be direct. If it's a mess, say it's a mess.
70
+ 3. **Action-oriented**: Every listed issue must imply a path (refactor/rewrite/retain).
71
+
72
+ ---
73
+
74
+ ## Toolbox
75
+
76
+ * `references/REPORT_TEMPLATE.md`: main report template.
77
+
78
+ ---
79
+
80
+ ## Consumers
81
+
82
+ Direct consumer of this report in `/blueprint` phase:
83
+ * **System Architect**: depends on your risk list to design mitigation strategy.
84
+
85
+ Your analysis quality **directly determines** design quality in the next phase.