feed-the-machine 1.3.0 → 1.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 (45) hide show
  1. package/ftm-audit/SKILL.md +383 -57
  2. package/ftm-brainstorm/SKILL.md +119 -51
  3. package/ftm-config/SKILL.md +1 -1
  4. package/ftm-council/SKILL.md +259 -31
  5. package/ftm-dashboard/SKILL.md +10 -10
  6. package/ftm-debug/SKILL.md +861 -54
  7. package/ftm-diagram/SKILL.md +1 -1
  8. package/ftm-executor/SKILL.md +6 -6
  9. package/ftm-git/SKILL.md +208 -22
  10. package/ftm-inbox/bin/start.sh +1 -1
  11. package/ftm-inbox/bin/status.sh +1 -1
  12. package/ftm-inbox/bin/stop.sh +1 -1
  13. package/ftm-intent/SKILL.md +0 -1
  14. package/ftm-map/SKILL.md +46 -14
  15. package/ftm-map/scripts/db.py +439 -118
  16. package/ftm-map/scripts/index.py +128 -54
  17. package/ftm-map/scripts/parser.py +89 -320
  18. package/ftm-map/scripts/queries/go-tags.scm +20 -0
  19. package/ftm-map/scripts/queries/javascript-tags.scm +19 -7
  20. package/ftm-map/scripts/queries/python-tags.scm +22 -8
  21. package/ftm-map/scripts/queries/ruby-tags.scm +19 -0
  22. package/ftm-map/scripts/queries/rust-tags.scm +37 -0
  23. package/ftm-map/scripts/queries/typescript-tags.scm +20 -8
  24. package/ftm-map/scripts/query.py +176 -24
  25. package/ftm-map/scripts/ranker.py +377 -0
  26. package/ftm-map/scripts/requirements.txt +3 -0
  27. package/ftm-map/scripts/setup.sh +11 -0
  28. package/ftm-map/scripts/test_db.py +355 -115
  29. package/ftm-map/scripts/test_parser.py +169 -101
  30. package/ftm-map/scripts/test_query.py +178 -61
  31. package/ftm-map/scripts/test_ranker.py +199 -0
  32. package/ftm-map/scripts/views.py +107 -61
  33. package/ftm-mind/SKILL.md +861 -11
  34. package/ftm-mind/references/event-registry.md +20 -0
  35. package/ftm-pause/SKILL.md +256 -37
  36. package/ftm-resume/SKILL.md +380 -75
  37. package/ftm-retro/SKILL.md +164 -27
  38. package/ftm-upgrade/SKILL.md +4 -4
  39. package/hooks/ftm-blackboard-enforcer.sh +2 -4
  40. package/install.sh +6 -1
  41. package/package.json +1 -1
  42. package/ftm-map/scripts/tests/fixtures/__init__.py +0 -0
  43. package/ftm-map/scripts/tests/fixtures/sample_project/api.ts +0 -16
  44. package/ftm-map/scripts/tests/fixtures/sample_project/auth.py +0 -15
  45. package/ftm-map/scripts/tests/fixtures/sample_project/utils.js +0 -16
@@ -1,29 +1,41 @@
1
1
  ; Functions
2
2
  (function_declaration
3
- name: (identifier) @name) @definition.function
3
+ name: (identifier) @name.definition.function) @definition.function
4
4
 
5
5
  ; Methods
6
6
  (method_definition
7
- name: (property_identifier) @name) @definition.method
7
+ name: (property_identifier) @name.definition.method) @definition.method
8
8
 
9
9
  ; Classes
10
10
  (class_declaration
11
- name: (type_identifier) @name) @definition.class
11
+ name: (type_identifier) @name.definition.class) @definition.class
12
12
 
13
- ; Arrow functions assigned to const/let variables
13
+ ; Arrow functions assigned to const/let
14
14
  (lexical_declaration
15
15
  (variable_declarator
16
- name: (identifier) @name
16
+ name: (identifier) @name.definition.function
17
17
  value: (arrow_function))) @definition.function
18
18
 
19
19
  ; Interfaces
20
20
  (interface_declaration
21
- name: (type_identifier) @name) @definition.class
21
+ name: (type_identifier) @name.definition.interface) @definition.interface
22
22
 
