@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.
- package/README.md +1 -1
- package/bin/cli.js +52 -22
- package/lib/diff.js +5 -2
- package/lib/init.js +217 -96
- package/lib/install-state.js +18 -3
- package/lib/manifest.js +510 -213
- package/lib/prompt.js +68 -0
- package/lib/resources/index.js +36 -2
- package/lib/update.js +12 -6
- package/package.json +48 -47
- package/templates/.agents/skills/anws-system/SKILL.md +108 -108
- package/templates/.agents/skills/code-reviewer/SKILL.md +170 -103
- package/templates/.agents/skills/concept-modeler/SKILL.md +230 -179
- package/templates/.agents/skills/craft-authoring/SKILL.md +112 -49
- package/templates/.agents/skills/craft-authoring/references/BUNDLE_POLICY.md +61 -0
- package/templates/.agents/skills/craft-authoring/references/PROMPT_QUALITY_RUBRIC.md +99 -0
- package/templates/.agents/skills/craft-authoring/references/SCORECARD_TEMPLATE.md +64 -0
- package/templates/.agents/skills/design-reviewer/SKILL.md +265 -190
- package/templates/.agents/skills/e2e-testing-guide/SKILL.md +246 -135
- package/templates/.agents/skills/nexus-mapper/SKILL.md +321 -321
- package/templates/.agents/skills/output-contract/SKILL.md +37 -0
- package/templates/.agents/skills/report-template/SKILL.md +92 -92
- package/templates/.agents/skills/sequential-thinking/SKILL.md +222 -225
- package/templates/.agents/skills/spec-writer/SKILL.md +75 -30
- package/templates/.agents/skills/system-architect/SKILL.md +538 -678
- package/templates/.agents/skills/system-designer/SKILL.md +601 -601
- package/templates/.agents/skills/task-planner/SKILL.md +1 -2
- package/templates/.agents/skills/task-reviewer/SKILL.md +428 -388
- package/templates/.agents/skills/tech-evaluator/SKILL.md +252 -144
- package/templates/.agents/workflows/blueprint.md +166 -43
- package/templates/.agents/workflows/challenge.md +331 -497
- package/templates/.agents/workflows/change.md +182 -339
- package/templates/.agents/workflows/craft.md +159 -236
- package/templates/.agents/workflows/design-system.md +202 -674
- package/templates/.agents/workflows/explore.md +187 -399
- package/templates/.agents/workflows/forge.md +650 -550
- package/templates/.agents/workflows/genesis.md +439 -351
- package/templates/.agents/workflows/probe.md +219 -241
- package/templates/.agents/workflows/quickstart.md +302 -123
- package/templates/.agents/workflows/upgrade.md +145 -182
- package/templates_en/.agents/skills/anws-system/SKILL.md +108 -0
- package/templates_en/.agents/skills/code-reviewer/SKILL.md +170 -0
- package/templates_en/.agents/skills/concept-modeler/SKILL.md +230 -0
- package/templates_en/.agents/skills/craft-authoring/SKILL.md +179 -0
- package/templates_en/.agents/skills/craft-authoring/references/BUNDLE_POLICY.md +60 -0
- package/templates_en/.agents/skills/craft-authoring/references/PROMPT_QUALITY_RUBRIC.md +92 -0
- package/templates_en/.agents/skills/craft-authoring/references/SCORECARD_TEMPLATE.md +52 -0
- package/templates_en/.agents/skills/design-reviewer/SKILL.md +265 -0
- package/templates_en/.agents/skills/e2e-testing-guide/SKILL.md +246 -0
- package/templates_en/.agents/skills/nexus-mapper/SKILL.md +306 -0
- package/templates_en/.agents/skills/nexus-mapper/references/language-customization.md +167 -0
- package/templates_en/.agents/skills/nexus-mapper/references/output-schema.md +311 -0
- package/templates_en/.agents/skills/nexus-mapper/references/probe-protocol.md +246 -0
- package/templates_en/.agents/skills/nexus-mapper/scripts/extract_ast.py +706 -0
- package/templates_en/.agents/skills/nexus-mapper/scripts/git_detective.py +194 -0
- package/templates_en/.agents/skills/nexus-mapper/scripts/languages.json +127 -0
- package/templates_en/.agents/skills/nexus-mapper/scripts/query_graph.py +556 -0
- package/templates_en/.agents/skills/nexus-mapper/scripts/requirements.txt +6 -0
- package/templates_en/.agents/skills/nexus-query/SKILL.md +114 -0
- package/templates_en/.agents/skills/nexus-query/scripts/extract_ast.py +706 -0
- package/templates_en/.agents/skills/nexus-query/scripts/git_detective.py +194 -0
- package/templates_en/.agents/skills/nexus-query/scripts/languages.json +127 -0
- package/templates_en/.agents/skills/nexus-query/scripts/query_graph.py +556 -0
- package/templates_en/.agents/skills/nexus-query/scripts/requirements.txt +6 -0
- package/templates_en/.agents/skills/output-contract/SKILL.md +37 -0
- package/templates_en/.agents/skills/report-template/SKILL.md +85 -0
- package/templates_en/.agents/skills/report-template/references/REPORT_TEMPLATE.md +100 -0
- package/templates_en/.agents/skills/runtime-inspector/SKILL.md +101 -0
- package/templates_en/.agents/skills/sequential-thinking/SKILL.md +214 -0
- package/templates_en/.agents/skills/spec-writer/SKILL.md +153 -0
- package/templates_en/.agents/skills/spec-writer/references/prd_template.md +177 -0
- package/templates_en/.agents/skills/system-architect/SKILL.md +538 -0
- package/templates_en/.agents/skills/system-architect/references/rfc_template.md +59 -0
- package/templates_en/.agents/skills/system-designer/SKILL.md +534 -0
- package/templates_en/.agents/skills/system-designer/references/system-design-detail-template.md +187 -0
- package/templates_en/.agents/skills/system-designer/references/system-design-template.md +605 -0
- package/templates_en/.agents/skills/task-planner/SKILL.md +251 -0
- package/templates_en/.agents/skills/task-planner/references/TASK_TEMPLATE_05A.md +109 -0
- package/templates_en/.agents/skills/task-planner/references/TASK_TEMPLATE_05B.md +176 -0
- package/templates_en/.agents/skills/task-reviewer/SKILL.md +428 -0
- package/templates_en/.agents/skills/tech-evaluator/SKILL.md +252 -0
- package/templates_en/.agents/skills/tech-evaluator/references/ADR_TEMPLATE.md +78 -0
- package/templates_en/.agents/workflows/blueprint.md +200 -0
- package/templates_en/.agents/workflows/challenge.md +331 -0
- package/templates_en/.agents/workflows/change.md +182 -0
- package/templates_en/.agents/workflows/craft.md +159 -0
- package/templates_en/.agents/workflows/design-system.md +202 -0
- package/templates_en/.agents/workflows/explore.md +187 -0
- package/templates_en/.agents/workflows/forge.md +651 -0
- package/templates_en/.agents/workflows/genesis.md +439 -0
- package/templates_en/.agents/workflows/probe.md +219 -0
- package/templates_en/.agents/workflows/quickstart.md +303 -0
- package/templates_en/.agents/workflows/upgrade.md +145 -0
- 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.
|