feed-the-machine 1.6.1 → 1.7.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 (269) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +170 -170
  3. package/bin/brain.py +1340 -0
  4. package/bin/convert_claude_skills_to_codex.py +490 -0
  5. package/bin/generate-manifest.mjs +463 -463
  6. package/bin/harden_codex_skills.py +141 -0
  7. package/bin/install.mjs +491 -491
  8. package/bin/migrate-eng-buddy-data.py +875 -0
  9. package/bin/playbook_engine/__init__.py +1 -0
  10. package/bin/playbook_engine/conftest.py +8 -0
  11. package/bin/playbook_engine/extractor.py +33 -0
  12. package/bin/playbook_engine/manager.py +102 -0
  13. package/bin/playbook_engine/models.py +84 -0
  14. package/bin/playbook_engine/registry.py +35 -0
  15. package/bin/playbook_engine/test_extractor.py +72 -0
  16. package/bin/playbook_engine/test_integration.py +129 -0
  17. package/bin/playbook_engine/test_manager.py +85 -0
  18. package/bin/playbook_engine/test_models.py +166 -0
  19. package/bin/playbook_engine/test_registry.py +67 -0
  20. package/bin/playbook_engine/test_tracer.py +86 -0
  21. package/bin/playbook_engine/tracer.py +93 -0
  22. package/bin/tasks_db.py +456 -0
  23. package/docs/HOOKS.md +243 -243
  24. package/docs/INBOX.md +233 -233
  25. package/ftm/SKILL.md +125 -122
  26. package/ftm-audit/SKILL.md +623 -623
  27. package/ftm-audit/references/protocols/PROJECT-PATTERNS.md +91 -91
  28. package/ftm-audit/references/protocols/RUNTIME-WIRING.md +66 -66
  29. package/ftm-audit/references/protocols/WIRING-CONTRACTS.md +135 -135
  30. package/ftm-audit/references/strategies/AUTO-FIX-STRATEGIES.md +69 -69
  31. package/ftm-audit/references/templates/REPORT-FORMAT.md +96 -96
  32. package/ftm-audit/scripts/run-knip.sh +23 -23
  33. package/ftm-audit.yml +2 -2
  34. package/ftm-brainstorm/SKILL.md +1003 -498
  35. package/ftm-brainstorm/evals/evals.json +180 -100
  36. package/ftm-brainstorm/evals/promptfoo.yaml +109 -109
  37. package/ftm-brainstorm/references/agent-prompts.md +552 -224
  38. package/ftm-brainstorm/references/plan-template.md +209 -121
  39. package/ftm-brainstorm.yml +2 -2
  40. package/ftm-browse/SKILL.md +454 -454
  41. package/ftm-browse/daemon/browser-manager.ts +206 -206
  42. package/ftm-browse/daemon/bun.lock +30 -30
  43. package/ftm-browse/daemon/cli.ts +347 -347
  44. package/ftm-browse/daemon/commands.ts +410 -410
  45. package/ftm-browse/daemon/main.ts +357 -357
  46. package/ftm-browse/daemon/package.json +17 -17
  47. package/ftm-browse/daemon/server.ts +189 -189
  48. package/ftm-browse/daemon/snapshot.ts +519 -519
  49. package/ftm-browse/daemon/tsconfig.json +22 -22
  50. package/ftm-browse.yml +4 -4
  51. package/ftm-capture/SKILL.md +370 -370
  52. package/ftm-capture.yml +4 -4
  53. package/ftm-codex-gate/SKILL.md +361 -361
  54. package/ftm-codex-gate.yml +2 -2
  55. package/ftm-config/SKILL.md +422 -345
  56. package/ftm-config.default.yml +125 -82
  57. package/ftm-config.yml +44 -2
  58. package/ftm-council/SKILL.md +416 -416
  59. package/ftm-council/references/prompts/CLAUDE-INVESTIGATION.md +60 -60
  60. package/ftm-council/references/prompts/CODEX-INVESTIGATION.md +58 -58
  61. package/ftm-council/references/prompts/GEMINI-INVESTIGATION.md +58 -58
  62. package/ftm-council/references/prompts/REBUTTAL-TEMPLATE.md +57 -57
  63. package/ftm-council/references/protocols/PREREQUISITES.md +47 -47
  64. package/ftm-council/references/protocols/STEP-0-FRAMING.md +46 -46
  65. package/ftm-council.yml +2 -2
  66. package/ftm-dashboard/SKILL.md +163 -163
  67. package/ftm-dashboard.yml +4 -4
  68. package/ftm-debug/SKILL.md +1037 -1037
  69. package/ftm-debug/references/phases/PHASE-0-INTAKE.md +58 -58
  70. package/ftm-debug/references/phases/PHASE-1-TRIAGE.md +46 -46
  71. package/ftm-debug/references/phases/PHASE-2-WAR-ROOM-AGENTS.md +279 -279
  72. package/ftm-debug/references/phases/PHASE-3-TO-6-EXECUTION.md +436 -436
  73. package/ftm-debug/references/protocols/BLACKBOARD.md +86 -86
  74. package/ftm-debug/references/protocols/EDGE-CASES.md +103 -103
  75. package/ftm-debug.yml +2 -2
  76. package/ftm-diagram/SKILL.md +277 -277
  77. package/ftm-diagram.yml +2 -2
  78. package/ftm-executor/SKILL.md +777 -777
  79. package/ftm-executor/references/STYLE-TEMPLATE.md +73 -73
  80. package/ftm-executor/references/phases/PHASE-0-VERIFICATION.md +62 -62
  81. package/ftm-executor/references/phases/PHASE-2-AGENT-ASSEMBLY.md +34 -34
  82. package/ftm-executor/references/phases/PHASE-3-WORKTREES.md +38 -38
  83. package/ftm-executor/references/phases/PHASE-4-5-AUDIT.md +72 -72
  84. package/ftm-executor/references/phases/PHASE-4-DISPATCH.md +66 -66
  85. package/ftm-executor/references/phases/PHASE-5-5-CODEX-GATE.md +73 -73
  86. package/ftm-executor/references/protocols/DOCUMENTATION-BOOTSTRAP.md +36 -36
  87. package/ftm-executor/references/protocols/MODEL-PROFILE.md +59 -59
  88. package/ftm-executor/references/protocols/PROGRESS-TRACKING.md +66 -66
  89. package/ftm-executor/runtime/ftm-runtime.mjs +252 -252
  90. package/ftm-executor/runtime/package.json +8 -8
  91. package/ftm-executor.yml +2 -2
  92. package/ftm-git/SKILL.md +441 -441
  93. package/ftm-git/evals/evals.json +26 -26
  94. package/ftm-git/evals/promptfoo.yaml +75 -75
  95. package/ftm-git/hooks/post-commit-experience.sh +92 -92
  96. package/ftm-git/references/patterns/SECRET-PATTERNS.md +104 -104
  97. package/ftm-git/references/protocols/REMEDIATION.md +139 -139
  98. package/ftm-git/scripts/pre-commit-secrets.sh +110 -110
  99. package/ftm-git.yml +2 -2
  100. package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
  101. package/ftm-inbox/backend/adapters/_retry.py +64 -64
  102. package/ftm-inbox/backend/adapters/base.py +230 -230
  103. package/ftm-inbox/backend/adapters/freshservice.py +104 -104
  104. package/ftm-inbox/backend/adapters/gmail.py +125 -125
  105. package/ftm-inbox/backend/adapters/jira.py +136 -136
  106. package/ftm-inbox/backend/adapters/registry.py +192 -192
  107. package/ftm-inbox/backend/adapters/slack.py +110 -110
  108. package/ftm-inbox/backend/db/connection.py +54 -54
  109. package/ftm-inbox/backend/db/schema.py +78 -78
  110. package/ftm-inbox/backend/executor/__init__.py +7 -7
  111. package/ftm-inbox/backend/executor/engine.py +149 -149
  112. package/ftm-inbox/backend/executor/step_runner.py +98 -98
  113. package/ftm-inbox/backend/main.py +103 -103
  114. package/ftm-inbox/backend/models/__init__.py +1 -1
  115. package/ftm-inbox/backend/models/unified_task.py +36 -36
  116. package/ftm-inbox/backend/planner/__init__.py +6 -6
  117. package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
  118. package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
  119. package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
  120. package/ftm-inbox/backend/planner/generator.py +127 -127
  121. package/ftm-inbox/backend/planner/schema.py +34 -34
  122. package/ftm-inbox/backend/requirements.txt +5 -5
  123. package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
  124. package/ftm-inbox/backend/routes/execute.py +186 -186
  125. package/ftm-inbox/backend/routes/health.py +52 -52
  126. package/ftm-inbox/backend/routes/inbox.py +68 -68
  127. package/ftm-inbox/backend/routes/plan.py +271 -271
  128. package/ftm-inbox/bin/launchagent.mjs +91 -91
  129. package/ftm-inbox/bin/setup.mjs +188 -188
  130. package/ftm-inbox/bin/start.sh +10 -10
  131. package/ftm-inbox/bin/status.sh +17 -17
  132. package/ftm-inbox/bin/stop.sh +8 -8
  133. package/ftm-inbox/config.example.yml +55 -55
  134. package/ftm-inbox/package-lock.json +2898 -2898
  135. package/ftm-inbox/package.json +26 -26
  136. package/ftm-inbox/postcss.config.js +6 -6
  137. package/ftm-inbox/src/app.css +199 -199
  138. package/ftm-inbox/src/app.html +18 -18
  139. package/ftm-inbox/src/lib/api.ts +166 -166
  140. package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -81
  141. package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -143
  142. package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -271
  143. package/ftm-inbox/src/lib/components/PlanView.svelte +206 -206
  144. package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -99
  145. package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -190
  146. package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -63
  147. package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -86
  148. package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -106
  149. package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -67
  150. package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -149
  151. package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -80
  152. package/ftm-inbox/src/lib/theme.ts +47 -47
  153. package/ftm-inbox/src/routes/+layout.svelte +76 -76
  154. package/ftm-inbox/src/routes/+page.svelte +401 -401
  155. package/ftm-inbox/svelte.config.js +12 -12
  156. package/ftm-inbox/tailwind.config.ts +63 -63
  157. package/ftm-inbox/tsconfig.json +13 -13
  158. package/ftm-inbox/vite.config.ts +6 -6
  159. package/ftm-intent/SKILL.md +241 -241
  160. package/ftm-intent.yml +2 -2
  161. package/ftm-manifest.json +3794 -3794
  162. package/ftm-map/SKILL.md +291 -291
  163. package/ftm-map/scripts/db.py +712 -712
  164. package/ftm-map/scripts/index.py +415 -415
  165. package/ftm-map/scripts/parser.py +224 -224
  166. package/ftm-map/scripts/queries/go-tags.scm +20 -20
  167. package/ftm-map/scripts/queries/javascript-tags.scm +35 -35
  168. package/ftm-map/scripts/queries/python-tags.scm +31 -31
  169. package/ftm-map/scripts/queries/ruby-tags.scm +19 -19
  170. package/ftm-map/scripts/queries/rust-tags.scm +37 -37
  171. package/ftm-map/scripts/queries/typescript-tags.scm +41 -41
  172. package/ftm-map/scripts/query.py +301 -301
  173. package/ftm-map/scripts/ranker.py +377 -377
  174. package/ftm-map/scripts/requirements.txt +5 -5
  175. package/ftm-map/scripts/setup-hooks.sh +27 -27
  176. package/ftm-map/scripts/setup.sh +56 -56
  177. package/ftm-map/scripts/test_db.py +364 -364
  178. package/ftm-map/scripts/test_parser.py +174 -174
  179. package/ftm-map/scripts/test_query.py +183 -183
  180. package/ftm-map/scripts/test_ranker.py +199 -199
  181. package/ftm-map/scripts/views.py +591 -591
  182. package/ftm-map.yml +2 -2
  183. package/ftm-mind/SKILL.md +201 -1943
  184. package/ftm-mind/evals/promptfoo.yaml +142 -142
  185. package/ftm-mind/references/blackboard-protocol.md +110 -0
  186. package/ftm-mind/references/blackboard-schema.md +328 -328
  187. package/ftm-mind/references/complexity-guide.md +110 -110
  188. package/ftm-mind/references/complexity-sizing.md +138 -0
  189. package/ftm-mind/references/decide-act-protocol.md +172 -0
  190. package/ftm-mind/references/direct-execution.md +51 -0
  191. package/ftm-mind/references/environment-discovery.md +77 -0
  192. package/ftm-mind/references/event-registry.md +319 -319
  193. package/ftm-mind/references/mcp-inventory.md +300 -296
  194. package/ftm-mind/references/ops-routing.md +47 -0
  195. package/ftm-mind/references/orient-protocol.md +234 -0
  196. package/ftm-mind/references/personality.md +40 -0
  197. package/ftm-mind/references/protocols/COMPLEXITY-SIZING.md +72 -72
  198. package/ftm-mind/references/protocols/MCP-HEURISTICS.md +32 -32
  199. package/ftm-mind/references/protocols/PLAN-APPROVAL.md +80 -80
  200. package/ftm-mind/references/reflexion-protocol.md +249 -249
  201. package/ftm-mind/references/routing/SCENARIOS.md +22 -22
  202. package/ftm-mind/references/routing-scenarios.md +35 -35
  203. package/ftm-mind.yml +2 -2
  204. package/ftm-ops.yml +4 -0
  205. package/ftm-pause/SKILL.md +395 -395
  206. package/ftm-pause/references/protocols/SKILL-RESTORE-PROTOCOLS.md +186 -186
  207. package/ftm-pause/references/protocols/VALIDATION.md +80 -80
  208. package/ftm-pause.yml +2 -2
  209. package/ftm-researcher/SKILL.md +275 -275
  210. package/ftm-researcher/evals/agent-diversity.yaml +17 -17
  211. package/ftm-researcher/evals/synthesis-quality.yaml +12 -12
  212. package/ftm-researcher/evals/trigger-accuracy.yaml +39 -39
  213. package/ftm-researcher/references/adaptive-search.md +116 -116
  214. package/ftm-researcher/references/agent-prompts.md +193 -193
  215. package/ftm-researcher/references/council-integration.md +193 -193
  216. package/ftm-researcher/references/output-format.md +203 -203
  217. package/ftm-researcher/references/synthesis-pipeline.md +165 -165
  218. package/ftm-researcher/scripts/score_credibility.py +234 -234
  219. package/ftm-researcher/scripts/validate_research.py +92 -92
  220. package/ftm-researcher.yml +2 -2
  221. package/ftm-resume/SKILL.md +518 -518
  222. package/ftm-resume/references/protocols/VALIDATION.md +172 -172
  223. package/ftm-resume.yml +2 -2
  224. package/ftm-retro/SKILL.md +380 -380
  225. package/ftm-retro/references/protocols/SCORING-RUBRICS.md +89 -89
  226. package/ftm-retro/references/templates/REPORT-FORMAT.md +109 -109
  227. package/ftm-retro.yml +2 -2
  228. package/ftm-routine/SKILL.md +170 -170
  229. package/ftm-routine.yml +4 -4
  230. package/ftm-state/blackboard/capabilities.json +5 -5
  231. package/ftm-state/blackboard/capabilities.schema.json +27 -27
  232. package/ftm-state/blackboard/context.json +37 -23
  233. package/ftm-state/blackboard/experiences/doom-statusline-fix.json +26 -0
  234. package/ftm-state/blackboard/experiences/hackathon-pages-site.json +26 -0
  235. package/ftm-state/blackboard/experiences/hindsight-sso-kickoff.json +42 -0
  236. package/ftm-state/blackboard/experiences/index.json +58 -9
  237. package/ftm-state/blackboard/experiences/learning-ragnarok-api-access.json +23 -0
  238. package/ftm-state/blackboard/experiences/nordlayer-members-auto-assign.json +26 -0
  239. package/ftm-state/blackboard/experiences/saml2aws-stale-session-fix.json +41 -0
  240. package/ftm-state/blackboard/patterns.json +6 -6
  241. package/ftm-state/schemas/context.schema.json +130 -130
  242. package/ftm-state/schemas/experience-index.schema.json +77 -77
  243. package/ftm-state/schemas/experience.schema.json +78 -78
  244. package/ftm-state/schemas/patterns.schema.json +44 -44
  245. package/ftm-upgrade/SKILL.md +194 -194
  246. package/ftm-upgrade/scripts/check-version.sh +76 -76
  247. package/ftm-upgrade/scripts/upgrade.sh +143 -143
  248. package/ftm-upgrade.yml +2 -2
  249. package/ftm-verify.yml +2 -2
  250. package/ftm.yml +2 -2
  251. package/hooks/ftm-auto-log.sh +137 -0
  252. package/hooks/ftm-blackboard-enforcer.sh +93 -93
  253. package/hooks/ftm-discovery-reminder.sh +90 -90
  254. package/hooks/ftm-drafts-gate.sh +61 -61
  255. package/hooks/ftm-event-logger.mjs +107 -107
  256. package/hooks/ftm-install-hooks.sh +240 -0
  257. package/hooks/ftm-learning-capture.sh +117 -0
  258. package/hooks/ftm-map-autodetect.sh +79 -79
  259. package/hooks/ftm-pending-sync-check.sh +22 -22
  260. package/hooks/ftm-plan-gate.sh +92 -92
  261. package/hooks/ftm-post-commit-trigger.sh +57 -57
  262. package/hooks/ftm-post-compaction.sh +138 -0
  263. package/hooks/ftm-pre-compaction.sh +147 -0
  264. package/hooks/ftm-session-end.sh +52 -0
  265. package/hooks/ftm-session-snapshot.sh +213 -0
  266. package/hooks/settings-template.json +81 -81
  267. package/install.sh +363 -363
  268. package/package.json +84 -84
  269. package/uninstall.sh +25 -25