23
23
  ; Type aliases
24
24
  (type_alias_declaration
25
- name: (type_identifier) @name) @definition.type
25
+ name: (type_identifier) @name.definition.type) @definition.type
26
26
 
27
27
  ; Enums
28
28
  (enum_declaration
29
- name: (identifier) @name) @definition.class
29
+ name: (identifier) @name.definition.enum) @definition.enum
30
+
31
+ ; Call references
32
+ (call_expression
33
+ function: [
34
+ (identifier) @name.reference.call
35
+ (member_expression
36
+ property: (property_identifier) @name.reference.call)
37
+ ]) @reference.call
38
+
39
+ ; New expressions
40
+ (new_expression
41
+ constructor: (identifier) @name.reference.class) @reference.class
@@ -1,5 +1,16 @@
1
1
  #!/usr/bin/env python3
2
- """ftm-map query interface: structural and text queries against the code graph."""
2
+ """ftm-map query interface: structural and text queries against the code graph.
3
+
4
+ Supports five query modes:
5
+ --blast-radius SYMBOL Transitive reverse dependencies (who is affected)
6
+ --deps SYMBOL Transitive forward dependencies (what does it need)
7
+ --search QUERY BM25-ranked full-text search over symbols
8
+ --info SYMBOL Full symbol details with callers, callees, refs
9
+ --context PageRank-based context selection with token budgeting
10
+ --stats Database statistics overview
11
+
12
+ All output is JSON on stdout.
13
+ """
3
14
 
4
15
  import argparse
5
16
  import json
@@ -8,7 +19,19 @@ import sys
8
19
 
9
20
  sys.path.insert(0, os.path.dirname(__file__))
10
21
 
11
- from db import get_connection, get_symbol_by_name, get_transitive_deps, get_reverse_deps, fts_search
22
+ from db import (
23
+ get_connection,
24
+ get_symbol_by_name,
25
+ get_transitive_deps,
26
+ get_reverse_deps,
27
+ fts_search,
28
+ get_stats,
29
+ )
30
+
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # Query functions
34
+ # ---------------------------------------------------------------------------
12
35
 
13
36
 
14
37
  def blast_radius(conn, symbol_name: str, max_depth: int = 10) -> dict:
@@ -17,15 +40,34 @@ def blast_radius(conn, symbol_name: str, max_depth: int = 10) -> dict:
17
40
  if not symbols:
18
41
  return {"error": f"Symbol '{symbol_name}' not found", "results": []}
19
42
 
20
- # Use first match
21
43
  sym = symbols[0]
44
+ # Resolve file path from file_id FK
45
+ file_row = conn.execute(
46
+ "SELECT path FROM files WHERE id=?", (sym["file_id"],)
47
+ ).fetchone()
48
+ file_path = file_row["path"] if file_row else "unknown"
49
+
22
50
  deps = get_reverse_deps(conn, sym["id"], max_depth)
23
51
 
52
+ # Enrich each dep with its file path
53
+ enriched = []
54
+ for d in deps:
55
+ dep_file = conn.execute(
56
+ "SELECT path FROM files WHERE id=?", (d["file_id"],)
57
+ ).fetchone()
58
+ enriched.append({
59
+ "id": d["id"],
60
+ "name": d["name"],
61
+ "kind": d["kind"],
62
+ "file_path": dep_file["path"] if dep_file else "unknown",
63
+ "depth": d["depth"],
64
+ })
65
+
24
66
  return {
25
67
  "symbol": symbol_name,
26
- "symbol_file": sym["file_path"],
27
- "affected_count": len(deps),
28
- "results": deps,
68
+ "symbol_file": file_path,
69
+ "affected_count": len(enriched),
70
+ "results": enriched,
29
71
  }
30
72
 
31
73
 
@@ -36,23 +78,58 @@ def dependency_chain(conn, symbol_name: str, max_depth: int = 10) -> dict:
36
78
  return {"error": f"Symbol '{symbol_name}' not found", "results": []}
37
79
 
38
80
  sym = symbols[0]
