rubik 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (523) hide show
  1. checksums.yaml +7 -0
  2. data/ATTRIBUTION.md +139 -0
  3. data/CHANGELOG.md +236 -0
  4. data/LICENSE +21 -0
  5. data/README.md +269 -0
  6. data/bin/rubik +82 -0
  7. data/lib/rubik/adapter/base.rb +195 -0
  8. data/lib/rubik/adapter/c.rb +36 -0
  9. data/lib/rubik/adapter/crystal.rb +78 -0
  10. data/lib/rubik/adapter/generic.rb +86 -0
  11. data/lib/rubik/adapter/go.rb +36 -0
  12. data/lib/rubik/adapter/node.rb +146 -0
  13. data/lib/rubik/adapter/process.rb +78 -0
  14. data/lib/rubik/adapter/python.rb +30 -0
  15. data/lib/rubik/adapter/registry.rb +81 -0
  16. data/lib/rubik/adapter/ruby.rb +30 -0
  17. data/lib/rubik/adapter/rust.rb +36 -0
  18. data/lib/rubik/adapter/zig.rb +36 -0
  19. data/lib/rubik/adapter.rb +18 -0
  20. data/lib/rubik/ast/body_loader.rb +19 -0
  21. data/lib/rubik/ast/c_extractor.rb +114 -0
  22. data/lib/rubik/ast/cache_store.rb +191 -0
  23. data/lib/rubik/ast/extractor.rb +59 -0
  24. data/lib/rubik/ast/fallback_cleaner.rb +145 -0
  25. data/lib/rubik/ast/go_extractor.rb +148 -0
  26. data/lib/rubik/ast/graph.rb +272 -0
  27. data/lib/rubik/ast/python_extractor.rb +120 -0
  28. data/lib/rubik/ast/ruby_extractor.rb +260 -0
  29. data/lib/rubik/ast/rust_extractor.rb +127 -0
  30. data/lib/rubik/ast/symbol.rb +103 -0
  31. data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
  32. data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
  33. data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
  34. data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
  35. data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
  36. data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
  37. data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
  38. data/lib/rubik/ast/treesitter_extractor.rb +93 -0
  39. data/lib/rubik/ast/typescript_extractor.rb +283 -0
  40. data/lib/rubik/ast/zig_extractor.rb +116 -0
  41. data/lib/rubik/ast.rb +13 -0
  42. data/lib/rubik/benchmark/runner.rb +224 -0
  43. data/lib/rubik/benchmark/synth_repo.rb +59 -0
  44. data/lib/rubik/benchmark.rb +9 -0
  45. data/lib/rubik/buffer/diff.rb +43 -0
  46. data/lib/rubik/buffer/manager.rb +427 -0
  47. data/lib/rubik/buffer/path_validator.rb +60 -0
  48. data/lib/rubik/buffer/snapshot.rb +99 -0
  49. data/lib/rubik/buffer.rb +10 -0
  50. data/lib/rubik/coeffects.rb +129 -0
  51. data/lib/rubik/concurrency/boundary.rb +74 -0
  52. data/lib/rubik/concurrency/dns_pool.rb +68 -0
  53. data/lib/rubik/concurrency/errors.rb +17 -0
  54. data/lib/rubik/concurrency/event_loop.rb +441 -0
  55. data/lib/rubik/concurrency/join.rb +55 -0
  56. data/lib/rubik/concurrency/process_waiter.rb +55 -0
  57. data/lib/rubik/concurrency/scheduler.rb +84 -0
  58. data/lib/rubik/concurrency/timers.rb +109 -0
  59. data/lib/rubik/concurrency.rb +132 -0
  60. data/lib/rubik/config/deep_ops.rb +80 -0
  61. data/lib/rubik/config/defaults.rb +248 -0
  62. data/lib/rubik/config/model_catalog.rb +93 -0
  63. data/lib/rubik/config/store.rb +155 -0
  64. data/lib/rubik/config.rb +216 -0
  65. data/lib/rubik/context/git_info.rb +84 -0
  66. data/lib/rubik/context/git_lister.rb +37 -0
  67. data/lib/rubik/context/index.rb +134 -0
  68. data/lib/rubik/context/repo_context_reader.rb +121 -0
  69. data/lib/rubik/context/scanner.rb +439 -0
  70. data/lib/rubik/context.rb +11 -0
  71. data/lib/rubik/diagnostics/collector.rb +200 -0
  72. data/lib/rubik/diagnostics/entry.rb +60 -0
  73. data/lib/rubik/diagnostics/parser.rb +107 -0
  74. data/lib/rubik/diagnostics.rb +10 -0
  75. data/lib/rubik/event_bus.rb +56 -0
  76. data/lib/rubik/kernel/dispatcher.rb +171 -0
  77. data/lib/rubik/kernel/dmesg.rb +116 -0
  78. data/lib/rubik/kernel/init.rb +177 -0
  79. data/lib/rubik/kernel/record.rb +85 -0
  80. data/lib/rubik/kernel/unit.rb +160 -0
  81. data/lib/rubik/llm/anthropic.rb +202 -0
  82. data/lib/rubik/llm/chunk_assembler.rb +285 -0
  83. data/lib/rubik/llm/client.rb +526 -0
  84. data/lib/rubik/llm/formatter.rb +450 -0
  85. data/lib/rubik/llm/message_sanitizer.rb +87 -0
  86. data/lib/rubik/llm/ollama.rb +167 -0
  87. data/lib/rubik/llm/openai.rb +21 -0
  88. data/lib/rubik/llm/openai_compatible.rb +216 -0
  89. data/lib/rubik/llm/provider.rb +125 -0
  90. data/lib/rubik/llm/registry.rb +336 -0
  91. data/lib/rubik/llm/router.rb +100 -0
  92. data/lib/rubik/llm.rb +18 -0
  93. data/lib/rubik/log.rb +38 -0
  94. data/lib/rubik/lsp/client.rb +475 -0
  95. data/lib/rubik/lsp/manager.rb +366 -0
  96. data/lib/rubik/lsp.rb +9 -0
  97. data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
  98. data/lib/rubik/mcp/client.rb +169 -0
  99. data/lib/rubik/mcp/errors.rb +11 -0
  100. data/lib/rubik/mcp/manager.rb +619 -0
  101. data/lib/rubik/mcp/proxy_tool.rb +104 -0
  102. data/lib/rubik/mcp/transport/stdio.rb +255 -0
  103. data/lib/rubik/mcp.rb +12 -0
  104. data/lib/rubik/orchestrator/activity_log.rb +75 -0
  105. data/lib/rubik/orchestrator/auto_apply.rb +166 -0
  106. data/lib/rubik/orchestrator/budget_manager.rb +97 -0
  107. data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
  108. data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
  109. data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
  110. data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
  111. data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
  112. data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
  113. data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
  114. data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
  115. data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
  116. data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
  117. data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
  118. data/lib/rubik/orchestrator/goal_state.rb +184 -0
  119. data/lib/rubik/orchestrator/goal_wait.rb +125 -0
  120. data/lib/rubik/orchestrator/loop.rb +388 -0
  121. data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
  122. data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
  123. data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
  124. data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
  125. data/lib/rubik/orchestrator/shell_deps.rb +85 -0
  126. data/lib/rubik/orchestrator/step_executor.rb +328 -0
  127. data/lib/rubik/orchestrator/stream_manager.rb +118 -0
  128. data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
  129. data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
  130. data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
  131. data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
  132. data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
  133. data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
  134. data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
  135. data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
  136. data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
  137. data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
  138. data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
  139. data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
  140. data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
  141. data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
  142. data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
  143. data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
  144. data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
  145. data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
  146. data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
  147. data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
  148. data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
  149. data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
  150. data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
  151. data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
  152. data/lib/rubik/orchestrator/tool_history.rb +39 -0
  153. data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
  154. data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
  155. data/lib/rubik/orchestrator/turn_executor.rb +376 -0
  156. data/lib/rubik/orchestrator.rb +11 -0
  157. data/lib/rubik/platform/filesystem.rb +64 -0
  158. data/lib/rubik/platform/io_drain.rb +53 -0
  159. data/lib/rubik/platform/path.rb +210 -0
  160. data/lib/rubik/platform/process.rb +38 -0
  161. data/lib/rubik/platform/sanitize.rb +157 -0
  162. data/lib/rubik/platform.rb +113 -0
  163. data/lib/rubik/plugin/dsl.rb +51 -0
  164. data/lib/rubik/plugin/manager.rb +247 -0
  165. data/lib/rubik/plugin/storage.rb +195 -0
  166. data/lib/rubik/plugin.rb +10 -0
  167. data/lib/rubik/prompt/context_builder.rb +377 -0
  168. data/lib/rubik/prompt/context_compactor.rb +326 -0
  169. data/lib/rubik/prompt/conversation_rows.rb +74 -0
  170. data/lib/rubik/prompt/doctrine.rb +160 -0
  171. data/lib/rubik/prompt/emergency_compactor.rb +81 -0
  172. data/lib/rubik/prompt/services.rb +60 -0
  173. data/lib/rubik/prompt/token_estimator.rb +63 -0
  174. data/lib/rubik/prompt/tool_teachings.rb +78 -0
  175. data/lib/rubik/prompt.rb +13 -0
  176. data/lib/rubik/repair/loop.rb +95 -0
  177. data/lib/rubik/repair.rb +8 -0
  178. data/lib/rubik/repl/ast_warmup.rb +129 -0
  179. data/lib/rubik/repl/command_context.rb +214 -0
  180. data/lib/rubik/repl/command_parser.rb +48 -0
  181. data/lib/rubik/repl/commands/agent.rb +30 -0
  182. data/lib/rubik/repl/commands/agent_context.rb +289 -0
  183. data/lib/rubik/repl/commands/agent_execution.rb +342 -0
  184. data/lib/rubik/repl/commands/agent_models.rb +212 -0
  185. data/lib/rubik/repl/commands/agent_providers.rb +232 -0
  186. data/lib/rubik/repl/commands/base.rb +55 -0
  187. data/lib/rubik/repl/commands/buffer.rb +124 -0
  188. data/lib/rubik/repl/commands/dmesg.rb +87 -0
  189. data/lib/rubik/repl/commands/lsp.rb +235 -0
  190. data/lib/rubik/repl/commands/mcp.rb +194 -0
  191. data/lib/rubik/repl/commands/plugins.rb +208 -0
  192. data/lib/rubik/repl/commands/pruner.rb +71 -0
  193. data/lib/rubik/repl/commands/session.rb +183 -0
  194. data/lib/rubik/repl/commands/skills.rb +111 -0
  195. data/lib/rubik/repl/commands/storage.rb +85 -0
  196. data/lib/rubik/repl/commands/subagents.rb +195 -0
  197. data/lib/rubik/repl/commands/system.rb +85 -0
  198. data/lib/rubik/repl/commands/tasks.rb +120 -0
  199. data/lib/rubik/repl/commands/workspace.rb +443 -0
  200. data/lib/rubik/repl/commands.rb +44 -0
  201. data/lib/rubik/repl/composition.rb +32 -0
  202. data/lib/rubik/repl/config_wiring.rb +142 -0
  203. data/lib/rubik/repl/confirmations.rb +56 -0
  204. data/lib/rubik/repl/hooks.rb +47 -0
  205. data/lib/rubik/repl/input_execution.rb +323 -0
  206. data/lib/rubik/repl/input_loop.rb +213 -0
  207. data/lib/rubik/repl/interrupt_handler.rb +217 -0
  208. data/lib/rubik/repl/lifecycle.rb +410 -0
  209. data/lib/rubik/repl/paste_support.rb +88 -0
  210. data/lib/rubik/repl/pending_notice_queue.rb +60 -0
  211. data/lib/rubik/repl/presentation.rb +132 -0
  212. data/lib/rubik/repl/pruner/brief.rb +101 -0
  213. data/lib/rubik/repl/pruner/gate.rb +279 -0
  214. data/lib/rubik/repl/pruner/harness.rb +236 -0
  215. data/lib/rubik/repl/pruner/rules.rb +316 -0
  216. data/lib/rubik/repl/pruner/service.rb +82 -0
  217. data/lib/rubik/repl/pruner.rb +39 -0
  218. data/lib/rubik/repl/service_catalog.rb +374 -0
  219. data/lib/rubik/repl/setup_factories.rb +66 -0
  220. data/lib/rubik/repl/shell.rb +231 -0
  221. data/lib/rubik/repl/step_formatter.rb +324 -0
  222. data/lib/rubik/repl/tee_io.rb +105 -0
  223. data/lib/rubik/repl/terminal_guard.rb +157 -0
  224. data/lib/rubik/repl/turn_recorder.rb +234 -0
  225. data/lib/rubik/repl.rb +14 -0
  226. data/lib/rubik/runtime/ast_service.rb +78 -0
  227. data/lib/rubik/runtime/context.rb +335 -0
  228. data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
  229. data/lib/rubik/runtime/discovery.rb +342 -0
  230. data/lib/rubik/runtime/fiber.rb +142 -0
  231. data/lib/rubik/runtime/fiber_ordering.rb +52 -0
  232. data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
  233. data/lib/rubik/runtime/index_service.rb +58 -0
  234. data/lib/rubik/runtime/loop_service.rb +73 -0
  235. data/lib/rubik/runtime/manager.rb +241 -0
  236. data/lib/rubik/runtime/overlay_storage.rb +481 -0
  237. data/lib/rubik/runtime/service_definition.rb +54 -0
  238. data/lib/rubik/runtime/service_registration.rb +33 -0
  239. data/lib/rubik/runtime/task_manager.rb +302 -0
  240. data/lib/rubik/runtime.rb +19 -0
  241. data/lib/rubik/sandbox/driver/base.rb +30 -0
  242. data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
  243. data/lib/rubik/sandbox/driver/docker.rb +113 -0
  244. data/lib/rubik/sandbox/driver/podman.rb +98 -0
  245. data/lib/rubik/sandbox/driver/process.rb +190 -0
  246. data/lib/rubik/sandbox/driver.rb +91 -0
  247. data/lib/rubik/sandbox/runner.rb +218 -0
  248. data/lib/rubik/sandbox/shell_resolver.rb +178 -0
  249. data/lib/rubik/sandbox.rb +8 -0
  250. data/lib/rubik/security/blocked_guidance.rb +187 -0
  251. data/lib/rubik/security/classifier/classifier.rb +440 -0
  252. data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
  253. data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
  254. data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
  255. data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
  256. data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
  257. data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
  258. data/lib/rubik/security/classifier/commands/black.yaml +12 -0
  259. data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
  260. data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
  261. data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
  262. data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
  263. data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
  264. data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
  265. data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
  266. data/lib/rubik/security/classifier/commands/column.yaml +7 -0
  267. data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
  268. data/lib/rubik/security/classifier/commands/command.yaml +12 -0
  269. data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
  270. data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
  271. data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
  272. data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
  273. data/lib/rubik/security/classifier/commands/date.yaml +10 -0
  274. data/lib/rubik/security/classifier/commands/df.yaml +7 -0
  275. data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
  276. data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
  277. data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
  278. data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
  279. data/lib/rubik/security/classifier/commands/du.yaml +7 -0
  280. data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
  281. data/lib/rubik/security/classifier/commands/env.yaml +15 -0
  282. data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
  283. data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
  284. data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
  285. data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
  286. data/lib/rubik/security/classifier/commands/false.yaml +7 -0
  287. data/lib/rubik/security/classifier/commands/file.yaml +7 -0
  288. data/lib/rubik/security/classifier/commands/find.yaml +25 -0
  289. data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
  290. data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
  291. data/lib/rubik/security/classifier/commands/free.yaml +7 -0
  292. data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
  293. data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
  294. data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
  295. data/lib/rubik/security/classifier/commands/git.yaml +524 -0
  296. data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
  297. data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
  298. data/lib/rubik/security/classifier/commands/go.yaml +44 -0
  299. data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
  300. data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
  301. data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
  302. data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
  303. data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
  304. data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
  305. data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
  306. data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
  307. data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
  308. data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
  309. data/lib/rubik/security/classifier/commands/head.yaml +7 -0
  310. data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
  311. data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
  312. data/lib/rubik/security/classifier/commands/id.yaml +7 -0
  313. data/lib/rubik/security/classifier/commands/install.yaml +7 -0
  314. data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
  315. data/lib/rubik/security/classifier/commands/java.yaml +35 -0
  316. data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
  317. data/lib/rubik/security/classifier/commands/join.yaml +7 -0
  318. data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
  319. data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
  320. data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
  321. data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
  322. data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
  323. data/lib/rubik/security/classifier/commands/less.yaml +7 -0
  324. data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
  325. data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
  326. data/lib/rubik/security/classifier/commands/make.yaml +21 -0
  327. data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
  328. data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
  329. data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
  330. data/lib/rubik/security/classifier/commands/more.yaml +7 -0
  331. data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
  332. data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
  333. data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
  334. data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
  335. data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
  336. data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
  337. data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
  338. data/lib/rubik/security/classifier/commands/node.yaml +20 -0
  339. data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
  340. data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
  341. data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
  342. data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
  343. data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
  344. data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
  345. data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
  346. data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
  347. data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
  348. data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
  349. data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
  350. data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
  351. data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
  352. data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
  353. data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
  354. data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
  355. data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
  356. data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
  357. data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
  358. data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
  359. data/lib/rubik/security/classifier/commands/python.yaml +9 -0
  360. data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
  361. data/lib/rubik/security/classifier/commands/read.yaml +7 -0
  362. data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
  363. data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
  364. data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
  365. data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
  366. data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
  367. data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
  368. data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
  369. data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
  370. data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
  371. data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
  372. data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
  373. data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
  374. data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
  375. data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
  376. data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
  377. data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
  378. data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
  379. data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
  380. data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
  381. data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
  382. data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
  383. data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
  384. data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
  385. data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
  386. data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
  387. data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
  388. data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
  389. data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
  390. data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
  391. data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
  392. data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
  393. data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
  394. data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
  395. data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
  396. data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
  397. data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
  398. data/lib/rubik/security/classifier/commands/test.yaml +7 -0
  399. data/lib/rubik/security/classifier/commands/time.yaml +8 -0
  400. data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
  401. data/lib/rubik/security/classifier/commands/top.yaml +7 -0
  402. data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
  403. data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
  404. data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
  405. data/lib/rubik/security/classifier/commands/true.yaml +7 -0
  406. data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
  407. data/lib/rubik/security/classifier/commands/type.yaml +7 -0
  408. data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
  409. data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
  410. data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
  411. data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
  412. data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
  413. data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
  414. data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
  415. data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
  416. data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
  417. data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
  418. data/lib/rubik/security/classifier/commands/which.yaml +7 -0
  419. data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
  420. data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
  421. data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
  422. data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
  423. data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
  424. data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
  425. data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
  426. data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
  427. data/lib/rubik/security/classifier/database.rb +216 -0
  428. data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
  429. data/lib/rubik/security/classifier/matcher/options.rb +245 -0
  430. data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
  431. data/lib/rubik/security/classifier/matcher.rb +249 -0
  432. data/lib/rubik/security/classifier/models.rb +170 -0
  433. data/lib/rubik/security/classifier/parser.rb +23 -0
  434. data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
  435. data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
  436. data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
  437. data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
  438. data/lib/rubik/security/classifier.rb +25 -0
  439. data/lib/rubik/security/errors.rb +39 -0
  440. data/lib/rubik/security/guard.rb +954 -0
  441. data/lib/rubik/security/redactor.rb +89 -0
  442. data/lib/rubik/security.rb +11 -0
  443. data/lib/rubik/session/manager.rb +331 -0
  444. data/lib/rubik/session/recorder.rb +149 -0
  445. data/lib/rubik/session/replayer.rb +65 -0
  446. data/lib/rubik/session.rb +10 -0
  447. data/lib/rubik/skills/frontmatter.rb +41 -0
  448. data/lib/rubik/skills/manager.rb +223 -0
  449. data/lib/rubik/skills/registry.rb +64 -0
  450. data/lib/rubik/skills/skill.rb +33 -0
  451. data/lib/rubik/skills/storage.rb +77 -0
  452. data/lib/rubik/skills/validator.rb +116 -0
  453. data/lib/rubik/skills.rb +20 -0
  454. data/lib/rubik/storage/folding.rb +23 -0
  455. data/lib/rubik/storage/layout.rb +145 -0
  456. data/lib/rubik/storage/render.rb +97 -0
  457. data/lib/rubik/storage/service.rb +42 -0
  458. data/lib/rubik/storage/store.rb +514 -0
  459. data/lib/rubik/storage/transcript_indexer.rb +116 -0
  460. data/lib/rubik/storage/uri_reader.rb +168 -0
  461. data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
  462. data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
  463. data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
  464. data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
  465. data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
  466. data/lib/rubik/storage/vakanuvis/service.rb +69 -0
  467. data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
  468. data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
  469. data/lib/rubik/storage/vakanuvis.rb +77 -0
  470. data/lib/rubik/storage.rb +29 -0
  471. data/lib/rubik/tools/base.rb +107 -0
  472. data/lib/rubik/tools/builtins/dmesg.rb +74 -0
  473. data/lib/rubik/tools/builtins/exec.rb +293 -0
  474. data/lib/rubik/tools/builtins/files.rb +354 -0
  475. data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
  476. data/lib/rubik/tools/builtins/grep.rb +281 -0
  477. data/lib/rubik/tools/builtins/interactive.rb +175 -0
  478. data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
  479. data/lib/rubik/tools/builtins/lsp.rb +283 -0
  480. data/lib/rubik/tools/builtins/pruner.rb +88 -0
  481. data/lib/rubik/tools/builtins/runtime.rb +120 -0
  482. data/lib/rubik/tools/builtins/send_message.rb +109 -0
  483. data/lib/rubik/tools/builtins/show_file.rb +84 -0
  484. data/lib/rubik/tools/builtins/skill.rb +72 -0
  485. data/lib/rubik/tools/builtins/storage.rb +232 -0
  486. data/lib/rubik/tools/builtins/subagent.rb +356 -0
  487. data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
  488. data/lib/rubik/tools/builtins/symbols.rb +209 -0
  489. data/lib/rubik/tools/builtins/synthesis.rb +314 -0
  490. data/lib/rubik/tools/builtins/task.rb +168 -0
  491. data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
  492. data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
  493. data/lib/rubik/tools/builtins.rb +30 -0
  494. data/lib/rubik/tools/catalog.rb +70 -0
  495. data/lib/rubik/tools/declaration.rb +90 -0
  496. data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
  497. data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
  498. data/lib/rubik/tools/native_search.rb +180 -0
  499. data/lib/rubik/tools/output.rb +54 -0
  500. data/lib/rubik/tools/preflight_runner.rb +209 -0
  501. data/lib/rubik/tools/registry.rb +247 -0
  502. data/lib/rubik/tools/result.rb +71 -0
  503. data/lib/rubik/tools/services.rb +96 -0
  504. data/lib/rubik/tools/truncator.rb +78 -0
  505. data/lib/rubik/tools/uri_resolver.rb +195 -0
  506. data/lib/rubik/tools.rb +14 -0
  507. data/lib/rubik/ui/approvals.rb +52 -0
  508. data/lib/rubik/ui/banner.rb +43 -0
  509. data/lib/rubik/ui/capability.rb +122 -0
  510. data/lib/rubik/ui/dialog.rb +106 -0
  511. data/lib/rubik/ui/diff_viewer.rb +78 -0
  512. data/lib/rubik/ui/frame.rb +95 -0
  513. data/lib/rubik/ui/markdown_stream.rb +107 -0
  514. data/lib/rubik/ui/panel.rb +98 -0
  515. data/lib/rubik/ui/renderer.rb +206 -0
  516. data/lib/rubik/ui/spinner.rb +120 -0
  517. data/lib/rubik/ui/statusline.rb +134 -0
  518. data/lib/rubik/ui/theme.rb +118 -0
  519. data/lib/rubik/ui.rb +57 -0
  520. data/lib/rubik/uris.rb +101 -0
  521. data/lib/rubik/version.rb +5 -0
  522. data/lib/rubik.rb +65 -0
  523. metadata +770 -0
