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,514 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "digest"
5
+ require "monitor"
6
+ require "time"
7
+ require "fileutils"
8
+ require_relative "../platform"
9
+ require_relative "../uris"
10
+ require_relative "layout"
11
+ require_relative "folding"
12
+
13
+ module Rubik
14
+ module Storage
15
+ # Merkezi sanal FS-DB deposu: rubik://storage/<project>/<type>/<id>
16
+ # adreslenebilir varlıklar (goal/plan/memory/wiki/transcript/review/note).
17
+ #
18
+ # id => "plan.3.9f86d081884c7d21" — tip + satır (row) + OLUŞTURMA
19
+ # anındaki içerik karmasının ilk 16 hexasi. Güncelleme id'yi
20
+ # korur (refs/linkler kopmaz), "hash" alanı güncel içeriği
21
+ # izler; row asla yeniden kullanılmaz.
22
+ # fiziki => <root>/<project>/store/{meta.json,<type>.jsonl}
23
+ # indeks => <root>/index.json (merkezi; proje sayıları + zaman damgaları)
24
+ #
25
+ # Tek kullanıcı CLI ölçeği: süreç içi Monitor ile thread-güvenli; çapraz
26
+ # süreç yazımlarında meta/index last-writer-wins (belgelenmiş sınırlama).
27
+ # Yazmalar Platform.atomic_write ile tmp+rename; kurulum tembeldir —
28
+ # ilk mutasyona kadar hiçbir dizin oluşturulmaz (standalone/test
29
+ # kurulumu dosya sistemine dokunmaz).
30
+ class Store
31
+ TYPES = ["goal", "plan", "memory", "wiki", "transcript", "review", "note"].freeze
32
+ ID_PATTERN = /\A([a-z]+)\.(\d+)\.([0-9a-f]{16})\z/
33
+ HASH_PREFIX_MIN = 8
34
+ REF_PATTERN = /\A[a-zA-Z0-9._-]+\z/
35
+ # BM25-lite (vakanüvis-v2 §4-V8): klasik Okapi değerleri — k1 terim
36
+ # doygunluğu, b uzunluk normalizasyonu ağırlığı.
37
+ BM25_K1 = 1.2
38
+ BM25_B = 0.75
39
+ # Alan kademeli bonus (issue #99 P3): BM25 birincil kalır; başlıkta
40
+ # geçen sorgu terimi etikette geçene, o da yalnız gövdede geçene
41
+ # üstünlük kurar. Terim başına birimdir ve tipik idf katkısının
42
+ # altındadır — maddesel BM25 farklarını ezmez, sadece eşit-yakın
43
+ # skorları alan bilgisiyle kırar.
44
+ TITLE_TIER_BONUS = 1.0
45
+ TAGS_TIER_BONUS = 0.5
46
+
47
+ Summary = Struct.new(:id, :row, :title, :status, :updated_at, :tags, keyword_init: true)
48
+
49
+ attr_reader :root, :project
50
+
51
+ def initialize(project:, root: Layout.storage_root)
52
+ @root = File.expand_path(root.to_s)
53
+ @project = Layout.slugify(project)
54
+ @monitor = Monitor.new
55
+ # Parse cache (vakanuvis-v2 §4-V8): tip => {mtime:, records:}. mtime
56
+ # doğrulaması hem yazı sonrası tazeliği hem dış müdahaleyi kapatır.
57
+ # Dönen dizi PAYLAŞILIR ve FROZEN (arch-deepening 2026-10-08 #10):
58
+ # dondurma cache dolumunda yapılır — "arayan mutasyon yapmaz" sözü
59
+ # artık yorum değil, FrozenError (put/list zaten kopya üzerinde
60
+ # çalışır).
61
+ @records_cache = {}
62
+ end
63
+
64
+ def project_dir
65
+ Layout.project_dir(@root, @project)
66
+ end
67
+
68
+ def uri_for(type, id)
69
+ Rubik::Uris.storage_entity(project: @project, type: type, id: id)
70
+ end
71
+
72
+ # Oluşturur veya günceller. id verilirse o kayıt (satır adresli)
73
+ # güncellenir — version artar, hash yeniden hesaplanır, id sabit kalır.
74
+ # ref verilirse alias o kaydın id'sine bağlanır.
75
+ def put(type:, title:, body: "", tags: [], links: {}, status: "active", id: nil, ref: nil)
76
+ type = type.to_s.downcase
77
+ raise ArgumentError, "unknown entity type '#{type}' (valid: #{TYPES.join(', ')})" unless TYPES.include?(type)
78
+ raise ArgumentError, "title is required" if title.to_s.strip.empty?
79
+
80
+ now = Time.now.utc.iso8601
81
+ @monitor.synchronize do
82
+ meta = load_meta
83
+ records = load_records(type)
84
+ existing = id && resolve_record(records, id.to_s, meta)
85
+ raise ArgumentError, "entity '#{id}' not found in #{type}" if id && !existing
86
+
87
+ record =
88
+ if existing
89
+ existing.merge(
90
+ "title" => title.to_s, "body" => body.to_s, "tags" => normalize_tags(tags),
91
+ "links" => normalize_links(links), "status" => status.to_s,
92
+ "hash" => content_hash(type, title, body, tags, links),
93
+ "version" => existing["version"].to_i + 1, "updated_at" => now
94
+ )
95
+ else
96
+ row = meta_next_row(meta, type)
97
+ hash = content_hash(type, title, body, tags, links)
98
+ { "id" => "#{type}.#{row}.#{hash[0, 16]}", "type" => type, "project" => @project,
99
+ "row" => row, "hash" => hash, "title" => title.to_s, "body" => body.to_s,
100
+ "tags" => normalize_tags(tags), "links" => normalize_links(links),
101
+ "status" => status.to_s, "version" => 1, "created_at" => now, "updated_at" => now }
102
+ end
103
+
104
+ records = records.reject { |r| r["row"] == record["row"] } << record
105
+ ensure_project_dirs
106
+ write_records(type, records.sort_by { |r| r["row"].to_i })
107
+ meta_bump_counts(meta, type) unless existing
108
+ meta_set_ref(meta, ref, record["id"]) if ref
109
+ save_meta(meta)
110
+ touch_index(meta)
111
+ record.dup
112
+ end
113
+ end
114
+
115
+ # Çözüm sırası: tam id ("plan.3.<hash16>") → "plan.3" (tip.satır) →
116
+ # salt satır ("3") → karma öneki (>= 8 hex) → alias (refs). Bulunamazsa
117
+ # nil — dürüst not-found arayan katmanda.
118
+ def get(type:, id:)
119
+ type = type.to_s.downcase
120
+ return nil unless TYPES.include?(type)
121
+
122
+ @monitor.synchronize do
123
+ resolve_record(load_records(type), id.to_s, load_meta)
124
+ end
125
+ end
126
+
127
+ def list(type: nil)
128
+ @monitor.synchronize do
129
+ resolve_types(type).flat_map { |t| load_records(t) }
130
+ .sort_by { |r| [r["type"].to_s, r["row"].to_i] }
131
+ .map { |r| summary_of(r) }
132
+ end
133
+ end
134
+
135
+ def search(query:, type: nil)
136
+ # Token-AND: her boşlukla ayrılmış terim, kaydın title/body/tags
137
+ # alanlarından en az birinde geçmeli. Tüm sorgu tek substring olarak
138
+ # arandığında "ratelimit-lock regression 11/11" gibi doğal çok
139
+ # kelimeli sorgular kelimesi kelimesine eşleşmediği için 0 sonuç
140
+ # dönüyordu (js-tangram olay raporu 2026-10-04, olay #2 — "indeks
141
+ # bayatlığı" değil, eşleştirme semantiğiydi). Tek terimli sorgular
142
+ # eski davranışın birebir aynısı.
143
+ tokens = Folding.fold(query).split
144
+ return [] if tokens.empty?
145
+
146
+ @monitor.synchronize do
147
+ matched = resolve_types(type).flat_map do |t|
148
+ records = load_records(t)
149
+ haystacks = haystacks_for(t, records)
150
+ records.each_index.select { |i| tokens.all? { |token| haystacks[i].include?(token) } }
151
+ .map { |i| records[i] }
152
+ end
153
+ matched.sort_by { |r| [r["type"].to_s, r["row"].to_i] }
154
+ end
155
+ end
156
+
157
+ # BM25-lite ilgi sıralaması (vakanuvis-v2 §4-V8, Aşama 1): aday üretim
158
+ # search ile AYNI token-AND eşiğidir; eşleşenler Okapi skoruna göre
159
+ # sıralanır. Ek maliyet disiplini: df TEK include? geçişinden (AND
160
+ # filtresinin kendisinden) gelir, uzunluk normalizasyonu bytesize (O(1))
161
+ # üzerinden, tf yalnız eşleşen kayıtlarda scan edilir. Eşitlik bozma
162
+ # updated_at desc → [type,row] ⇒ aynı depo ⇒ aynı sıra (replay/pin).
163
+ # Alan kademeli bonus (issue #99 P3): skora terim başına +1.0 başlık
164
+ # / +0.5 etiket izi eklenir — BM25 birincil kalmaya devam eder, bonus
165
+ # yalnız alan-tierini kırar (belgelenmiş sözleşme değişikliği).
166
+ # `search`'ün [type,row] sözleşmesi bilinçli olarak değişmez —
167
+ # sıralama politikası yalnız recall sunum katmanının talebidir.
168
+ # require_all (issue #99 P2): false iken aday eşiği OR'a gevşer
169
+ # (herhangi bir terimi taşıyan kayıt) — fallback recall içindir;
170
+ # skorlama iki ducte de aynıdır, default davranış birebir korunur.
171
+ def search_ranked(query:, type: nil, require_all: true)
172
+ tokens = Folding.fold(query).split
173
+ return [] if tokens.empty?
174
+
175
+ @monitor.synchronize do
176
+ corpus = resolve_types(type).filter_map do |t|
177
+ records = load_records(t)
178
+ next if records.empty?
179
+
180
+ haystacks = haystacks_for(t, records)
181
+ { records: records, haystacks: haystacks,
182
+ has: tokens.map { |token| haystacks.map { |h| h.include?(token) } } }
183
+ end
184
+ return [] if corpus.empty?
185
+
186
+ total = corpus.sum { |c| c[:records].size }
187
+ avgdl = corpus.sum { |c| c[:haystacks].sum(&:bytesize) }.fdiv(total)
188
+ dfs = tokens.each_index.map { |ti| corpus.sum { |c| c[:has][ti].count(true) } }
189
+
190
+ gate = ranked_gate(require_all)
191
+ scored = corpus.flat_map do |c|
192
+ c[:records].each_index.select { |i| gate.call(c[:has], i) }.map do |i|
193
+ score = tokens.each_with_index.sum do |token, ti|
194
+ df = dfs[ti]
195
+ next 0.0 if df.zero?
196
+
197
+ tf = c[:haystacks][i].scan(token).size
198
+ next 0.0 if tf.zero?
199
+
200
+ idf = Math.log(((total - df + 0.5) / (df + 0.5)) + 1)
201
+ dl = c[:haystacks][i].bytesize
202
+ idf * (tf * (BM25_K1 + 1)) / (tf + (BM25_K1 * (1 - BM25_B + (BM25_B * dl / avgdl))))
203
+ end
204
+ [score + field_tier_bonus(tokens, c[:records][i]), c[:records][i]]
205
+ end
206
+ end
207
+ ordered = scored.sort do |(score_a, rec_a), (score_b, rec_b)|
208
+ (score_b <=> score_a).nonzero? ||
209
+ (rec_b["updated_at"].to_s <=> rec_a["updated_at"].to_s).nonzero? ||
210
+ (rec_a["type"].to_s <=> rec_b["type"].to_s).nonzero? ||
211
+ (rec_a["row"].to_i <=> rec_b["row"].to_i)
212
+ end
213
+ ordered.map { |_score, record| record }
214
+ end
215
+ end
216
+
217
+ # Aday eşiği: require_all (AND — default, recall) ya da OR
218
+ # gevşetmesi (fallback — issue #99 P2).
219
+ def ranked_gate(require_all)
220
+ if require_all
221
+ ->(has, i) { has.all? { |row| row[i] } }
222
+ else
223
+ ->(has, i) { has.any? { |row| row[i] } }
224
+ end
225
+ end
226
+
227
+ # Terim başına alan izi: katlanmış başlıkta geçen terim +1.0,
228
+ # yalnız katlanmış etiketlerde geçen +0.5, gövde izi 0 — bkz.
229
+ # TITLE_TIER_BONUS/TAGS_TIER_BONUS. Yalnız AND eşiğini geçen
230
+ # kayıtlar için hesaplanır (ek tarama yok).
231
+ def field_tier_bonus(tokens, record)
232
+ title = Folding.fold(record["title"])
233
+ tags = Folding.fold(Array(record["tags"]).join("\n"))
234
+ tokens.sum do |token|
235
+ if title.include?(token)
236
+ TITLE_TIER_BONUS
237
+ elsif tags.include?(token)
238
+ TAGS_TIER_BONUS
239
+ else
240
+ 0.0
241
+ end
242
+ end
243
+ end
244
+
245
+ def set_ref(name, id)
246
+ name = name.to_s
247
+ raise ArgumentError, "invalid ref name '#{name}'" unless name.match?(REF_PATTERN)
248
+
249
+ @monitor.synchronize do
250
+ meta = load_meta
251
+ target = TYPES.lazy.map { |t| resolve_record(load_records(t), id.to_s, meta) }
252
+ .find(&:itself)
253
+ raise ArgumentError, "entity '#{id}' not found in any type" unless target
254
+
255
+ meta_set_ref(meta, name, target["id"])
256
+ ensure_project_dirs
257
+ save_meta(meta)
258
+ { "ref" => name, "id" => target["id"], "uri" => uri_for(target["type"], target["id"]) }
259
+ end
260
+ end
261
+
262
+ def refs
263
+ @monitor.synchronize do
264
+ load_meta.fetch("refs", {}).dup
265
+ end
266
+ end
267
+
268
+ def delete(type:, id:)
269
+ type = type.to_s.downcase
270
+ return nil unless TYPES.include?(type)
271
+
272
+ @monitor.synchronize do
273
+ meta = load_meta
274
+ records = load_records(type)
275
+ target = resolve_record(records, id.to_s, meta)
276
+ return nil unless target
277
+
278
+ write_records(type, records.reject { |r| r["row"] == target["row"] })
279
+ meta["counts"] = (meta["counts"] || {}).then { |c| c.merge(type => [c[type].to_i - 1, 0].max) }
280
+ meta["refs"] = meta.fetch("refs", {}).reject { |_k, v| v == target["id"] }
281
+ save_meta(meta)
282
+ touch_index(meta)
283
+ target
284
+ end
285
+ end
286
+
287
+ # Proje özeti: tip başına sayı + son yazım zamanı.
288
+ def project_summary
289
+ @monitor.synchronize do
290
+ meta = load_meta
291
+ { "project" => @project, "path" => project_dir,
292
+ "entity_counts" => meta.fetch("counts", {}).dup,
293
+ "created_at" => meta["created_at"], "updated_at" => meta["updated_at"],
294
+ "refs" => meta.fetch("refs", {}).dup }
295
+ end
296
+ end
297
+
298
+ # Merkezi indeks anlık görüntüsü (sınıf metodu — kök altındaki tüm
299
+ # projeler). Disk okunur; eksikse boş iskelet.
300
+ def self.index_snapshot(root)
301
+ path = Layout.index_path(File.expand_path(root.to_s))
302
+ return { "version" => 1, "updated_at" => nil, "projects" => {} } unless File.exist?(path)
303
+
304
+ raw = JSON.parse(File.read(path, encoding: "UTF-8").scrub)
305
+ raw.merge("projects" => raw.fetch("projects", {}))
306
+ rescue JSON::ParserError, Errno::ENOENT
307
+ { "version" => 1, "updated_at" => nil, "projects" => {} }
308
+ end
309
+
310
+ def index_snapshot
311
+ self.class.index_snapshot(@root)
312
+ end
313
+
314
+ private
315
+
316
+ def summary_of(record)
317
+ Summary.new(id: record["id"], row: record["row"].to_i, title: record["title"].to_s,
318
+ status: record["status"].to_s, updated_at: record["updated_at"].to_s,
319
+ tags: Array(record["tags"]).dup)
320
+ end
321
+
322
+ def resolve_record(records, selector, meta)
323
+ selector = selector.to_s.strip
324
+ return nil if selector.empty?
325
+
326
+ # Tam id içerik adreslemesidir: birebir eşleşme (bayat hash → not
327
+ # found; satır kısa formu ayrı dalda ele alınır).
328
+ return records.find { |r| r["id"] == selector } if selector.match?(ID_PATTERN)
329
+
330
+ # tip.satır formları alias'tan ÖNCE denir; kronikçi alias'ları bu
331
+ # şekle asla düşmez — tek Garantör: Vakanuvis::Vocabulary
332
+ # (rakam koruması, slug mint'inde).
333
+ if (m = selector.match(/\A([a-z]+)\.(\d+)\z/))
334
+ type = m[1]
335
+ row = m[2].to_i
336
+ return records.find { |r| r["type"] == type && r["row"] == row }
337
+ end
338
+ return records.find { |r| r["row"].to_s == selector } if selector.match?(/\A\d+\z/)
339
+ return records.find { |r| r["hash"].to_s.start_with?(selector.downcase) } if selector.match?(/\A[0-9a-f]{#{HASH_PREFIX_MIN},64}\z/i)
340
+
341
+ # Son şans: alias — refs üzerinden tek sekme. Eski depolarda
342
+ # transcript alias'ı ÇİFT önekle basılmıştı ("subagent-subagent-..."):
343
+ # doğal form ("subagent-1-...") gelirse öneki ekleyip bir kez daha
344
+ # bakarız — böylece completion-time transcript upsert'i eski satırı
345
+ # bulur, kopya varlık açılmaz (uris.rb transcript_ref düzeltmesiyle
346
+ # birlikte; js-tangram olay raporu 2026-10-04, olay #4).
347
+ refs = meta.fetch("refs", {})
348
+ resolved_id = refs[selector]
349
+ resolved_id = refs["#{Rubik::Uris::TRANSCRIPT_REF_PREFIX}#{selector}"] if resolved_id.nil? && selector.start_with?(Rubik::Uris::TRANSCRIPT_REF_PREFIX)
350
+ resolved_id ? records.find { |r| r["id"] == resolved_id } : nil
351
+ end
352
+
353
+ def content_hash(type, title, body, tags, links)
354
+ canonical = {
355
+ "type" => type.to_s, "title" => title.to_s, "body" => body.to_s,
356
+ "tags" => normalize_tags(tags), "links" => normalize_links(links)
357
+ }
358
+ Digest::SHA256.hexdigest(Rubik::Platform.safe_json_generate(canonical))
359
+ end
360
+
361
+ # Yazım sınırında başarısız-hızlı (issue #100 P1): boolean/sayılar
362
+ # sessizce "false"/"42" dizgelerine dönüşerek etiketi çöpüne
363
+ # çeviriyordu — coercion yok, ret var. Mesaj ne geldiğini ve nasıl
364
+ # düzeltileceğini söyler.
365
+ def normalize_tags(tags)
366
+ list = Array(tags)
367
+ list.each do |tag|
368
+ next if tag.is_a?(String)
369
+
370
+ raise ArgumentError,
371
+ "tags must be strings — got #{tag.class} (#{tag.inspect}). " \
372
+ "Stringify the value before put, or pass an array of strings."
373
+ end
374
+ list.reject { |t| t.strip.empty? }.uniq.sort
375
+ end
376
+
377
+ def normalize_links(links)
378
+ links.to_h { |k, v| [k.to_s, v.to_s] }.sort.to_h
379
+ end
380
+
381
+ def meta_next_row(meta, type)
382
+ rows = (meta["next_row"] ||= {})
383
+ rows[type] = rows[type].to_i + 1
384
+ meta["updated_at"] = Time.now.utc.iso8601
385
+ rows[type]
386
+ end
387
+
388
+ def meta_bump_counts(meta, type)
389
+ counts = (meta["counts"] ||= {})
390
+ counts[type] = counts[type].to_i + 1
391
+ end
392
+
393
+ def meta_set_ref(meta, name, id)
394
+ refs = (meta["refs"] ||= {})
395
+ refs[name] = id
396
+ meta["updated_at"] = Time.now.utc.iso8601
397
+ end
398
+
399
+ def ensure_project_dirs
400
+ FileUtils.mkdir_p(Layout.store_dir(project_dir))
401
+ end
402
+
403
+ def load_meta
404
+ path = Layout.meta_path(project_dir)
405
+ return {} unless File.exist?(path)
406
+
407
+ JSON.parse(File.read(path, encoding: "UTF-8").scrub)
408
+ rescue JSON::ParserError, Errno::ENOENT
409
+ {}
410
+ end
411
+
412
+ def save_meta(meta)
413
+ meta["created_at"] ||= Time.now.utc.iso8601
414
+ Rubik::Platform.atomic_write(Layout.meta_path(project_dir),
415
+ JSON.pretty_generate(meta))
416
+ end
417
+
418
+ # nil => tüm tipler; String => tek tip; Array => çoklu tip (vakanuvis-v2
419
+ # §4-V8: recall artık memory+wiki ikilisini TEK aramada ister — 7 tipin
420
+ # tamamını parse etmekten kurtulur).
421
+ # Arama samanlığı (title+body+tags, Folding.fold — TR I-sınıfı dahil
422
+ # tek katlama kuralı, folding.rb): parse cache girdisinde tembel
423
+ # memoize edilir — sorgu başına 1 MB'lık dizgi kurulumu tekrarlamaz.
424
+ # Kayıt kümesi paylaşımlı olduğundan samanlıklar da dondurulmuş
425
+ # paylaşılır (include?/scan/bytesize mutasyon yapmaz).
426
+ def haystacks_for(type, records)
427
+ entry = @records_cache[type]
428
+ return records.map { |r| haystack_of(r) } unless entry
429
+
430
+ entry[:haystacks] ||= records.map { |r| haystack_of(r) }.freeze
431
+ end
432
+
433
+ def haystack_of(record)
434
+ Folding.fold("#{record['title']}\n#{record['body']}\n#{Array(record['tags']).join("\n")}")
435
+ end
436
+
437
+ def resolve_types(type)
438
+ return TYPES.dup if type.nil?
439
+
440
+ Array(type).map { |t| t.to_s.downcase }.select { |t| TYPES.include?(t) }
441
+ end
442
+
443
+ def load_records(type)
444
+ path = Layout.entities_path(project_dir, type)
445
+ return [] unless File.exist?(path)
446
+
447
+ mtime = File.mtime(path)
448
+ cached = @records_cache[type]
449
+ return cached[:records] if cached && cached[:mtime] == mtime
450
+
451
+ # Depo dosyaları her zaman UTF-8 yazılır (safe_json_generate + "wb");
452
+ # okuma da işlem dış kodlamasına değil UTF-8'a kilitlenir —
453
+ # UTF-8 olmayan Windows yerelinde (TR: cp1254/cp857) yanlış etiketli
454
+ # dizgiler ASCII olmayan recall sorgularında CompatibilityError
455
+ # üretiyordu. Scrub bozuk baytı U+FFFD'ye düşürür, satırı düşürmez.
456
+ # Bozuk/Hash-olmayan satır atlanır ve SAYILIR (review: sessiz
457
+ # kısmi-okuma gözlemlenebilir olsun); Hash olmayan JSON değeri
458
+ # (elle düzenleme: "42", ["x"]) tüm tipi zehirlemesin.
459
+ skipped = 0
460
+ records = File.foreach(path, encoding: "UTF-8").filter_map do |line|
461
+ next if line.strip.empty?
462
+
463
+ parsed = begin
464
+ JSON.parse(line.scrub)
465
+ rescue JSON::ParserError
466
+ nil
467
+ end
468
+ if parsed.is_a?(Hash)
469
+ parsed
470
+ else
471
+ skipped += 1
472
+ nil
473
+ end
474
+ end
475
+ Rubik::Log.warn(:storage, "#{path}: skipped #{skipped} malformed row(s)") if skipped.positive?
476
+ # Paylaşılan cache girdisi dondurulur: dizi + her kayıt hash'i +
477
+ # kayıtların iç koleksiyonları (tags/links — shallow freeze yeterli
478
+ # değildi, en çok uzatılan alanlar sessiz bozulabilirdi).
479
+ # Haystack'ler ilk memoize'da donar (haystacks_for). Paylaşım
480
+ # korunur; kazara mutasyon fail-loud olur.
481
+ records.freeze
482
+ records.each do |record|
483
+ record.each_value { |v| v.freeze if v.is_a?(Array) || v.is_a?(Hash) }
484
+ record.freeze
485
+ end
486
+ @records_cache[type] = { mtime: mtime, records: records }
487
+ records
488
+ rescue Errno::ENOENT
489
+ []
490
+ end
491
+
492
+ def write_records(type, records)
493
+ @records_cache.delete(type) # mtime da yenilenir; çifte güvence
494
+ body = records.map { |r| Rubik::Platform.safe_json_generate(r) }.join("\n")
495
+ body += "\n" unless body.empty?
496
+ Rubik::Platform.atomic_write(Layout.entities_path(project_dir, type), body)
497
+ end
498
+
499
+ # Merkezi indeksi günceller: bu projenin girdisi canlı meta'dan
500
+ # yeniden yazılır, diğer projelerin girdileri korunur.
501
+ def touch_index(meta)
502
+ snapshot = self.class.index_snapshot(@root)
503
+ snapshot["projects"][@project] = {
504
+ "path" => project_dir,
505
+ "entity_counts" => meta.fetch("counts", {}).dup,
506
+ "created_at" => (snapshot["projects"][@project] || {})["created_at"] || meta["created_at"],
507
+ "updated_at" => meta["updated_at"]
508
+ }
509
+ snapshot["updated_at"] = meta["updated_at"]
510
+ Rubik::Platform.atomic_write(Layout.index_path(@root), JSON.pretty_generate(snapshot))
511
+ end
512
+ end
513
+ end
514
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../security/redactor"
5
+ require_relative "../uris"
6
+
7
+ module Rubik
8
+ module Storage
9
+ # Subagent tamamlanınca transcript'ı depoya indeksler (supervisor
10
+ # handle_subagent_completion'dan tek satır çağrılır). Gövde task +
11
+ # final result merkezli deterministik kompozisyondur (2026-10-10
12
+ # transcript sağlık paketi): system-prompt metni arama korpusuna HİÇ
13
+ # girmez; sonuç metni aranabilir olur ("hangi subagent kırmızı
14
+ # bildirdi?" sorusu görevle değil sonuçla bulunur). Tam kopya zaten
15
+ # rubik://subagent/<storage-id>/transcript üzerinden okunur.
16
+ # Başarısızlık asla subagent akışını bozmaz — uyarı basılır (AGENTS.md
17
+ # §5: sessiz yutma yok, yarım durum birikmez).
18
+ module TranscriptIndexer
19
+ module_function
20
+
21
+ # Gövde parçası başına üst sınır (~1-2KB): task ve result kendi
22
+ # içinde kırpılır, fazlalık işaretlenir.
23
+ PART_LIMIT = 1200
24
+
25
+ def index(store, inst)
26
+ return unless store && inst
27
+
28
+ file = inst.transcript_uri.to_s
29
+ # Gerçek upsert: alias üzerinden varsa mevcut id güncellenir — resume
30
+ # sonrası ikinci tamamlanma yinelenen transcript varlığı yaratmaz.
31
+ ref = Rubik::Uris.transcript_ref(inst.storage_id)
32
+ existing = store.get(type: "transcript", id: ref)
33
+ store.put(
34
+ type: "transcript",
35
+ title: "[#{inst.type_name}] #{inst.role} (#{inst.id}) — #{inst.state}",
36
+ body: entity_body(inst, file),
37
+ tags: [inst.type_name.to_s, inst.state.to_s].uniq,
38
+ links: {
39
+ "subagent_id" => inst.storage_id.to_s,
40
+ # The raw path is forensics-only (read_file denies it): the
41
+ # canonical read route rides in the entity itself so recall
42
+ # consumers land on show_file in one hop (issue #97 finding 5).
43
+ "canonical" => Rubik::Uris.subagent_transcript(inst.storage_id),
44
+ "transcript" => file
45
+ },
46
+ status: inst.state.to_s,
47
+ id: existing&.[]("id"),
48
+ ref: ref
49
+ )
50
+ rescue StandardError => e
51
+ label = inst.respond_to?(:id) ? inst.id : inst.class
52
+ warn "[Storage] transcript index failed for #{label}: #{e.class}: #{e.message}"
53
+ end
54
+
55
+ # Task: transcript dosyasındaki İLK user satırı (seed'deki brif);
56
+ # dosya yoksa inst.prompt'a düşer ve dürüst marker taşır. Result:
57
+ # terminalde nil kalabilir (failed) — gövde yine kurulur, state
58
+ # satırı düşer. System satırı bu kompozisyona asla girmez. Dosyadan
59
+ # gelmeyen her metin (prompt fallback, inst.result) HAM memory'den
60
+ # gelir — kalıcı+aranabilir gövdeye girmeden Redactor'dan geçer
61
+ # (dosya tarafının maskeleme paritesi; review bulgusu).
62
+ def entity_body(inst, file)
63
+ task = first_user_task(file)
64
+ task_part = if task
65
+ "[task] #{clip(task)}"
66
+ elsif inst.respond_to?(:prompt)
67
+ "[task] #{clip(redact(inst.prompt.to_s))} (transcript file unavailable)"
68
+ else
69
+ "[task] (transcript file unavailable)"
70
+ end
71
+
72
+ result = inst.respond_to?(:result) ? redact(inst.result.to_s) : ""
73
+ result_part = result.empty? ? "[state] #{inst.state} — no result recorded" : "[result] #{clip(result)}"
74
+ "#{task_part}\n#{result_part}"
75
+ end
76
+
77
+ def redact(text)
78
+ Rubik::Security::Redactor.redact(text)
79
+ end
80
+
81
+ # foreach: task için tüm dosyayı belleğe almak yok; ilk user satırında
82
+ # durur. UTF-8 kilitli okuma: transcript dosyaları UTF-8 yazılır;
83
+ # işlem dış kodlaması (Windows TR yereli) başlık alıntısını bozmasın
84
+ # (store.rb load_records ile aynı disiplin). Okuma/parse arızası
85
+ # sessiz yutulmaz: uyarı basılır, inst.prompt fallback'i döner.
86
+ def first_user_task(file)
87
+ return nil unless File.exist?(file)
88
+
89
+ File.foreach(file, encoding: "UTF-8") do |line|
90
+ row = parse_row(line)
91
+ next unless row.is_a?(Hash) && row["role"] == "user"
92
+
93
+ content = row["content"].to_s
94
+ return content unless content.empty?
95
+ end
96
+ nil
97
+ rescue StandardError => e
98
+ warn "[Storage] transcript task read failed for #{file}: #{e.class}: #{e.message}"
99
+ nil
100
+ end
101
+
102
+ def parse_row(line)
103
+ JSON.parse(line)
104
+ rescue StandardError
105
+ nil
106
+ end
107
+
108
+ def clip(text)
109
+ text = text.to_s.scrub
110
+ return text if text.length <= PART_LIMIT
111
+
112
+ "#{text.slice(0, PART_LIMIT)}\n... [truncated #{text.length - PART_LIMIT} chars]"
113
+ end
114
+ end
115
+ end
116
+ end