81
+ file_row = conn.execute(
82
+ "SELECT path FROM files WHERE id=?", (sym["file_id"],)
83
+ ).fetchone()
84
+ file_path = file_row["path"] if file_row else "unknown"
85
+
39
86
  deps = get_transitive_deps(conn, sym["id"], max_depth)
40
87
 
88
+ enriched = []
89
+ for d in deps:
90
+ dep_file = conn.execute(
91
+ "SELECT path FROM files WHERE id=?", (d["file_id"],)
92
+ ).fetchone()
93
+ enriched.append({
94
+ "id": d["id"],
95
+ "name": d["name"],
96
+ "kind": d["kind"],
97
+ "file_path": dep_file["path"] if dep_file else "unknown",
98
+ "depth": d["depth"],
99
+ })
100
+
41
101
  return {
42
102
  "symbol": symbol_name,
43
- "symbol_file": sym["file_path"],
44
- "dependency_count": len(deps),
45
- "results": deps,
103
+ "symbol_file": file_path,
104
+ "dependency_count": len(enriched),
105
+ "results": enriched,
46
106
  }
47
107
 
48
108
 
49
109
  def search(conn, query_text: str, limit: int = 10) -> dict:
50
110
  """BM25-ranked full-text search."""
51
111
  results = fts_search(conn, query_text, limit)
112
+
113
+ # Enrich results with file path from FK
114
+ enriched = []
115
+ for r in results:
116
+ file_row = conn.execute(
117
+ "SELECT path FROM files WHERE id=?", (r["file_id"],)
118
+ ).fetchone()
119
+ enriched.append({
120
+ "id": r["id"],
121
+ "name": r["name"],
122
+ "qualified_name": r.get("qualified_name", ""),
123
+ "kind": r["kind"],
124
+ "file_path": file_row["path"] if file_row else "unknown",
125
+ "line_start": r["line_start"],
126
+ "rank": r["rank"],
127
+ })
128
+
52
129
  return {
53
130
  "query": query_text,
54
- "result_count": len(results),
55
- "results": results,
131
+ "result_count": len(enriched),
132
+ "results": enriched,
56
133
  }
57
134
 
58
135
 
@@ -65,43 +142,93 @@ def symbol_info(conn, symbol_name: str) -> dict:
65
142
  sym = symbols[0]
66
143
  sym_id = sym["id"]
67
144
 
68
- # Direct callers (who calls me)
145
+ # Resolve file path from file_id FK
146
+ file_row = conn.execute(
147
+ "SELECT path FROM files WHERE id=?", (sym["file_id"],)
148
+ ).fetchone()
149
+ file_path = file_row["path"] if file_row else "unknown"
150
+
151
+ # Direct callers (who references me) via symbol_edges
69
152
  callers = conn.execute(
70
153
  """
71
- SELECT s.name, s.kind, s.file_path, s.start_line
72
- FROM edges e JOIN symbols s ON s.id = e.source_id
73
- WHERE e.target_id = ?
154
+ SELECT s.name, s.kind, f.path AS file_path, s.line_start
155
+ FROM symbol_edges se
156
+ JOIN symbols s ON s.id = se.source_symbol_id
157
+ JOIN files f ON f.id = s.file_id
158
+ WHERE se.target_symbol_id = ?
74
159
  """,
75
160
  (sym_id,),
76
161
  ).fetchall()
77
162
 
78
- # Direct callees (who do I call)
163
+ # Direct callees (who I reference) via symbol_edges
79
164
  callees = conn.execute(
80
165
  """
81
- SELECT s.name, s.kind, s.file_path, s.start_line
82
- FROM edges e JOIN symbols s ON s.id = e.target_id
83
- WHERE e.source_id = ?
166
+ SELECT s.name, s.kind, f.path AS file_path, s.line_start
167
+ FROM symbol_edges se
168
+ JOIN symbols s ON s.id = se.target_symbol_id
169
+ JOIN files f ON f.id = s.file_id
170
+ WHERE se.source_symbol_id = ?
84
171
  """,
85
172
  (sym_id,),
86
173
  ).fetchall()
87
174
 
175
+ # Reference count from refs table
176
+ ref_count = conn.execute(
177
+ "SELECT COUNT(*) FROM refs WHERE symbol_name=?", (sym["name"],)
178
+ ).fetchone()[0]
179
+
88
180
  # Blast radius count
