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,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubik
4
+ module Storage
5
+ module Vakanuvis
6
+ # Düzlem 2'nin durumlu yarısı: digest iş kuyruğu, doğum sicili ve
7
+ # kirli işaret — DistillGate kurallarının (doğrulayıcı, brif,
8
+ # makbuz) biriktirici Ω'su. ctx servisi DEĞİLDİR, kendi thread'i
9
+ # YOKTUR (worker Harness'te kalır); ömrü sahibi Harness'in ömrüdür
10
+ # (kompozisyon) — o yüzden ctx.on_dispose tersi gerekmez.
11
+ #
12
+ # Tek kilit (@lock) kuyruk + sicil + kirli işareti SAHİPLENİR ve
13
+ # kilit sıralaması kendiliğinden tekyönlüdür: @lock altında yalnız
14
+ # store/supervisor çağrılır (store monitoru asla geri çağırmadığı
15
+ # için @lock → store TEK yuvalanmadır); Harness'in @monitor'u @lock
16
+ # altında ASLA alınmaz. Eski with_gate → @monitor dansı silindi,
17
+ # taşınmadı — gate durumu ile harness durumu artık aynı kod yolunu
18
+ # paylaşmaz.
19
+ module DistillGate
20
+ # Doğum kapısı + iş defteri. Kurucu enjeksiyonuyla çalışır: store,
21
+ # supervisor, event_bus ve sözlü girdiler dışarıdan verilir —
22
+ # Harness'e ASLA geri dokunmaz.
23
+ class Gate
24
+ attr_reader :store, :supervisor, :dmesg, :session_slug, :max_steps, :model
25
+
26
+ def initialize(store:, max_steps:, session_slug:, supervisor: nil, dmesg: nil,
27
+ event_bus: nil, enabled: true, model: nil)
28
+ @store = store
29
+ @supervisor = supervisor
30
+ @dmesg = dmesg
31
+ @event_bus = event_bus
32
+ @enabled = enabled
33
+ @max_steps = max_steps
34
+ @model = model
35
+ @session_slug = session_slug
36
+ @pending_digests = []
37
+ @distillers = {}
38
+ @lock = Mutex.new
39
+ # v2 §4-V5: kirli işareti — son başarılı derlemeden bu yana KALICI
40
+ # içerik (memory+wiki+note satır ayak izi) değişmediyse toplu
41
+ # damıtıcı doğmaz. Turn satırları ayak izini değiştirmez: saf
42
+ # turn akışında derleme boşa yakılmaz.
43
+ @compile_footprint = nil
44
+ end
45
+
46
+ # ---- Seam 1: maestro tool thread'i (Harness#digest delege eder) --
47
+
48
+ # Hedef için dedupe denetimi + itme TEK @lock kritik bölümünde;
49
+ # hemen ardından doğum denemesi AYRI bir @lock penceresinde
50
+ # (iki fazlı — tek büyük kritik bölüm İÇİNE ALMA). Doğum
51
+ # başarısızsa iş kuyrukta kalır: yarım durum birikmez.
52
+ # Dönen makbuz dizgesi bayt-bayt eski Harness#digest çıktısıdır.
53
+ def submit(query)
54
+ target = Vocabulary.wiki_ref(query)
55
+ # v2 §4-V5: aynı hedefli iş zaten kuyrukta ya da uçuştaysa makbuz
56
+ # 'already queued' varyantına düşer — çift doğum yok.
57
+ in_flight = @lock.synchronize do
58
+ busy = @pending_digests.any? { |j| j[:target] == target } ||
59
+ @distillers.any? { |_id, j| j[:target] == target }
60
+ unless busy
61
+ @pending_digests << { query: query, target: target,
62
+ transcript_uris: source_transcript_uris }
63
+ end
64
+ busy
65
+ end
66
+ if in_flight
67
+ return "Digest already queued for '#{target}' (#{AnnalWriter.one_line(query, 60)}): the chronicler is " \
68
+ "compiling it — recall it later with vakanuvis(action: 'recall', query: '...')."
69
+ end
70
+
71
+ spawned = spawn_job!(batch: false)
72
+ DistillGate.receipt(query: query, target: target, spawned: !spawned.nil?)
73
+ end
74
+
75
+ # ---- Seam 2: worker thread'i (Harness#handle_subagent çağırır) ---
76
+
77
+ # Ham subagent tamamlanma olayı. İş sicilden @lock altında
78
+ # silinir, KİLİT BIRAKILIR, sonra supervisor.get + hasat TEK
79
+ # kurtarma bölgesinde koşar: get/inst.result (hedeflenmemiş
80
+ # double, kayıp kayıt) ve DistillGate.harvest (disk hatası, bozuk
81
+ # satır) raise ederse iş kaybolmaz — hedefli iş kuyruğa geri
82
+ # konur (review: sessiz-yutma sınıfı), toplu iş dürüstçe düşer.
83
+ # Dönen değer: :compiled | :incomplete | :crashed | :rejected | nil.
84
+ def completion_event(payload)
85
+ job = @lock.synchronize { @distillers.delete(payload[:id].to_s) }
86
+ return unless job
87
+
88
+ outcome = begin
89
+ inst = @supervisor.respond_to?(:get) ? @supervisor.get(payload[:id].to_s) : nil
90
+ finish_body = inst.respond_to?(:result) ? inst.result : nil
91
+ if payload[:state].to_s != "completed" || finish_body.nil?
92
+ :incomplete
93
+ else
94
+ DistillGate.harvest(self, job, finish_body)
95
+ end
96
+ rescue StandardError => e
97
+ if job[:target]
98
+ @lock.synchronize { @pending_digests << job }
99
+ Vakanuvis.log_via(@dmesg, :warn,
100
+ "harvest crashed for #{job[:target]} (#{e.class}: #{e.message}) — digest requeued")
101
+ else
102
+ Vakanuvis.log_via(@dmesg, :warn,
103
+ "harvest crashed for batch compile (#{e.class}: #{e.message}) — compile dropped")
104
+ end
105
+ :crashed
106
+ end
107
+
108
+ case outcome
109
+ when :compiled
110
+ Vakanuvis.log_via(@dmesg, :info, "distiller compiled #{job[:target] || 'a wiki page'}")
111
+ when :incomplete
112
+ if job[:target]
113
+ @lock.synchronize { @pending_digests << job }
114
+ Vakanuvis.log_via(@dmesg, :warn,
115
+ "distiller for #{job[:target]} ended state=#{payload[:state]} — digest requeued")
116
+ else
117
+ Vakanuvis.log_via(@dmesg, :warn, "batch distiller ended state=#{payload[:state]} — compile dropped")
118
+ end
119
+ when :crashed, :rejected then nil # kendi dalında detay loglandı (crash requeue/drop; B6 ret sebebi + trim)
120
+ else
121
+ # Beklenmedik outcome — yanlış "schema ret" etiketi yerine
122
+ # gerçek değeri yaz (future harvest sembolleri sessiz kalmasın).
123
+ Vakanuvis.log_via(@dmesg, :warn, "distiller ended with unexpected outcome=#{outcome.inspect} — dropped, store untouched")
124
+ end
125
+ outcome
126
+ end
127
+
128
+ # ---- Seam 3: worker thread'inin flush noktası (Harness#maintain) --
129
+
130
+ # Toplu doğum kararı, kirli işarete bağlıdır (v2 §4-V5): kalıcı
131
+ # içerik değişmediyse damıtıcı yakılmaz.
132
+ def flush_tick
133
+ spawn_job!(batch: true) if dirty?
134
+ end
135
+
136
+ # Salt-okunuma peeki (test/kurcalama yüzeyi): kuyruğun başını
137
+ # gösterir — tüketmez. Üretim görünürlüğü aşağıdaki job_targets'tır.
138
+ def peek_job(batch: false)
139
+ @lock.synchronize { @pending_digests.first || (batch ? { batch: true } : nil) }
140
+ end
141
+
142
+ # İş defteri görünümü (issue #100 P2b): kuyruktaki + uçuştaki
143
+ # hedefler. @lock altında YALNIZ kuyruk/sicil okunur — store'a
144
+ # dokunulmaz (kilit sırası tekyönlü kalır).
145
+ def job_targets
146
+ @lock.synchronize do
147
+ { queued: @pending_digests.map { |j| j[:target] || "batch compile" },
148
+ in_flight: @distillers.values.map { |j| j[:target] || "batch compile" } }
149
+ end
150
+ end
151
+
152
+ # Başarılı derlemenin hemen ardından DistillGate çağırır — ayak izi
153
+ # o anki hale snapshot'lanır; sonraki flush ancak YENİ kalıcı
154
+ # içerikte doğum kararı verir. Snapshot'ın kendisi patlarsa sayfa
155
+ # zaten başarılı yazılmıştır: gate kirli kalır (bir sonraki
156
+ # flush'ta yeniden snapshot denenir) — ekonomi kaybı, doğruluk değil.
157
+ def mark_compiled!
158
+ @lock.synchronize { @compile_footprint = footprint }
159
+ end
160
+
161
+ # Oturum annal'ının rubik:// URI'si (DistillGate.brief kaynak
162
+ # listesi için okur). Annal yoksa nil — brif o kaynağı listelemez.
163
+ def session_annal_uri
164
+ rec = @store.get(type: "note", id: Vocabulary.session_ref(@session_slug))
165
+ rec && @store.uri_for("note", rec["id"])
166
+ end
167
+
168
+ private
169
+
170
+ # Doğum kararı: digest kuyruğu önceliklidir. İş claim→spawn→confirm
171
+ # dizisiyle TEK kapıdan (@lock) alınır: maestro thread'i (submit)
172
+ # ile worker'ın flush'u aynı iş için çift doğum yarışamaz; doğum
173
+ # başarısızsa iş kuyruğa geri konur, sonraki flush'ta yeniden
174
+ # denenir.
175
+ def spawn_job!(batch:)
176
+ return unless spawn_allowed?
177
+
178
+ @lock.synchronize do
179
+ job = claim_job(batch: batch)
180
+ next unless job
181
+
182
+ begin
183
+ inst = @supervisor.spawn(
184
+ role: DISTILLER_ROLE, prompt: DistillGate.brief(self, job), type_name: TYPE_NAME,
185
+ # parent_id omitted — the root plane is spawn's default
186
+ # (ConversationId::ROOT_ID); storage stays plane-blind.
187
+ max_steps: @max_steps, model: @model || "inherit"
188
+ )
189
+ confirm(inst.id, job)
190
+ Vakanuvis.log_via(@dmesg, :info, "distiller spawned for #{DistillGate.job_description(job)}")
191
+ inst
192
+ rescue StandardError => e
193
+ restore_job(job)
194
+ Vakanuvis.log_via(@dmesg, :warn,
195
+ "distiller spawn failed (#{DistillGate.job_description(job)}): #{e.class}: #{e.message}")
196
+ nil
197
+ end
198
+ end
199
+ end
200
+
201
+ # Bus yoksa hasat da olamaz (damıtıcının tamamlanma olayı gelmez):
202
+ # doğum kapısı kapalı kalır, makbuz dürüst kalır.
203
+ def spawn_allowed?
204
+ @enabled && @event_bus.respond_to?(:subscribe) &&
205
+ @supervisor.respond_to?(:spawn) &&
206
+ @supervisor.respond_to?(:type_registry) && @supervisor.type_registry.get(TYPE_NAME)
207
+ end
208
+
209
+ # Aşağısı YALNIZ @lock altından çağrılır (çağıran: submit /
210
+ # spawn_job! / completion_event) — kendi kilidi almaz.
211
+ def claim_job(batch: false)
212
+ @pending_digests.shift || (batch ? { batch: true, transcript_uris: source_transcript_uris } : nil)
213
+ end
214
+
215
+ def restore_job(job)
216
+ @pending_digests.unshift(job) if job[:target]
217
+ end
218
+
219
+ def confirm(inst_id, job)
220
+ @distillers[inst_id.to_s] = job
221
+ end
222
+
223
+ def dirty?
224
+ @lock.synchronize { @compile_footprint.nil? || @compile_footprint != footprint }
225
+ end
226
+
227
+ # Kalıcı içerik ayak izi: kart + sayfa + not (annal dahil) satır
228
+ # sayıları. Turn satırları mevcut annal notuna satır ekler, ayak
229
+ # izini değiştirmez — saf turn akışında derleme boşa yakılmaz.
230
+ def footprint
231
+ ["memory", "wiki", "note"].sum { |t| @store.list(type: t).size }
232
+ end
233
+
234
+ # Kaynak disiplini (v2 §4-V4): yalnız status=completed transcript
235
+ # satırları; kronikçinin kendi etiketlileri hariç — özyinelemeli
236
+ # kaynak kirliliği. En yeni N satır (updated_at, sonra row) —
237
+ # deterministik sıra.
238
+ def source_transcript_uris
239
+ summaries = @store.list(type: "transcript").reject do |s|
240
+ s.status != Vocabulary::TRANSCRIPT_COMPLETED || Array(s.tags).include?(Vocabulary::BASE_TAG)
241
+ end
242
+ summaries.sort_by { |s| [s.updated_at.to_s, s.row.to_i] }
243
+ .last(Harness::SOURCE_TRANSCRIPT_LIMIT)
244
+ .map { |s| @store.uri_for("transcript", s.id) }
245
+ end
246
+ end
247
+ end
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,421 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "monitor"
4
+
5
+ module Rubik
6
+ module Storage
7
+ module Vakanuvis
8
+ # Düzlem 1 — Deterministik Çekirdek: tek worker thread + sınırlı
9
+ # FIFO kuyruk. Olay kapıları mevcut bus abonelikleridir (subagent
10
+ # tamamlanma + goal bitişi — K3); NE ZAMAN, NEREYE, NASIL yazılacağı
11
+ # burada kural tabanlıdır. Sessizlik sözleşmesi (§4.3): WAKE_KINDS
12
+ # dışında kalır, notice yaymaz — sadece dmesg/stderr. Ömür: ctx
13
+ # dispose'u LIFO ile kapatır (worker kill + abonelik geri alımı).
14
+ class Harness
15
+ STOP = :__vakanuvis_stop__
16
+ MAX_QUEUE = 200
17
+ RECALL_LIMIT = 5
18
+ RECALL_FALLBACK_LIMIT = 3
19
+ RECALL_BODY_SLICE = 140
20
+ RECALL_TYPES = ["memory", "wiki"].freeze
21
+ # v2 §4-V4: damıtıcı kaynak disiplini — yalnız TAMAMLANMIŞ
22
+ # transcript'ler, damıtıcının kendisi Hariç (özyineleme kirliliği:
23
+ # canlı depoda transcript satırları 'vakanuvis' etiketi taşır).
24
+ SOURCE_TRANSCRIPT_LIMIT = 8
25
+ # Literaller, depolama katmanını orchestrator require'larından
26
+ # uzak tutar; test ikizleri her ikisini de sabitlere pinler:
27
+ # Notices::COMPLETED_EVENT ve GoalState::FINISH_EVENT. TURN_EVENT
28
+ # REPL TurnRecorder#emit_turn_finished kapısıdır (root-turn planı
29
+ # 2026-10-04).
30
+ SUBAGENT_EVENT = :subagent_completed
31
+ GOAL_EVENT = :goal_finished
32
+ TURN_EVENT = :turn_finished
33
+
34
+ attr_reader :store, :supervisor, :dmesg, :session_slug, :gate
35
+
36
+ def initialize(store:, config: nil, event_bus: nil, supervisor: nil, dmesg: nil,
37
+ context: nil, session_slug: nil, max_queue: MAX_QUEUE)
38
+ @store = store
39
+ @config = config
40
+ @event_bus = event_bus
41
+ @supervisor = supervisor
42
+ @dmesg = dmesg
43
+ @context = context
44
+ @session_slug = session_slug || self.class.mint_session_slug
45
+ @flush_events = fetch_cfg("subagents.vakanuvis.flush_events", 10).to_i
46
+ @max_steps = fetch_cfg("subagents.vakanuvis.max_steps", 8).to_i
47
+ @model = fetch_cfg("subagents.vakanuvis.model", nil)
48
+ @retention = fetch_cfg("subagents.vakanuvis.retention", "supersede").to_s
49
+ @distiller_enabled = fetch_cfg("subagents.vakanuvis.distiller", true) != false
50
+ @root_turns = fetch_cfg("subagents.vakanuvis.root_turns", true) != false
51
+ @max_queue = max_queue.to_i
52
+ @queue = Thread::Queue.new
53
+ @monitor = Monitor.new
54
+ @seq = 0
55
+ @goal_ordinal = 0
56
+ @turn_ordinal = 0
57
+ @events_since_distill = 0
58
+ @dropped = 0
59
+ @stopped = false
60
+ @busy = 0
61
+ @worker = nil
62
+ # Damıtıcı doğum kapısı: kuyruk + sicil + kirli işaret Gate'de
63
+ # (kurucu enjeksiyonu — gate Harness'e geri dokunmaz).
64
+ @gate = DistillGate::Gate.new(store: @store, supervisor: @supervisor, dmesg: @dmesg,
65
+ event_bus: @event_bus, enabled: @distiller_enabled,
66
+ max_steps: @max_steps, model: @model, session_slug: @session_slug)
67
+ end
68
+
69
+ class << self
70
+ # Oturum slug'ı: tek süreçte deterministik, süreçler arasında
71
+ # ayrışır. Replay testleri sabit slug enjekte eder.
72
+ def mint_session_slug
73
+ "#{Time.now.utc.strftime('%Y%m%d-%H%M%S')}-#{Process.pid}"
74
+ end
75
+ end
76
+
77
+ # ---- yaşam döngüsü -------------------------------------------------
78
+
79
+ def start!
80
+ subscribe_events
81
+ @worker = Thread.new { run_worker }
82
+ @worker.name = "rubik-vakanuvis" if @worker.respond_to?(:name=)
83
+ self
84
+ end
85
+
86
+ def stopped? = @monitor.synchronize { @stopped }
87
+
88
+ # Dispose yolu: FIFO'daki her şey (STOP dahil öncesindeki olaylar)
89
+ # işlenir, sonra thread sonlanır. Kalan olay yarımda kalmaz; thread
90
+ # 2 saniyede durmazsa (yavaş bir flush/spawn içinde) kill gelir ve
91
+ # işlenemeyen olay sayısı dmesg'e düşer — sessiz kayıp yok.
92
+ def shutdown
93
+ @monitor.synchronize { @stopped = true }
94
+ @monitor.synchronize { @busy += 1 } # STOP işaretinin de işleniş sayılır
95
+ @queue.push(STOP)
96
+ @worker&.join(2)
97
+ if @worker&.alive?
98
+ Vakanuvis.log_via(@dmesg, :warn, "worker did not stop in 2s — killing with #{queue_size} event(s) unprocessed")
99
+ @worker.kill
100
+ end
101
+ @worker = nil
102
+ end
103
+
104
+ def wait_idle(timeout: 2.0)
105
+ deadline = Time.now + timeout
106
+ sleep 0.01 while busy? && Time.now < deadline
107
+ !busy?
108
+ end
109
+
110
+ # Kuyrukta + uçuştaki olay sayısı (push'ta artar, işlenince/düşürülünce
111
+ # azalır) — pop ile işaretleme arasındaki yarış penceresini kapatır.
112
+ def busy? = @monitor.synchronize { @busy.positive? }
113
+
114
+ # ---- olay giriş kapıları (kworker thread'inde çağrılır) -----------
115
+
116
+ def enqueue_event(kind, payload)
117
+ return false if stopped?
118
+
119
+ unless payload.is_a?(Hash)
120
+ Vakanuvis.log_via(@dmesg, :warn, "discarded non-hash #{kind} event (#{payload.class})")
121
+ return false
122
+ end
123
+
124
+ drop_oldest! if @queue.size >= @max_queue
125
+ @monitor.synchronize { @busy += 1 }
126
+ @queue.push([kind, payload])
127
+ true
128
+ end
129
+
130
+ def queue_size = @queue.size
131
+
132
+ # ---- talep düzlemi (maestro aracı buraya delege eder; K7) ---------
133
+
134
+ # Tam deterministik, LLM'siz: Store#search_ranked BM25-lite sırası +
135
+ # RECALL_TYPES kısıtı + ilk RECALL_LIMIT kayıt ⇒ aynı depo ⇒ aynı
136
+ # digest. type: dizisi (v2 §4-V8) transcript/note gibi arama dışı
137
+ # tipleri hiç parse etmez; sıralama politika kararına aittir —
138
+ # Store#search'in [type,row] sözleşmesi değişmez.
139
+ def recall(query, types: RECALL_TYPES)
140
+ q = query.to_s.strip
141
+ raise ArgumentError, "recall: 'query' is required" if q.empty?
142
+
143
+ allowed = Array(types).map(&:to_s)
144
+ allowed = RECALL_TYPES if allowed.empty?
145
+ hits = @store.search_ranked(query: q, type: allowed).first(RECALL_LIMIT)
146
+ unless hits.empty?
147
+ lines = hits.map { |r| recall_line(r) }
148
+ header = "Recalled #{hits.size} entit#{hits.size == 1 ? 'y' : 'ies'} for '#{AnnalWriter.one_line(q, 80)}':"
149
+ return "#{header}\n#{lines.join("\n")}\nRead a full record with read_file(path: '<uri above>')."
150
+ end
151
+
152
+ relaxed_digest(q, allowed)
153
+ end
154
+
155
+ # record tool thread'inden, sweep worker'dan koşar — get-then-put
156
+ # dansını tek kilide alınca çift satır açma yarışı kapanır (store
157
+ # monitoru asla geri çağırmadığı için kilit sırası tekyönlü).
158
+ def record(topic:, body:, links: {}, tags: [])
159
+ @monitor.synchronize { MemorySteward.record(@store, topic: topic, body: body, links: links, tags: tags) }
160
+ end
161
+
162
+ def forget(ref:)
163
+ raise ArgumentError, "forget: 'ref' is required" if ref.to_s.strip.empty?
164
+
165
+ MemorySteward.forget(@store, ref: ref, retention: @retention)
166
+ end
167
+
168
+ # Asenkron (K9): istek kuyruğa yazılır, maestro'ya deterministik
169
+ # makbuz döner — turn'ü asla LLM bekletilmez. Sınır nöbetçisi:
170
+ # boş sorgu burada patlar (tool katmanı yakalar), gerisi Gate'e
171
+ # delege (dedupe + doğum + makbuz orada).
172
+ def digest(query)
173
+ q = query.to_s.strip
174
+ raise ArgumentError, "digest: 'query' is required" if q.empty?
175
+
176
+ @gate.submit(q)
177
+ end
178
+
179
+ # İş kuyruğu görünürlüğü (issue #100 P2b): "yavaş kuyruk mu, düşen
180
+ # iş mi" ayırt edilemezdi — makbuzdan sonra recall, derleme inene
181
+ # dek hiçbir şey göstermiyordu. Deterministik dizgi.
182
+ def digest_status
183
+ targets = @gate.job_targets
184
+ if targets[:queued].empty? && targets[:in_flight].empty?
185
+ return "Digest queue: nothing queued, nothing in flight. " \
186
+ "recall shows compiled pages; a submitted digest appears here until it compiles."
187
+ end
188
+
189
+ "Digest queue: #{targets[:queued].size} queued (#{targets[:queued].join(', ')}), " \
190
+ "#{targets[:in_flight].size} in flight (#{targets[:in_flight].join(', ')})."
191
+ end
192
+
193
+ # Soğuk-başlangıç yönelimi (issue #99 P2): ilk sorgu sözcük dağı
194
+ # tahmin etmek zorunda kalmadan hangi konular var? Aktif ve mem.*
195
+ # bağlı kartlar [ref, row] sırasıyla — deterministik.
196
+ def topics
197
+ bound = @store.refs.select { |ref, _| Vocabulary.memory_ref?(ref) }
198
+ cards = @store.list(type: "memory").select do |s|
199
+ s.status == Vocabulary::STATUS_ACTIVE && bound.value?(s.id)
200
+ end
201
+ if cards.empty?
202
+ return "No memory topics recorded yet — teach the chronicler with " \
203
+ "vakanuvis(action: 'record', topic: ..., body: ...)."
204
+ end
205
+
206
+ lines = cards.sort_by { |s| [bound.key(s.id), s.row] }.filter_map do |s|
207
+ record = @store.get(type: "memory", id: s.id)
208
+ next unless record # list→get arası silme yarışı (review L2)
209
+
210
+ "- #{bound.key(s.id)} — #{s.title} — #{first_body_line(record)} — #{@store.uri_for('memory', s.id)}"
211
+ end
212
+ "Recorded topics (#{lines.size}):\n#{lines.join("\n")}"
213
+ end
214
+
215
+ private
216
+
217
+ # v2 §4-V2 + issue #99 P2: AND boşluğu tek tip körlük değil — en
218
+ # yakın kısmi eşleşmeler dürüstçe "(fallback)" damgasıyla sunulur
219
+ # (gerçek eşleşme değildir), transcript izleri kapsam ipucu olarak
220
+ # sayılır. Ne fallback ne transcript izi varsa bugünkü dil/terim
221
+ # yönlendirmesi birebir döner (keyword-AND'da en sık neden TR
222
+ # sorgu ↔ EN gövde uyuşmazlığıdır; yanlış "hemen yeniden kaydet"
223
+ # yönlendirmesi burada kesilir).
224
+ def relaxed_digest(query, allowed)
225
+ q = query.to_s.strip
226
+ # Süpersede edilmiş sayılar "en yakın kısmi eşleşme" olarak
227
+ # sunulmaz (review M1) — emekli bir sayfayı okumaya davet etmek,
228
+ # issue #100'ün "iki kopya, biri bozuk" bulgusunun aynısıdır.
229
+ relaxed = @store.search_ranked(query: q, type: allowed, require_all: false)
230
+ .reject { |r| r["status"] == Vocabulary::STATUS_SUPERSEDED }
231
+ .first(RECALL_FALLBACK_LIMIT)
232
+ transcripts = @store.search(query: q, type: "transcript")
233
+ if relaxed.empty? && transcripts.empty?
234
+ return "No chronicled knowledge matches '#{AnnalWriter.one_line(q, 80)}'. Recall is keyword-AND: retry with " \
235
+ "fewer or other-language terms first (e.g. the TR/EN Keywords: lines). If the knowledge is genuinely " \
236
+ "missing, record it (vakanuvis action 'record') or queue a digest."
237
+ end
238
+
239
+ parts = []
240
+ # Başlık + read_file ayağı yalnız gerçek fallback izi varken
241
+ # basılır (review CR#1: "yukarıdaki uri" diyecek uri yokken
242
+ # olmayan bir talimat modele verilmesin).
243
+ unless relaxed.empty?
244
+ parts << "No exact keyword-AND match for '#{AnnalWriter.one_line(q, 80)}' — closest partial matches (fallback, not true matches):"
245
+ parts.concat(relaxed.map { |r| "- (fallback) #{recall_line(r)}" })
246
+ end
247
+ unless transcripts.empty?
248
+ parts << "#{transcripts.size} transcript entr#{transcripts.size == 1 ? 'y' : 'ies'} mention these terms — " \
249
+ "the raw session logs may hold it; queue a digest to compile them."
250
+ end
251
+ parts << "If none of these help, retry with fewer or other-language terms (the TR/EN Keywords: lines), " \
252
+ "record the knowledge, or queue a digest."
253
+ parts << "Read a full record with read_file(path: '<uri above>')." unless relaxed.empty?
254
+ parts.join("\n")
255
+ end
256
+
257
+ def first_body_line(record)
258
+ slice = record["body"].to_s.lines.map(&:strip).reject(&:empty?).first.to_s.slice(0, RECALL_BODY_SLICE)
259
+ slice.empty? ? "(no body)" : slice
260
+ end
261
+
262
+ def recall_line(record)
263
+ "- #{record['title']} [#{record['id']}] #{first_body_line(record)} — #{@store.uri_for(record['type'], record['id'])}"
264
+ end
265
+
266
+ def subscribe_events
267
+ return unless @event_bus.respond_to?(:subscribe)
268
+
269
+ # Aboneliğin geri alımı ctx.on_dispose ile aynı nefeste (§1A):
270
+ # dispatcher subscribe'a verilen context'e unregister inverse'i
271
+ # kendisi bağlar.
272
+ @event_bus.subscribe(SUBAGENT_EVENT, context: @context) { |p| enqueue_event(:subagent, p) }
273
+ @event_bus.subscribe(GOAL_EVENT, context: @context) { |p| enqueue_event(:goal, p) }
274
+ return unless @root_turns
275
+
276
+ # Kök-turn kapısı knob arkasında (R7): kapalıyken abonelik hiç
277
+ # kurulmaz, :turn_finished olayı kronikçiye akmaz.
278
+ @event_bus.subscribe(TURN_EVENT, context: @context) { |p| enqueue_event(:turn, p) }
279
+ end
280
+
281
+ # Taşma düşürmesi asla STOP işaretini yutamaz (yutsa worker hiç
282
+ # sonlanamazdı): STOP geldiyse kuyruğun sonuna geri konur.
283
+ def drop_oldest!
284
+ item = @queue.pop(true)
285
+ if item == STOP
286
+ @queue.push(STOP)
287
+ else
288
+ @monitor.synchronize do
289
+ @dropped += 1
290
+ @busy -= 1
291
+ end
292
+ Vakanuvis.log_via(@dmesg, :warn, "event queue overflow — dropped oldest (total: #{@dropped})")
293
+ end
294
+ rescue ThreadError
295
+ nil
296
+ end
297
+
298
+ def run_worker
299
+ loop do
300
+ kind, payload = @queue.pop
301
+ break if kind == STOP
302
+
303
+ handle_event(kind, payload)
304
+ ensure
305
+ @monitor.synchronize { @busy -= 1 }
306
+ end
307
+ end
308
+
309
+ # TranscriptIndexer disiplini: kronikçinin kendi hatası akışı
310
+ # asla bozmaz — uyarı düşer, sonraki olay işlenir.
311
+ def handle_event(kind, payload)
312
+ case kind
313
+ when :subagent then handle_subagent(payload)
314
+ when :goal then handle_goal(payload)
315
+ when :turn then handle_turn(payload)
316
+ end
317
+ rescue StandardError => e
318
+ Vakanuvis.log_via(@dmesg, :warn, "event handling failed (#{kind}): #{e.class}: #{e.message}")
319
+ end
320
+
321
+ def handle_subagent(payload)
322
+ # Önce kronik (Düzlem 1 garantisi), sonra hasat (Düzlem 2): hasat
323
+ # patlarsa bile olayın annal satırı yazılmış olur — deterministik
324
+ # katman LLM katmanının hatasından zarar görmez.
325
+ seq = next_seq
326
+ line = AnnalWriter.subagent_line(seq, payload)
327
+ append_session([[event_key(payload), line]])
328
+ @gate.completion_event(payload)
329
+ maintain if flush_due?
330
+ end
331
+
332
+ # Kök-turn kroniği: ordinal session-slug süreç-başına mint
333
+ # edildiğinden harness-lokal sayaç yeterli (yeni süreç = yeni
334
+ # session annal; goal sırasındaki mağazadan-türetme burada
335
+ # gereksiz). Turn olayı flush sayacına sayılır ama koşulsuz
336
+ # maintain tetiklemez — turn sıklığı yüksek (R8).
337
+ def handle_turn(payload)
338
+ ordinal = @monitor.synchronize { @turn_ordinal += 1 }
339
+ seq = next_seq
340
+ append_session([AnnalWriter.turn_line(seq, ordinal, payload)])
341
+ maintain if flush_due?
342
+ end
343
+
344
+ def handle_goal(payload)
345
+ ordinal = next_goal_ordinal
346
+ enriched = payload.merge(goal_ordinal: ordinal)
347
+ seq = next_seq
348
+ goal_body = [AnnalWriter.goal_header_line(enriched[:goal])] +
349
+ AnnalWriter.goal_lines(seq, enriched).map { |_k, l| l }
350
+ Vakanuvis.upsert(@store, type: "note", ref: Vocabulary.goal_ref(ordinal),
351
+ title: AnnalWriter.goal_title(ordinal, enriched[:goal]),
352
+ body: goal_body.join("\n"), tags: Vocabulary::ANNAL_TAGS + [Vocabulary::GOAL_TAG],
353
+ links: { "session" => Vocabulary.session_ref(@session_slug) })
354
+ append_session(AnnalWriter.goal_lines(seq, enriched))
355
+ maintain
356
+ end
357
+
358
+ # Goal sırası mağazadan türetilir (refs'teki en büyük annal.goal.<n>
359
+ # + 1): süreç yeniden başlasa bile aynı depoda numara çakışmaz —
360
+ # determinizm depo durumundan, saatlerden değil.
361
+ def next_goal_ordinal
362
+ @monitor.synchronize do
363
+ max_ref = @store.refs.keys.filter_map { |k| Vocabulary.goal_ordinal(k) }.max.to_i
364
+ @goal_ordinal = [@goal_ordinal, max_ref].max + 1
365
+ end
366
+ end
367
+
368
+ # Flush sayacı (R8): subagent ve turn olayları aynı ölçüde sayılır;
369
+ # flush_events eşiği aşılınca true döner ve sayaç sıfırlanır.
370
+ def flush_due?
371
+ @monitor.synchronize do
372
+ @events_since_distill += 1
373
+ crossed = @events_since_distill >= @flush_events
374
+ @events_since_distill = 0 if crossed
375
+ crossed
376
+ end
377
+ end
378
+
379
+ # Flush bakımı (yalnızca flush noktalarında — flush_events eşiği
380
+ # aşımı veya goal bitişi): kaçak memory süpürme (K9) + wiki home
381
+ # yeniden hesap + damıtıcı doğum kararı. v2 §4-V5: toplu doğum
382
+ # yalnızca kirli işaretle — kalıcı içerik değişmediyse (yalnızca
383
+ # turn satırları biriktiyse) damıtıcı yakılmaz.
384
+ def maintain
385
+ # Sweep record() ile aynı kilide koşar (tek-yazar disiplini):
386
+ # get-then-put dansı çift satır açamaz.
387
+ swept = @monitor.synchronize { MemorySteward.sweep_rogue_memory(@store) }
388
+ Vakanuvis.log_via(@dmesg, :info, "steward swept #{swept} rogue memory row(s)") if swept.positive?
389
+ WikiSkeleton.rebuild_home(@store)
390
+ @gate.flush_tick
391
+ end
392
+
393
+ def append_session(lines)
394
+ overflow = nil
395
+ @monitor.synchronize do
396
+ if @dropped.positive?
397
+ seq = @seq += 1
398
+ overflow = [["dropped.#{seq}", "- [#{seq}] #{@dropped} event(s) dropped — queue overflow (event=dropped.#{seq})"]]
399
+ @dropped = 0
400
+ end
401
+ end
402
+ AnnalWriter.append(@store, ref: Vocabulary.session_ref(@session_slug),
403
+ title: AnnalWriter.session_title(@session_slug),
404
+ lines: lines + overflow.to_a)
405
+ end
406
+
407
+ def next_seq = @monitor.synchronize { @seq += 1 }
408
+
409
+ def event_key(payload)
410
+ sid = payload[:storage_id].to_s
411
+ sid = payload[:id].to_s if sid.empty?
412
+ "subagent.#{sid}.#{payload[:state]}"
413
+ end
414
+
415
+ def fetch_cfg(key, default)
416
+ @config.respond_to?(:fetch) ? @config.fetch(key, default) : default
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end