@@ -0,0 +1,272 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "symbol"
4
+ require_relative "extractor"
5
+ require_relative "cache_store"
6
+
7
+ module Rubik
8
+ module AST
9
+ class SymbolMap < Hash
10
+ attr_reader :qualified_symbols
11
+
12
+ def initialize(plain = {}, qualified = {})
13
+ super()
14
+ @qualified_symbols = qualified
15
+ replace(plain)
16
+ end
17
+
18
+ def [](key)
19
+ super || @qualified_symbols[key]
20
+ end
21
+
22
+ # Checks whether a symbol key exists in either the plain or qualified symbol maps.
23
+ #
24
+ # @param key [String] the symbol name or qualified key to look up
25
+ # @return [Boolean] true if the key is present in either map
26
+ def key?(key)
27
+ super || @qualified_symbols.key?(key)
28
+ end
29
+
30
+ # deprecated: use #key?; kept for callers using the legacy Hash#has_key? spelling.
31
+ alias has_key? key?
32
+ end
33
+
34
+ class Graph
35
+ def initialize
36
+ @symbols = {}
37
+ @qualified_symbols = {}
38
+ @file_symbols = Hash.new { |h, k| h[k] = [] }
39
+ @mutex = Mutex.new
40
+ end
41
+
42
+ def symbols
43
+ @mutex.synchronize do
44
+ SymbolMap.new(@symbols.dup, @qualified_symbols.dup)
45
+ end
46
+ end
47
+
48
+ def file_symbols
49
+ @mutex.synchronize { @file_symbols.dup }
50
+ end
51
+
52
+ def clear
53
+ @mutex.synchronize do
54
+ @symbols.clear
55
+ @qualified_symbols.clear
56
+ @file_symbols.clear
57
+ end
58
+ end
59
+
60
+ def index_file(file_path, content = nil)
61
+ @mutex.synchronize do
62
+ _index_file_unlocked(file_path, content)
63
+ end
64
+ end
65
+
66
+ def remove_file(file_path)
67
+ @mutex.synchronize do
68
+ _remove_file_unlocked(file_path)
69
+ end
70
+ end
71
+
72
+ def load_from_cache(cache_store)
73
+ return 0 unless cache_store
74
+
75
+ loaded_files = cache_store.load
76
+ return 0 if loaded_files.nil? || loaded_files.empty?
77
+
78
+ @mutex.synchronize do
79
+ total_symbols = 0
80
+ loaded_files.each do |file_path, syms|
81
+ ext = File.extname(file_path).downcase
82
+ lang = CacheStore::LANGUAGE_MAP[ext] || "code"
83
+
84
+ @file_symbols[file_path] = syms.dup
85
+ syms.each do |sym|
86
+ @symbols[sym.name] = sym
87
+ q_key = "#{lang}:#{file_path}##{sym.name}"
88
+ @qualified_symbols[q_key] = sym
89
+ total_symbols += 1
90
+ end
91
+ end
92
+ total_symbols
93
+ end
94
+ end
95
+
96
+ def save_to_cache(cache_store)
97
+ return false unless cache_store
98
+
99
+ snapshot = @mutex.synchronize { @file_symbols.dup }
100
+ cache_store.save(snapshot)
101
+ end
102
+
103
+ # Duplicate-free symbol search: iterates the live map under the mutex
104
+ # instead of copying it via #symbols (callers only read immutable
105
+ # attributes, so handing out the Symbol objects is as safe as the
106
+ # snapshot was — it shared the very same objects).
107
+ def search_symbols(query)
108
+ q = query.to_s.downcase
109
+ @mutex.synchronize do
110
+ @symbols.values.select { |sym| sym.name.downcase.include?(q) }
111
+ end
112
+ end
113
+
114
+ # The callers of `symbol_name` are the symbols whose `calls` list
115
+ # actually references it. `symbol_name` may arrive either as a plain
116
+ # identifier (e.g. "add") or as a qualified key (e.g.
117
+ # "rust:src/lib.rs##Config::new") — the `!refs` / callers flow does the
118
+ # latter. We resolve it to the set of concrete callee spellings it maps
119
+ # to (plain name plus every qualified key that owns that name), then keep
120
+ # the symbols whose calls contain one of those spellings. This mirrors the
121
+ # plain-then-qualified lookup used by #smart_context_for / #symbol_definition
122
+ # and avoids the previous backwards test, which flagged any symbol merely
123
+ # mentioning the substring as a caller.
124
+ def callers_of(symbol_name)
125
+ @mutex.synchronize do
126
+ target_names = caller_targets(symbol_name)
127
+ return [] if target_names.empty?
128
+
129
+ @symbols.values.uniq.select do |sym|
130
+ sym.calls.any? { |c| target_names.include?(c) }
131
+ end
132
+ end
133
+ end
134
+
135
+ def smart_context_for(symbol_name)
136
+ @mutex.synchronize do
137
+ target = @symbols[symbol_name] ||
138
+ @qualified_symbols[symbol_name] ||
139
+ @symbols.values.find { |s| s.name == symbol_name || s.name.end_with?("##{symbol_name}") }
140
+ return "" unless target
141
+
142
+ context_blocks = []
143
+ context_blocks << "# Target symbol: #{target.name} (#{target.file}:#{target.line_start}-#{target.line_end})"
144
+ context_blocks << target.body if target.body
145
+
146
+ target.calls.each do |called_name|
147
+ matching = @symbols[called_name] ||
148
+ @qualified_symbols[called_name] ||
149
+ @symbols.values.find { |s| s.name == called_name || s.name.end_with?("##{called_name}") }
150
+ if matching
151
+ context_blocks << "# Dependency signature: #{matching.name}"
152
+ context_blocks << matching.signature
153
+ end
154
+ end
155
+
156
+ context_blocks.join("\n\n")
157
+ end
158
+ end
159
+
160
+ def outline_for(file_path)
161
+ @mutex.synchronize do
162
+ syms = _get_file_symbols_unlocked(file_path)
163
+ syms = _index_file_unlocked(file_path) if syms.empty? && File.exist?(file_path)
164
+
165
+ return "No symbols found in '#{file_path}'." if syms.empty?
166
+
167
+ lines = ["=== Outline for #{file_path} (#{syms.size} symbols) ==="]
168
+ syms.sort_by(&:line_start).each do |sym|
169
+ indent = [:class, :struct, :module, :interface, :type].include?(sym.kind) ? "" : " "
170
+ lines << "#{indent}• [#{sym.kind.to_s.upcase}] #{sym.name} (lines #{sym.line_start}-#{sym.line_end}) -> #{sym.signature}"
171
+ end
172
+ lines.join("\n")
173
+ end
174
+ end
175
+
176
+ def symbol_definition(symbol_name)
177
+ @mutex.synchronize do
178
+ target = @symbols[symbol_name] ||
179
+ @qualified_symbols[symbol_name] ||
180
+ @symbols.values.find do |s|
181
+ s.name.downcase == symbol_name.downcase ||
182
+ s.name.end_with?("##{symbol_name}") ||
183
+ s.name.end_with?(".#{symbol_name}")
184
+ end
185
+ return nil unless target
186
+
187
+ {
188
+ name: target.name,
189
+ kind: target.kind,
190
+ file: target.file,
191
+ line_start: target.line_start,
192
+ line_end: target.line_end,
193
+ signature: target.signature,
194
+ body: target.body,
195
+ docstring: target.docstring,
196
+ calls: target.calls
197
+ }
198
+ end
199
+ end
200
+
201
+ private
202
+
203
+ # Concrete callee spellings that a reference to `symbol_name` might use.
204
+ # `symbol_name` may be a bare identifier ("Config::new") or a qualified key
205
+ # ("rust:src/lib.rs##Config::new"); we pull the trailing identifier in both
206
+ # cases and collect every symbol that owns that name so a call site is
207
+ # matched whether it wrote the bare identifier or the fully-qualified key.
208
+ # Caller MUST hold @mutex.
209
+ def caller_targets(symbol_name)
210
+ bare = symbol_name.split("##").last.to_s
211
+ return [] if bare.empty?
212
+
213
+ names = [bare]
214
+ @symbols.each_value do |sym|
215
+ names << sym.name if sym.name == bare || sym.name.end_with?("##{bare}")
216
+ end
217
+ names.uniq
218
+ end
219
+
220
+ def _index_file_unlocked(file_path, content = nil)
221
+ _remove_file_unlocked(file_path)
222
+
223
+ extracted = Extractor.extract(file_path, content)
224
+ ext = File.extname(file_path).downcase
225
+ lang = CacheStore::LANGUAGE_MAP[ext] || "code"
226
+
227
+ extracted.each do |sym|
228
+ @symbols[sym.name] = sym
229
+ q_key = "#{lang}:#{file_path}##{sym.name}"
230
+ @qualified_symbols[q_key] = sym
231
+ @file_symbols[file_path] << sym
232
+ end
233
+ extracted
234
+ end
235
+
236
+ def _remove_file_unlocked(file_path)
237
+ existing = @file_symbols.delete(file_path)
238
+ if existing.nil? || existing.empty?
239
+ canonical_target = Rubik::Platform.normalize_path(File.expand_path(file_path))
240
+ matching_key = @file_symbols.keys.find do |k|
241
+ Rubik::Platform.path_equal?(k, file_path) ||
242
+ Rubik::Platform.normalize_path(File.expand_path(k)) == canonical_target
243
+ end
244
+ existing = @file_symbols.delete(matching_key) if matching_key
245
+ end
246
+
247
+ return unless existing && !existing.empty?
248
+
249
+ ext = File.extname(file_path).downcase
250
+ lang = CacheStore::LANGUAGE_MAP[ext] || "code"
251
+
252
+ existing.each do |sym|
253
+ @symbols.delete(sym.name) if @symbols[sym.name] == sym
254
+ @qualified_symbols.delete("#{lang}:#{file_path}##{sym.name}")
255
+ @qualified_symbols.delete("#{lang}:#{sym.file}##{sym.name}") if sym.file
256
+ end
257
+ end
258
+
259
+ def _get_file_symbols_unlocked(file_path)
260
+ syms = @file_symbols[file_path]
261
+ return syms if syms && !syms.empty?
262
+
263
+ canonical_target = Rubik::Platform.normalize_path(File.expand_path(file_path))
264
+ matching_key = @file_symbols.keys.find do |k|
265
+ Rubik::Platform.path_equal?(k, file_path) ||
266
+ Rubik::Platform.normalize_path(File.expand_path(k)) == canonical_target
267
+ end
268
+ (matching_key && @file_symbols[matching_key]) || []
269
+ end
270
+ end
271
+ end
272
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "symbol"
4
+ require_relative "body_loader"
5
+
6
+ module Rubik
7
+ module AST
8
+ class PythonExtractor
9
+ include BodyLoader
10
+
11
+ EXCLUDED_KEYWORDS = Set.new(
12
+ ["def", "async", "if", "elif", "else", "for", "while", "return", "print", "len", "range", "super"]
13
+ ).freeze
14
+
15
+ def self.extract(file_path, content = nil)
16
+ new(file_path, content).extract
17
+ end
18
+
19
+ def initialize(file_path, content = nil)
20
+ @file_path = file_path
21
+ @content = content || (File.exist?(file_path) ? File.read(file_path) : "")
22
+ @lines = @content.lines
23
+ end
24
+
25
+ def extract
26
+ symbols = []
27
+ return symbols if @content.empty?
28
+
29
+ current_class = nil
30
+ i = 0
31
+
32
+ while i < @lines.size
33
+ line = @lines[i]
34
+ line_num = i + 1
35
+ indent = line[/^\s*/].size
36
+ stripped = line.strip
37
+
38
+ if stripped.start_with?("import ") || stripped.start_with?("from ")
39
+ symbols << Symbol.new(
40
+ name: stripped,
41
+ kind: :import,
42
+ file: @file_path,
43
+ line_start: line_num,
44
+ line_end: line_num,
45
+ signature: stripped
46
+ )
47
+ elsif (match = stripped.match(/^class\s+([A-Za-z0-9_]+)(?:\((.*?)\))?:/))
48
+ class_name = match[1]
49
+ current_class = class_name
50
+ end_line = find_block_end(i, indent)
51
+ symbols << Symbol.new(
52
+ name: class_name,
53
+ kind: :class,
54
+ file: @file_path,
55
+ line_start: line_num,
56
+ line_end: end_line,
57
+ signature: stripped,
58
+ body_loader: body_loader_for(line_num, end_line)
59
+ )
60
+ elsif (match = stripped.match(/^(?:async\s+)?def\s+([A-Za-z0-9_]+)\s*\((.*?)\):/))
61
+ fn_name = match[1]
62
+ full_name = indent.positive? && current_class ? "#{current_class}.#{fn_name}" : fn_name
63
+ end_line = find_block_end(i, indent)
64
+ body_for_calls = extract_lines(line_num, end_line)
65
+ calls = extract_calls(body_for_calls, fn_name)
66
+
67
+ symbols << Symbol.new(
68
+ name: full_name,
69
+ kind: :function,
70
+ file: @file_path,
71
+ line_start: line_num,
72
+ line_end: end_line,
73
+ signature: stripped,
74
+ calls: calls,
75
+ body_loader: body_loader_for(line_num, end_line)
76
+ )
77
+ end
78
+
79
+ current_class = nil if indent.zero? && !stripped.start_with?("class ") && !stripped.empty?
80
+
81
+ i += 1
82
+ end
83
+
84
+ symbols
85
+ end
86
+
87
+ private
88
+
89
+ def extract_calls(body, self_name)
90
+ calls = Set.new
91
+ body.scan(/\b[A-Za-z0-9_]+(?=\s*\()/) do |c|
92
+ calls << c unless c == self_name || EXCLUDED_KEYWORDS.include?(c)
93
+ end
94
+ calls.to_a
95
+ end
96
+
97
+ def find_block_end(start_idx, base_indent)
98
+ last_line = start_idx + 1
99
+
100
+ ((start_idx + 1)..(@lines.size - 1)).each do |idx|
101
+ line = @lines[idx]
102
+ next if line.strip.empty? || line.strip.start_with?("#")
103
+
104
+ indent = line[/^\s*/].size
105
+ return last_line if indent <= base_indent
106
+
107
+ last_line = idx + 1
108
+ end
109
+
110
+ @lines.size
111
+ end
112
+
113
+ def extract_lines(start_line, end_line)
114
+ s_idx = [start_line - 1, 0].max
115
+ e_idx = [end_line - 1, @lines.size - 1].min
116
+ @lines[s_idx..e_idx].join
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "symbol"
4
+ require_relative "body_loader"
5
+ require_relative "fallback_cleaner"
6
+
7
+ module Rubik
8
+ module AST
9
+ class RubyExtractor
10
+ EXCLUDED_CALLS = Set.new(["def", "if", "unless", "case", "return", "raise", "puts", "p", "print", "require"]).freeze
11
+ begin
12
+ require "prism"
13
+ PRISM_AVAILABLE = true
14
+ rescue LoadError
15
+ PRISM_AVAILABLE = false
16
+ end
17
+
18
+ include BodyLoader
19
+
20
+ def self.extract(file_path, content = nil)
21
+ new(file_path, content).extract
22
+ end
23
+
24
+ def initialize(file_path, content = nil, force_fallback: false)
25
+ @file_path = file_path
26
+ @content = content || (File.exist?(file_path) ? File.read(file_path) : "")
27
+ @lines = nil
28
+ # force_fallback exists so the Prism-less scanner stays testable even
29
+ # on installs where prism is available (PRISM_AVAILABLE cannot be
30
+ # stubbed; it is resolved at require time).
31
+ @use_prism = PRISM_AVAILABLE && !force_fallback
32
+ @code_lines = nil
33
+ end
34
+
35
+ # Line array is materialized on demand: the Prism walk only needs AST
36
+ # locations, so a parse-only session over a file whose bodies are never
37
+ # read never pays for String#lines. The fallback scanner and the lazy
38
+ # body loader share this memo.
39
+ def lines
40
+ @lines ||= @content.lines
41
+ end
42
+
43
+ # Quote/comment/heredoc-cleaned view of the source for the fallback
44
+ # scanner only (Plan 54 D-7); body loading keeps reading the raw lines.
45
+ def code_lines
46
+ @code_lines ||= FallbackCleaner.code_lines(lines)
47
+ end
48
+
49
+ def extract
50
+ symbols = []
51
+ return symbols if @content.empty?
52
+
53
+ if @use_prism
54
+ extract_via_prism(symbols)
55
+ else
56
+ extract_via_fallback(symbols)
57
+ end
58
+ symbols
59
+ end
60
+
61
+ private
62
+
63
+ def extract_via_prism(symbols)
64
+ result = Prism.parse(@content)
65
+ return unless result.value
66
+
67
+ walk_node(result.value, nil, symbols)
68
+ end
69
+
70
+ def walk_node(node, current_scope, symbols)
71
+ return unless node
72
+
73
+ case node
74
+ when Prism::ClassNode
75
+ name = node.name.to_s
76
+ line_start = node.location.start_line
77
+ line_end = node.location.end_line
78
+ symbols << Symbol.new(
79
+ name: name,
80
+ kind: :class,
81
+ file: @file_path,
82
+ line_start: line_start,
83
+ line_end: line_end,
84
+ signature: "class #{name}"
85
+ )
86
+ node.compact_child_nodes.each { |child| walk_node(child, name, symbols) }
87
+
88
+ when Prism::ModuleNode
89
+ name = node.name.to_s
90
+ line_start = node.location.start_line
91
+ line_end = node.location.end_line
92
+ symbols << Symbol.new(
93
+ name: name,
94
+ kind: :module,
95
+ file: @file_path,
96
+ line_start: line_start,
97
+ line_end: line_end,
98
+ signature: "module #{name}"
99
+ )
100
+ node.compact_child_nodes.each { |child| walk_node(child, name, symbols) }
101
+
102
+ when Prism::DefNode
103
+ method_name = node.name.to_s
104
+ full_name = current_scope ? "#{current_scope}##{method_name}" : method_name
105
+ line_start = node.location.start_line
106
+ line_end = node.location.end_line
107
+ calls = extract_calls_from_node(node, method_name)
108
+
109
+ symbols << Symbol.new(
110
+ name: full_name,
111
+ kind: :function,
112
+ file: @file_path,
113
+ line_start: line_start,
114
+ line_end: line_end,
115
+ signature: "def #{method_name}",
116
+ calls: calls,
117
+ body_loader: body_loader_for(line_start, line_end)
118
+ )
119
+
120
+ when Prism::CallNode
121
+ name = node.name.to_s
122
+ if ["require", "require_relative"].include?(name)
123
+ line_num = node.location.start_line
124
+ args_node = node.arguments&.arguments
125
+ arg = args_node&.first&.slice.to_s
126
+ symbols << Symbol.new(
127
+ name: "#{name} #{arg}",
128
+ kind: :import,
129
+ file: @file_path,
130
+ line_start: line_num,
131
+ line_end: line_num,
132
+ signature: "#{name} #{arg}"
133
+ )
134
+ end
135
+ node.compact_child_nodes.each { |child| walk_node(child, current_scope, symbols) }
136
+
137
+ when Prism::ProgramNode, Prism::StatementsNode
138
+ node.compact_child_nodes.each { |child| walk_node(child, current_scope, symbols) }
139
+ else
140
+ node.compact_child_nodes.each { |child| walk_node(child, current_scope, symbols) } if node.respond_to?(:compact_child_nodes)
141
+ end
142
+ end
143
+
144
+ def extract_calls_from_node(node, self_name)
145
+ calls = []
146
+ queue = [node]
147
+
148
+ while (curr = queue.pop)
149
+ if curr.is_a?(Prism::CallNode)
150
+ cname = curr.name.to_s
151
+ calls << cname unless cname == self_name || ["require", "require_relative", "raise", "puts", "p", "print"].include?(cname)
152
+ end
153
+ queue.concat(curr.compact_child_nodes) if curr.respond_to?(:compact_child_nodes)
154
+ end
155
+
156
+ calls.uniq
157
+ end
158
+
159
+ def extract_via_fallback(symbols)
160
+ current_class_or_module = nil
161
+ i = 0
162
+
163
+ while i < code_lines.size
164
+ line = code_lines[i]
165
+ line_num = i + 1
166
+ stripped = line.strip
167
+
168
+ if stripped.start_with?("class ") || stripped.start_with?("module ")
169
+ kind = stripped.start_with?("class ") ? :class : :module
170
+ name = stripped.split(/\s+/)[1].to_s.split("<").first.to_s.strip
171
+ current_class_or_module = name
172
+ end_line = find_matching_end(i)
173
+
174
+ symbols << Symbol.new(
175
+ name: name,
176
+ kind: kind,
177
+ file: @file_path,
178
+ line_start: line_num,
179
+ line_end: end_line,
180
+ signature: stripped
181
+ )
182
+ elsif stripped.start_with?("def ")
183
+ method_name = stripped.sub(/^def\s+/, "").split(/[(\s;]/).first.strip
184
+ full_name = current_class_or_module ? "#{current_class_or_module}##{method_name}" : method_name
185
+ end_line = find_matching_end(i)
186
+ calls_src = lines[(line_num - 1)..(end_line - 1)]&.join.to_s
187
+ calls = []
188
+ calls_src.scan(/\b([a-z_][a-zA-Z0-9_]*)\s*\(/) do |m|
189
+ c = m[0]
190
+ calls << c unless c == method_name || EXCLUDED_CALLS.include?(c) || calls.include?(c)
191
+ end
192
+
193
+ symbols << Symbol.new(
194
+ name: full_name,
195
+ kind: :function,
196
+ file: @file_path,
197
+ line_start: line_num,
198
+ line_end: end_line,
199
+ signature: stripped,
200
+ calls: calls,
201
+ body_loader: body_loader_for(line_num, end_line)
202
+ )
203
+ elsif stripped.start_with?("require ") || stripped.start_with?("require_relative ")
204
+ symbols << Symbol.new(
205
+ name: stripped,
206
+ kind: :import,
207
+ file: @file_path,
208
+ line_start: line_num,
209
+ line_end: line_num,
210
+ signature: stripped
211
+ )
212
+ end
213
+
214
+ i += 1
215
+ end
216
+ end
217
+
218
+ def find_matching_end(start_idx)
219
+ depth = 0
220
+ (start_idx..(code_lines.size - 1)).each do |idx|
221
+ line = code_lines[idx]
222
+ next if line.strip.empty?
223
+
224
+ depth += openers_in(line)
225
+ depth -= line.scan(/\bend\b/).size
226
+ return idx + 1 if depth <= 0 && idx >= start_idx
227
+ end
228
+ start_idx + 1
229
+ end
230
+
231
+ # End-matching counts end-consuming openers (class/module/def/do/case/
232
+ # begin/for plus statement-position if/unless/while/until) against
233
+ # `end` tokens over FallbackCleaner lines (Plan 54 D-7). Modifier
234
+ # conditionals and endless defs consume no end and are excluded.
235
+ def openers_in(line)
236
+ count = line.scan(/\b(?:class|module|def|do|case|begin|for)\b/).size
237
+ count -= 1 if endless_def?(line)
238
+ count += 1 if line.match?(/\A\s*(?:if|unless|while|until)\b/)
239
+ count += line.scan(/[=;]\s*(?:if|unless|while|until)\b/).size
240
+ count
241
+ end
242
+
243
+ # `def name = body` and `def name(x) = body` consume no end. Setter defs
244
+ # (`def name=(v)`) do, so a `=` immediately followed by `(` is excluded.
245
+ def endless_def?(line)
246
+ m = line.match(/\A\s*def\s+[\w.]+(?:\([^)]*\))?/)
247
+ return false unless m
248
+
249
+ rest = line[m[0].size..].to_s
250
+ rest.match?(/\A\s*=\s*\S/) && !rest.match?(/\A\s*=\s*\(/)
251
+ end
252
+
253
+ def extract_lines(start_line, end_line)
254
+ s_idx = [start_line - 1, 0].max
255
+ e_idx = [end_line - 1, lines.size - 1].min
256
+ lines[s_idx..e_idx].join
257
+ end
258
+ end
259
+ end
260
+ end