89
181
  blast = get_reverse_deps(conn, sym_id)
90
182
 
91
183
  return {
92
184
  "name": sym["name"],
185
+ "qualified_name": sym.get("qualified_name", ""),
93
186
  "kind": sym["kind"],
94
- "file": sym["file_path"],
95
- "start_line": sym["start_line"],
96
- "end_line": sym["end_line"],
187
+ "file": file_path,
188
+ "line_start": sym["line_start"],
189
+ "line_end": sym.get("line_end"),
97
190
  "signature": sym.get("signature", ""),
98
- "doc_comment": sym.get("doc_comment", ""),
99
191
  "callers": [dict(r) for r in callers],
100
192
  "callees": [dict(r) for r in callees],
193
+ "reference_count": ref_count,
101
194
  "blast_radius_count": len(blast),
102
195
  }
103
196
 
104
197
 
198
+ def context(conn, seed_files=None, seed_keywords=None, seed_symbols=None, token_budget=8000):
199
+ """PageRank-based context selection with personalization.
200
+
201
+ Uses the ranker module to score files by structural importance,
202
+ optionally biased toward seed files/keywords/symbols. When a token
203
+ budget is provided, fits the highest-ranked files into that budget.
204
+ """
205
+ from ranker import rank_files, fit_to_budget
206
+
207
+ ranked = rank_files(conn, seed_files, seed_keywords, seed_symbols)
208
+ if not ranked:
209
+ return {"error": "No files in index or no edges to rank", "files": []}
210
+
211
+ if token_budget:
212
+ files, total_tokens = fit_to_budget(ranked, conn, token_budget)
213
+ return {"files": files, "total_tokens": total_tokens}
214
+ else:
215
+ # Return all files with scores, no budget constraint
216
+ return {
217
+ "files": [{"path": p, "score": round(s, 6)} for p, s in ranked],
218
+ "total_tokens": None,
219
+ }
220
+
221
+
222
+ def stats(conn):
223
+ """Show database statistics."""
224
+ return get_stats(conn)
225
+
226
+
227
+ # ---------------------------------------------------------------------------
228
+ # CLI
229
+ # ---------------------------------------------------------------------------
230
+
231
+
105
232
  def main():
106
233
  parser = argparse.ArgumentParser(description="ftm-map query interface")
107
234
  parser.add_argument(
@@ -112,6 +239,24 @@ def main():
112
239
  )
113
240
  parser.add_argument("--search", metavar="QUERY", help="Full-text search")
114
241
  parser.add_argument("--info", metavar="SYMBOL", help="Full symbol info")
242
+ parser.add_argument(
243
+ "--context", action="store_true", help="PageRank context selection"
244
+ )
245
+ parser.add_argument(
246
+ "--seed-files", nargs="*", help="Seed files for context personalization"
247
+ )
248
+ parser.add_argument(
249
+ "--seed-keywords", nargs="*", help="Seed keywords for context personalization"
250
+ )
251
+ parser.add_argument(
252
+ "--seed-symbols", nargs="*", help="Seed symbols for context personalization"
253
+ )
254
+ parser.add_argument(
255
+ "--token-budget", type=int, default=8000, help="Token budget for context output"
256
+ )
257
+ parser.add_argument(
258
+ "--stats", action="store_true", help="Show database statistics"
259
+ )
115
260
  parser.add_argument(
116
261
  "--limit", type=int, default=10, help="Result limit for search"
117
262
  )
@@ -128,7 +273,14 @@ def main():
128
273
 
129
274
  conn = get_connection(args.project_root)
130
275
  try:
131
- if args.blast_radius:
276
+ if args.context:
277
+ result = context(
278
+ conn, args.seed_files, args.seed_keywords,
279
+ args.seed_symbols, args.token_budget,
280
+ )
281
+ elif args.stats:
282
+ result = stats(conn)
283
+ elif args.blast_radius:
132
284
  result = blast_radius(conn, args.blast_radius, args.max_depth)
133
285
  elif args.deps:
134
286
  result = dependency_chain(conn, args.deps, args.max_depth)