@@ -1,301 +1,301 @@
1
- #!/usr/bin/env python3
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
- """
14
-
15
- import argparse
16
- import json
17
- import os
18
- import sys
19
-
20
- sys.path.insert(0, os.path.dirname(__file__))
21
-
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
- # ---------------------------------------------------------------------------
35
-
36
-
37
- def blast_radius(conn, symbol_name: str, max_depth: int = 10) -> dict:
38
- """Get all symbols that would be affected if this symbol changes."""
39
- symbols = get_symbol_by_name(conn, symbol_name)
40
- if not symbols:
41
- return {"error": f"Symbol '{symbol_name}' not found", "results": []}
42
-
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
-
50
- deps = get_reverse_deps(conn, sym["id"], max_depth)
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
-
66
- return {
67
- "symbol": symbol_name,
68
- "symbol_file": file_path,
69
- "affected_count": len(enriched),
70
- "results": enriched,
71
- }
72
-
73
-
74
- def dependency_chain(conn, symbol_name: str, max_depth: int = 10) -> dict:
75
- """Get all symbols this one depends on."""
76
- symbols = get_symbol_by_name(conn, symbol_name)
77
- if not symbols:
78
- return {"error": f"Symbol '{symbol_name}' not found", "results": []}
79
-
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
-
86
- deps = get_transitive_deps(conn, sym["id"], max_depth)
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
-
101
- return {
102
- "symbol": symbol_name,
103
- "symbol_file": file_path,
104
- "dependency_count": len(enriched),
105
- "results": enriched,
106
- }
107
-
108
-
109
- def search(conn, query_text: str, limit: int = 10) -> dict:
110
- """BM25-ranked full-text search."""
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
-
129
- return {
130
- "query": query_text,
131
- "result_count": len(enriched),
132
- "results": enriched,
133
- }
134
-
135
-
136
- def symbol_info(conn, symbol_name: str) -> dict:
137
- """Full details about a symbol including callers, callees, and blast radius count."""
138
- symbols = get_symbol_by_name(conn, symbol_name)
139
- if not symbols:
140
- return {"error": f"Symbol '{symbol_name}' not found"}
141
-
142
- sym = symbols[0]
143
- sym_id = sym["id"]
144
-
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
152
- callers = conn.execute(
153
- """
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 = ?
159
- """,
160
- (sym_id,),
161
- ).fetchall()
162
-
163
- # Direct callees (who I reference) via symbol_edges
164
- callees = conn.execute(
165
- """
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 = ?
171
- """,
172
- (sym_id,),
173
- ).fetchall()
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
-
180
- # Blast radius count
181
- blast = get_reverse_deps(conn, sym_id)
182
-
183
- return {
184
- "name": sym["name"],
185
- "qualified_name": sym.get("qualified_name", ""),
186
- "kind": sym["kind"],
187
- "file": file_path,
188
- "line_start": sym["line_start"],
189
- "line_end": sym.get("line_end"),
190
- "signature": sym.get("signature", ""),
191
- "callers": [dict(r) for r in callers],
192
- "callees": [dict(r) for r in callees],
193
- "reference_count": ref_count,
194
- "blast_radius_count": len(blast),
195
- }
196
-
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
-
232
- def main():
233
- parser = argparse.ArgumentParser(description="ftm-map query interface")
234
- parser.add_argument(
235
- "--blast-radius", metavar="SYMBOL", help="Show blast radius for a symbol"
236
- )
237
- parser.add_argument(
238
- "--deps", metavar="SYMBOL", help="Show dependency chain for a symbol"
239
- )
240
- parser.add_argument("--search", metavar="QUERY", help="Full-text search")
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
- )
260
- parser.add_argument(
261
- "--limit", type=int, default=10, help="Result limit for search"
262
- )
263
- parser.add_argument(
264
- "--max-depth", type=int, default=10, help="Max traversal depth"
265
- )
266
- parser.add_argument(
267
- "--project-root",
268
- default=os.getcwd(),
269
- help="Project root directory",
270
- )
271
-
272
- args = parser.parse_args()
273
-
274
- conn = get_connection(args.project_root)
275
- try:
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:
284
- result = blast_radius(conn, args.blast_radius, args.max_depth)
285
- elif args.deps:
286
- result = dependency_chain(conn, args.deps, args.max_depth)
287
- elif args.search:
288
- result = search(conn, args.search, args.limit)
289
- elif args.info:
290
- result = symbol_info(conn, args.info)
291
- else:
292
- parser.print_help()
293
- sys.exit(1)
294
-
295
- print(json.dumps(result, indent=2, default=str))
296
- finally:
297
- conn.close()
298
-
299
-
300
- if __name__ == "__main__":
301
- main()
1
+ #!/usr/bin/env python3
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
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+ import sys
19
+
20
+ sys.path.insert(0, os.path.dirname(__file__))
21
+
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
+ # ---------------------------------------------------------------------------
35
+
36
+
37
+ def blast_radius(conn, symbol_name: str, max_depth: int = 10) -> dict:
38
+ """Get all symbols that would be affected if this symbol changes."""
39
+ symbols = get_symbol_by_name(conn, symbol_name)
40
+ if not symbols:
41
+ return {"error": f"Symbol '{symbol_name}' not found", "results": []}
42
+
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
+
50
+ deps = get_reverse_deps(conn, sym["id"], max_depth)
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
+
66
+ return {
67
+ "symbol": symbol_name,
68
+ "symbol_file": file_path,
69
+ "affected_count": len(enriched),
70
+ "results": enriched,
71
+ }
72
+
73
+
74
+ def dependency_chain(conn, symbol_name: str, max_depth: int = 10) -> dict:
75
+ """Get all symbols this one depends on."""
76
+ symbols = get_symbol_by_name(conn, symbol_name)
77
+ if not symbols:
78
+ return {"error": f"Symbol '{symbol_name}' not found", "results": []}
79
+
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
+
86
+ deps = get_transitive_deps(conn, sym["id"], max_depth)
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
+
101
+ return {
102
+ "symbol": symbol_name,
103
+ "symbol_file": file_path,
104
+ "dependency_count": len(enriched),
105
+ "results": enriched,
106
+ }
107
+
108
+
109
+ def search(conn, query_text: str, limit: int = 10) -> dict:
110
+ """BM25-ranked full-text search."""
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
+
129
+ return {
130
+ "query": query_text,
131
+ "result_count": len(enriched),
132
+ "results": enriched,
133
+ }
134
+
135
+
136
+ def symbol_info(conn, symbol_name: str) -> dict:
137
+ """Full details about a symbol including callers, callees, and blast radius count."""
138
+ symbols = get_symbol_by_name(conn, symbol_name)
139
+ if not symbols:
140
+ return {"error": f"Symbol '{symbol_name}' not found"}
141
+
142
+ sym = symbols[0]
143
+ sym_id = sym["id"]
144
+
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
152
+ callers = conn.execute(
153
+ """
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 = ?
159
+ """,
160
+ (sym_id,),
161
+ ).fetchall()
162
+
163
+ # Direct callees (who I reference) via symbol_edges
164
+ callees = conn.execute(
165
+ """
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 = ?
171
+ """,
172
+ (sym_id,),
173
+ ).fetchall()
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
+
180
+ # Blast radius count
181
+ blast = get_reverse_deps(conn, sym_id)
182
+
183
+ return {
184
+ "name": sym["name"],
185
+ "qualified_name": sym.get("qualified_name", ""),
186
+ "kind": sym["kind"],
187
+ "file": file_path,
188
+ "line_start": sym["line_start"],
189
+ "line_end": sym.get("line_end"),
190
+ "signature": sym.get("signature", ""),
191
+ "callers": [dict(r) for r in callers],
192
+ "callees": [dict(r) for r in callees],
193
+ "reference_count": ref_count,
194
+ "blast_radius_count": len(blast),
195
+ }
196
+
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
+
232
+ def main():
233
+ parser = argparse.ArgumentParser(description="ftm-map query interface")
234
+ parser.add_argument(
235
+ "--blast-radius", metavar="SYMBOL", help="Show blast radius for a symbol"
236
+ )
237
+ parser.add_argument(
238
+ "--deps", metavar="SYMBOL", help="Show dependency chain for a symbol"
239
+ )
240
+ parser.add_argument("--search", metavar="QUERY", help="Full-text search")
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
+ )
260
+ parser.add_argument(
261
+ "--limit", type=int, default=10, help="Result limit for search"
262
+ )
263
+ parser.add_argument(
264
+ "--max-depth", type=int, default=10, help="Max traversal depth"
265
+ )
266
+ parser.add_argument(
267
+ "--project-root",
268
+ default=os.getcwd(),
269
+ help="Project root directory",
270
+ )
271
+
272
+ args = parser.parse_args()
273
+
274
+ conn = get_connection(args.project_root)
275
+ try:
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:
284
+ result = blast_radius(conn, args.blast_radius, args.max_depth)
285
+ elif args.deps:
286
+ result = dependency_chain(conn, args.deps, args.max_depth)
287
+ elif args.search:
288
+ result = search(conn, args.search, args.limit)
289
+ elif args.info:
290
+ result = symbol_info(conn, args.info)
291
+ else:
292
+ parser.print_help()
293
+ sys.exit(1)
294
+
295
+ print(json.dumps(result, indent=2, default=str))
296
+ finally:
297
+ conn.close()
298
+
299
+
300
+ if __name__ == "__main__":
301
+ main()