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,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Rubik
6
+ module Context
7
+ # Reads repository-level context that the Prompt layer would otherwise
8
+ # have to handle inline. Pulls filesystem reads out of ContextBuilder so
9
+ # the prompt layer is a pure data-transformation layer (no I/O).
10
+ #
11
+ # Usage:
12
+ # reader = Rubik::Context::RepoContextReader.new(root_dir: Dir.pwd)
13
+ # ctx = reader.read # => { protocol_xml:, workspace_summary:, git_branch: }
14
+ #
15
+ # All methods are safe to call on a non-git / empty directory; they
16
+ # return nil / empty values rather than raising.
17
+ class RepoContextReader
18
+ DEFAULT_PROTOCOL_CANDIDATES = [
19
+ "AGENTS.md",
20
+ ".rubik/AGENTS.md",
21
+ "RULES.md",
22
+ "CLAUDE.md",
23
+ ".cursorrules"
24
+ ].freeze
25
+
26
+ # Entry-point filenames that the workspace-summary considers worth
27
+ # surfacing when the file count is large.
28
+ ENTRY_POINT_BASENAMES = [
29
+ "Gemfile", "package.json", "Cargo.toml", "go.mod", "Makefile", "pyproject.toml", "Rakefile", "index.html", "README.md"
30
+ ].freeze
31
+
32
+ def initialize(root_dir: Dir.pwd, protocol_candidates: DEFAULT_PROTOCOL_CANDIDATES)
33
+ @root_dir = root_dir
34
+ @protocol_candidates = protocol_candidates
35
+ end
36
+
37
+ # Returns a Hash with three keys, each pre-extracted and pre-rendered
38
+ # so the prompt layer never has to touch the filesystem.
39
+ def read
40
+ {
41
+ protocol_xml: read_protocol_xml,
42
+ workspace_summary: read_workspace_summary,
43
+ git_branch: read_git_branch
44
+ }
45
+ end
46
+
47
+ # List of files that match the protocol-file candidates.
48
+ def find_protocol_files
49
+ @protocol_candidates.filter_map do |candidate|
50
+ full_path = File.join(@root_dir, candidate)
51
+ { path: candidate, full_path: full_path } if File.file?(full_path)
52
+ end
53
+ end
54
+
55
+ # Render the matched protocol files into an XML block.
56
+ def read_protocol_xml
57
+ files = find_protocol_files
58
+ return "" if files.empty?
59
+
60
+ files.map do |f|
61
+ content = Rubik::Platform.read_text(f[:full_path]).strip
62
+ <<~XML
63
+ <protocol source="#{f[:path]}">
64
+ #{content}
65
+ </protocol>
66
+ XML
67
+ end.join("\n").strip
68
+ end
69
+
70
+ # Returns a Hash describing the project layout. The prompt layer
71
+ # can decide how to render it (the existing `build_workspace_xml`
72
+ # in ContextBuilder does, and the new path can be passed in).
73
+ def read_workspace_summary
74
+ {
75
+ project_name: File.basename(@root_dir),
76
+ file_count: nil, # filled in by the caller's context_index
77
+ languages: [], # filled in by the caller's context_index
78
+ files: {} # filled in by the caller's context_index
79
+ }
80
+ end
81
+
82
+ # Read the current git branch from .git/HEAD. Returns nil if not a
83
+ # git repo; in detached-HEAD state returns the raw commit SHA.
84
+ def read_git_branch
85
+ git_dir = resolve_git_dir
86
+ return nil unless git_dir
87
+
88
+ head_file = File.join(git_dir, "HEAD")
89
+ return nil unless File.exist?(head_file)
90
+
91
+ content = Rubik::Platform.read_text(head_file).strip
92
+ return nil if content.empty?
93
+
94
+ # HEAD is either a ref like "ref: refs/heads/main" or a raw SHA
95
+ prefix = "ref: refs/heads/"
96
+ if content.start_with?(prefix)
97
+ content[prefix.length..]
98
+ else
99
+ # detached HEAD: return the raw SHA
100
+ content
101
+ end
102
+ end
103
+
104
+ private
105
+
106
+ # Worktrees/submodules store a `.git` FILE: "gitdir: <path>" — HEAD
107
+ # lives in that directory. Plain clones have a .git directory.
108
+ def resolve_git_dir
109
+ dot_git = File.join(@root_dir, ".git")
110
+ if File.directory?(dot_git)
111
+ dot_git
112
+ elsif File.file?(dot_git)
113
+ target = Rubik::Platform.read_text(dot_git).strip.delete_prefix("gitdir:").strip
114
+ File.expand_path(target, @root_dir) unless target.empty?
115
+ end
116
+ rescue SystemCallError, ArgumentError
117
+ nil
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "pathname"
5
+ require_relative "../platform"
6
+ require_relative "git_lister"
7
+
8
+ module Rubik
9
+ module Context
10
+ class Scanner
11
+ EXTENSION_MAP = {
12
+ ".rb" => "ruby",
13
+ ".py" => "python",
14
+ ".go" => "go",
15
+ ".rs" => "rust",
16
+ ".c" => "c",
17
+ ".h" => "c",
18
+ ".cpp" => "cpp",
19
+ ".hpp" => "cpp",
20
+ ".cc" => "cpp",
21
+ ".zig" => "zig",
22
+ ".js" => "javascript",
23
+ ".jsx" => "javascript",
24
+ ".ts" => "typescript",
25
+ ".tsx" => "typescript",
26
+ ".html" => "html",
27
+ ".htm" => "html",
28
+ ".css" => "css",
29
+ ".json" => "json",
30
+ ".yaml" => "yaml",
31
+ ".yml" => "yaml",
32
+ ".md" => "markdown",
33
+ ".sh" => "shell",
34
+ ".java" => "java",
35
+ ".kt" => "kotlin",
36
+ ".swift" => "swift"
37
+ }.freeze
38
+
39
+ BUILD_SIGNATURES = {
40
+ "Gemfile" => "ruby",
41
+ "pyproject.toml" => "python",
42
+ "go.mod" => "go",
43
+ "Cargo.toml" => "rust",
44
+ "Makefile" => "c",
45
+ "build.zig" => "zig",
46
+ "package.json" => "javascript"
47
+ }.freeze
48
+
49
+ DEFAULT_IGNORES = [
50
+ ".rubik/**",
51
+ ".git/**",
52
+ # GitNexus parse cache: thousands of binary index files; indexing
53
+ # them is pure cost and their non-UTF-8 bytes are a grep hazard.
54
+ ".gitnexus/**",
55
+ ".ua/**",
56
+ ".trash*/**",
57
+ "**/.trash*/**",
58
+ ".turbo/**",
59
+ ".sst/**",
60
+ ".next/**",
61
+ ".nuxt/**",
62
+ "node_modules/**",
63
+ "vendor/**",
64
+ "tmp/**",
65
+ "dist/**",
66
+ "build/**",
67
+ ".cache/**",
68
+ "coverage/**"
69
+ ].freeze
70
+
71
+ # Subtrees whose ignore pattern is "<path>/**" (optionally with a
72
+ # "**/" prefix) are pruned whole when the stem's components occur
73
+ # contiguously in a directory's path. NOTE: this is intentionally
74
+ # STRONGER than the legacy per-file filter — Ruby's fnmatch treats a
75
+ # trailing `**` as a single segment, so patterns like "node_modules/**"
76
+ # historically rejected only first-level entries while deeper files
77
+ # leaked into the index. The walker applies the pattern's plain meaning
78
+ # (whole subtree ignored), matching real gitignore semantics.
79
+ PRUNABLE_PATTERN = %r{\A(?:\*\*/)?(.+)/\*\*\z}
80
+ # Also hard-rejected inside `ignored?`; pruned here before descending.
81
+ HARD_PRUNE_ROOTS = [".git", ".rubik", ".ua", ".turbo", ".sst", ".next", ".nuxt"].freeze
82
+
83
+ attr_reader :root_dir, :additional_roots
84
+ attr_accessor :custom_includes, :custom_excludes
85
+
86
+ def initialize(root_dir: Dir.pwd, additional_roots: [], includes: [], excludes: [], native: nil)
87
+ @root_dir = File.expand_path(root_dir)
88
+ @additional_roots = (additional_roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
89
+ @custom_includes = includes || []
90
+ @custom_excludes = excludes || []
91
+ @native = native
92
+ end
93
+
94
+ def additional_roots=(roots)
95
+ @additional_roots = (roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
96
+ end
97
+
98
+ # Builds the file info index. Two listing strategies per root:
99
+ # - `git ls-files` fast path (when enabled and git answers): C-speed
100
+ # walk + gitignore handling; entries still pass the Ruby ignore
101
+ # filters (DEFAULT_IGNORES may cover paths absent from the target
102
+ # repo's .gitignore) and are ordered exactly like the walker.
103
+ # - the pruned recursive walker as fallback.
104
+ # Pass a previous scan result (rel_path => info hash) to enable the
105
+ # stat-dirty model: stat-clean entries reuse the previous hash (which
106
+ # may itself be nil — content never hashed), only stat-changed files
107
+ # are read and re-hashed. Cold entries start hash-less.
108
+ def scan(previous = {})
109
+ files = {}
110
+
111
+ # 1. Scan primary root
112
+ primary_ignores = DEFAULT_IGNORES + parse_gitignore(@root_dir) + @custom_excludes
113
+ files.merge!(scan_root(@root_dir, ".", primary_ignores, previous))
114
+
115
+ # 2. Scan additional roots
116
+ @additional_roots.each do |extra_root|
117
+ next unless File.directory?(extra_root)
118
+
119
+ extra_ignores = DEFAULT_IGNORES + parse_gitignore(extra_root) + @custom_excludes
120
+ files.merge!(scan_root(extra_root, relative_prefix(extra_root), extra_ignores, previous))
121
+ end
122
+
123
+ files
124
+ rescue StandardError => e
125
+ Rubik::Log.warn(:context, "context scan aborted partway (#{e.class}: #{e.message}); returning partial index")
126
+ files || {}
127
+ end
128
+
129
+ def detect_languages(files_hash)
130
+ languages = Set.new
131
+
132
+ files_hash.each_value do |info|
133
+ languages << info[:language] if info[:language]
134
+ end
135
+
136
+ BUILD_SIGNATURES.each do |build_file, lang|
137
+ languages << lang if File.exist?(File.join(@root_dir, build_file))
138
+ @additional_roots.each do |extra_root|
139
+ languages << lang if File.exist?(File.join(extra_root, build_file))
140
+ end
141
+ end
142
+
143
+ languages.to_a.sort
144
+ end
145
+
146
+ # Public single-file probe. Delegates to entry_for so the persisted
147
+ # info (notably the microsecond mtime) stays unit-identical with scan
148
+ # results — mixed units would make incremental_scan flag every
149
+ # update_file-touched entry as modified. The probe always computes a
150
+ # real content hash: its caller (Index#update_file) compares hashes to
151
+ # decide reindexing, and a lazy nil would mask every change.
152
+ def inspect_file(rel_path, full_path)
153
+ entry_for(rel_path, full_path, {}, force_hash: true)
154
+ end
155
+
156
+ private
157
+
158
+ # Stat-dirty model (à la git): a previous entry whose size and mtime
159
+ # (microsecond precision) both match is NEVER read from disk — its
160
+ # content hash is reused verbatim, including when it is nil (content
161
+ # never hashed). Only a stat change triggers a read + SHA-256, and a
162
+ # cold entry (no previous) starts hash-less. force_hash: true computes
163
+ # a real hash regardless (single-file probes that must detect content
164
+ # changes). Residual race — a same-size, same-microsecond-mtime rewrite
165
+ # between scans — is the deliberate tradeoff the git model makes.
166
+ def entry_for(rel_path, full_path, previous, stat: nil, force_hash: false)
167
+ stat ||= File.stat(full_path)
168
+ ext = File.extname(full_path)
169
+ lang = EXTENSION_MAP[ext] || BUILD_SIGNATURES[File.basename(full_path)]
170
+ mtime_us = mtime_micros(stat)
171
+
172
+ prev = previous.is_a?(Hash) ? previous[rel_path] : nil
173
+ # Cold entry (no previous): content unknown — defer the read until a
174
+ # future scan observes a stat change (git's stat-dirty model), so the
175
+ # implicit else is nil.
176
+ hash =
177
+ if !force_hash && prev.is_a?(Hash) &&
178
+ prev[:size] == stat.size && prev[:mtime] == mtime_us
179
+ # Stat-clean: reuse verbatim — including a nil (never hashed).
180
+ prev[:hash]
181
+ elsif force_hash || prev.is_a?(Hash)
182
+ # Stat-dirty (or a probe that must see content): hash eagerly.
183
+ content = begin
184
+ Rubik::Platform.read_text(full_path)
185
+ rescue Errno::ENOENT, Errno::EACCES
186
+ ""
187
+ end
188
+ Digest::SHA256.hexdigest(content)
189
+ end
190
+
191
+ {
192
+ size: stat.size,
193
+ mtime: mtime_us,
194
+ hash: hash,
195
+ language: lang
196
+ }
197
+ end
198
+
199
+ def mtime_micros(stat)
200
+ (stat.mtime.to_r * 1_000_000).to_i
201
+ end
202
+
203
+ def walk_dir(base_dir, rel_prefix, key_prefix, ignore_patterns, previous, needles)
204
+ current = rel_prefix.empty? ? base_dir : "#{base_dir}/#{rel_prefix}"
205
+ out = {}
206
+
207
+ # Sorted like the historical recursive Dir.glob walk so that index
208
+ # insertion order (and anything downstream relying on it, e.g.
209
+ # workspace_xml entry_points listing) stays deterministic.
210
+ entries = begin
211
+ Dir.foreach(current).sort - [".", ".."]
212
+ rescue SystemCallError
213
+ []
214
+ end
215
+
216
+ entries.each do |entry|
217
+ entry = entry.tr("\\", "/")
218
+ rel = rel_prefix.empty? ? entry : "#{rel_prefix}/#{entry}"
219
+
220
+ begin
221
+ lst = File.lstat("#{base_dir}/#{rel}")
222
+ rescue Errno::ENOENT
223
+ next
224
+ end
225
+
226
+ if lst.directory? && !lst.symlink?
227
+ next if subtree_ignored?(rel, needles)
228
+
229
+ out.merge!(walk_dir(base_dir, rel, key_prefix, ignore_patterns, previous, needles))
230
+ else
231
+ # Symlinked FILES are still listed (File.stat follows the link),
232
+ # matching the old Dir.glob behavior; only directory descent is
233
+ # withheld — glob never followed dir symlinks either, and doing
234
+ # so would allow root escapes and cyclic recursion.
235
+ st = begin
236
+ File.stat("#{base_dir}/#{rel}")
237
+ rescue Errno::ENOENT, Errno::EACCES
238
+ nil
239
+ end
240
+ next unless st&.file?
241
+ next if ignored?(rel, ignore_patterns)
242
+ next unless included?(rel, @custom_includes)
243
+
244
+ rel_key = key_prefix == "." ? rel : "#{key_prefix}/#{rel}"
245
+ out[rel_key] = entry_for(rel_key, "#{base_dir}/#{rel}", previous, stat: st)
246
+ end
247
+ end
248
+
249
+ out
250
+ rescue StandardError => e
251
+ Rubik::Log.warn(:context, "directory walk failed under #{base_dir} (#{e.class}: #{e.message}); keeping partial results")
252
+ out
253
+ end
254
+
255
+ def subtree_ignored?(dir_rel, needles)
256
+ comps = dir_rel.split("/")
257
+ return true if HARD_PRUNE_ROOTS.include?(comps.first)
258
+
259
+ needles.any? { |needle| contiguous_components?(comps, needle) }
260
+ end
261
+
262
+ # "<dir>/**" ignore patterns (see PRUNABLE_PATTERN) pre-split into
263
+ # needle component arrays, once per scan root. Matching a directory
264
+ # then costs plain array compares — no per-ancestor regex evaluation
265
+ # and no re-splitting of the pattern's stem for every file visited.
266
+ def prunable_needles(ignore_patterns)
267
+ ignore_patterns.filter_map do |pattern|
268
+ next unless (m = PRUNABLE_PATTERN.match(pattern))
269
+
270
+ m[1].split("/")
271
+ end
272
+ end
273
+
274
+ def contiguous_components?(haystack, needle)
275
+ return true if haystack == needle
276
+ return false if haystack.size < needle.size
277
+
278
+ (0..(haystack.size - needle.size)).any? do |offset|
279
+ haystack[offset, needle.size] == needle
280
+ end
281
+ end
282
+
283
+ # Key prefix for files found under an additional root, mirroring the
284
+ # historical layout: subtree roots get their path relative to the main
285
+ # root; unrelated roots fall back to their absolute path.
286
+ def relative_prefix(dir)
287
+ Pathname.new(dir).relative_path_from(Pathname.new(@root_dir)).to_s.gsub("\\", "/")
288
+ rescue ArgumentError
289
+ dir.gsub("\\", "/")
290
+ end
291
+
292
+ # One recursive directory walk with EARLY SUBTREE PRUNING: instead of
293
+ # enumerating ignored trees (.git/, node_modules/, vendor/ – thousands
294
+ # of entries on real projects) only to reject each file afterwards,
295
+ # descent into a matching subtree never happens. Pruning applies the
296
+ # ignore pattern's plain whole-subtree meaning (see PRUNABLE_PATTERN)
297
+ # and `ignored?` still filters every file that IS visited. `prefix` is
298
+ # the key namespace for this root ("." for the primary project root).
299
+ def scan_root(base_dir, prefix, ignore_patterns, previous)
300
+ needles = prunable_needles(ignore_patterns)
301
+ if git_listing? && @custom_includes.empty? && (listed = GitLister.files(base_dir))
302
+ return index_listed(base_dir, prefix, listed, ignore_patterns, previous, needles)
303
+ end
304
+
305
+ walk_dir(base_dir, "", prefix, ignore_patterns, previous, needles)
306
+ rescue StandardError => e
307
+ Rubik::Log.warn(:context, "root scan failed for #{base_dir} (#{e.class}: #{e.message}); root omitted from index")
308
+ {}
309
+ end
310
+
311
+ # Fast path over a `git ls-files` listing. The listing is stat-filled,
312
+ # re-filtered through the Ruby-side ignore rules (ls-files only knows
313
+ # the repo's own gitignore: DEFAULT_IGNORES subtrees may be missing
314
+ # from it, and tracked files ignore gitignore entirely), ordered
315
+ # identically to walk_dir's depth-first-per-directory sort, and
316
+ # ancestor-pruned with the same whole-subtree semantics as the walker.
317
+ # Residual drift the OTHER way: gitignore features the walker's crude
318
+ # fnmatch translation lacks (negations, anchored patterns) can make the
319
+ # fast path list fewer files.
320
+ def index_listed(base_dir, key_prefix, rel_paths, ignore_patterns, previous, needles)
321
+ out = {}
322
+ # Verdict per ancestor directory, computed on first sight: walker
323
+ # order keeps siblings adjacent, so each distinct directory's ignore
324
+ # status is evaluated once per scan instead of once per file per
325
+ # depth. subtree_ignored? is a pure function of (dir, needles), so
326
+ # the memo is exact.
327
+ dir_verdicts = Hash.new { |h, dir| h[dir] = subtree_ignored?(dir, needles) }
328
+ order_like_walker(rel_paths).each do |rel|
329
+ # PERF: zero-allocation ancestor check using string slicing instead of Array#split and Array#join
330
+ ignored_dir = false
331
+ idx = 0
332
+ while (idx = rel.index("/", idx))
333
+ if dir_verdicts[rel[0, idx]]
334
+ ignored_dir = true
335
+ break
336
+ end
337
+ idx += 1
338
+ end
339
+ next if ignored_dir
340
+ next if ignored?(rel, ignore_patterns)
341
+ next unless included?(rel, @custom_includes)
342
+
343
+ full = "#{base_dir}/#{rel}"
344
+ st = begin
345
+ File.stat(full)
346
+ rescue Errno::ENOENT, Errno::EACCES
347
+ nil
348
+ end
349
+ next unless st&.file?
350
+
351
+ rel_key = key_prefix == "." ? rel : "#{key_prefix}/#{rel}"
352
+ out[rel_key] = entry_for(rel_key, full, previous, stat: st)
353
+ end
354
+ out
355
+ rescue StandardError => e
356
+ Rubik::Log.warn(:context, "git ls-files fast path failed for #{base_dir} (#{e.class}: #{e.message}); keeping partial index")
357
+ out
358
+ end
359
+
360
+ def git_listing?
361
+ GitLister.enabled?(@native)
362
+ end
363
+
364
+ # Sorts flat relative paths into the exact emission order of walk_dir:
365
+ # every level name-sorts its entries and a directory descends in place,
366
+ # so whole subtrees interleave positionally — which is precisely an
367
+ # element-wise compare of the component arrays ("a/x" before "a.txt",
368
+ # since "a" < "a.txt" at the first component; "lib/deep/core.rb" before
369
+ # "lib/util.rb" via "deep" < "util").
370
+ def order_like_walker(paths)
371
+ paths.sort_by { |p| p.split("/") }
372
+ end
373
+
374
+ def parse_gitignore(dir = @root_dir)
375
+ gitignore_path = File.join(dir, ".gitignore")
376
+ return [] unless File.exist?(gitignore_path)
377
+
378
+ patterns = File.readlines(gitignore_path, chomp: true).map(&:strip).reject do |line|
379
+ line.empty? || line.start_with?("#")
380
+ end
381
+ patterns.map do |pattern|
382
+ pattern.end_with?("/") ? "#{pattern}**" : pattern
383
+ end
384
+ rescue Errno::ENOENT, Errno::EACCES
385
+ []
386
+ end
387
+
388
+ def ignored?(path, ignore_patterns)
389
+ return true if path == ".rubik" || path.start_with?(".rubik/")
390
+ return true if path == ".git" || path.start_with?(".git/")
391
+
392
+ # PERF: precompile simple globs to memoized Regexp objects to avoid
393
+ # repeated File.fnmatch? overhead across thousands of files; exotic
394
+ # globs keep the fnmatch semantics untouched.
395
+ compile_ignore_regexes(ignore_patterns).any? { |r| pattern_match?(r, path) }
396
+ end
397
+
398
+ # Translates a glob into a Regexp only where the mapping is exact: no
399
+ # character classes, no leading-slash anchoring, and `**` only ever a
400
+ # whole path segment (leading "**/", mid "/**/", trailing "/**").
401
+ # Anything else returns nil and pattern_match? falls back to
402
+ # File.fnmatch?, whose segment rules the Regexp form cannot express
403
+ # (fnmatch "a**b" never crosses "/" and "**/x" also matches bare "x").
404
+ def compile_ignore_regexes(ignore_patterns)
405
+ @compiled_ignore_regexes ||= {}
406
+ @compiled_ignore_regexes[ignore_patterns] ||= ignore_patterns.map do |pat|
407
+ simple_glob_regex(pat) || pat
408
+ end
409
+ end
410
+
411
+ def simple_glob_regex(pat)
412
+ return nil if pat.empty? || pat.start_with?("/") || pat.include?("[")
413
+ return nil unless pat.split("/").all? { |seg| !seg.include?("*") || seg == "**" || !seg.include?("**") }
414
+
415
+ src = Regexp.escape(pat)
416
+ src = src.sub(%r{\A\\\*\\\*/}, "") # leading "**/": the (?:.*/)? wrapper matches zero dirs too
417
+ src = src.gsub("/\\*\\*/", "/(?:.*/)?") # mid "/**/": zero or more directories
418
+ src = src.gsub("\\*\\*", ".*") # trailing "/**" or lone "**"
419
+ src = src.gsub("\\*", "[^/]*").gsub("\\?", "[^/]")
420
+ Regexp.new("\\A(?:.*/)?#{src}\\z")
421
+ end
422
+
423
+ def pattern_match?(pattern_or_regex, path)
424
+ if pattern_or_regex.is_a?(Regexp)
425
+ pattern_or_regex.match?(path)
426
+ else
427
+ File.fnmatch?(pattern_or_regex, path, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
428
+ File.fnmatch?("**/#{pattern_or_regex}", path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
429
+ end
430
+ end
431
+
432
+ def included?(path, patterns)
433
+ return true if patterns.nil? || patterns.empty?
434
+
435
+ patterns.any? { |pattern| pattern_match?(pattern, path) }
436
+ end
437
+ end
438
+ end
439
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "context/scanner"
4
+ require_relative "context/index"
5
+ require_relative "context/git_info"
6
+ require_relative "context/repo_context_reader"
7
+
8
+ module Rubik
9
+ module Context
10
+ end
11
+ end