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.
- checksums.yaml +7 -0
- data/ATTRIBUTION.md +139 -0
- data/CHANGELOG.md +236 -0
- data/LICENSE +21 -0
- data/README.md +269 -0
- data/bin/rubik +82 -0
- data/lib/rubik/adapter/base.rb +195 -0
- data/lib/rubik/adapter/c.rb +36 -0
- data/lib/rubik/adapter/crystal.rb +78 -0
- data/lib/rubik/adapter/generic.rb +86 -0
- data/lib/rubik/adapter/go.rb +36 -0
- data/lib/rubik/adapter/node.rb +146 -0
- data/lib/rubik/adapter/process.rb +78 -0
- data/lib/rubik/adapter/python.rb +30 -0
- data/lib/rubik/adapter/registry.rb +81 -0
- data/lib/rubik/adapter/ruby.rb +30 -0
- data/lib/rubik/adapter/rust.rb +36 -0
- data/lib/rubik/adapter/zig.rb +36 -0
- data/lib/rubik/adapter.rb +18 -0
- data/lib/rubik/ast/body_loader.rb +19 -0
- data/lib/rubik/ast/c_extractor.rb +114 -0
- data/lib/rubik/ast/cache_store.rb +191 -0
- data/lib/rubik/ast/extractor.rb +59 -0
- data/lib/rubik/ast/fallback_cleaner.rb +145 -0
- data/lib/rubik/ast/go_extractor.rb +148 -0
- data/lib/rubik/ast/graph.rb +272 -0
- data/lib/rubik/ast/python_extractor.rb +120 -0
- data/lib/rubik/ast/ruby_extractor.rb +260 -0
- data/lib/rubik/ast/rust_extractor.rb +127 -0
- data/lib/rubik/ast/symbol.rb +103 -0
- data/lib/rubik/ast/treesitter/base_walker.rb +109 -0
- data/lib/rubik/ast/treesitter/c_walker.rb +122 -0
- data/lib/rubik/ast/treesitter/go_walker.rb +120 -0
- data/lib/rubik/ast/treesitter/python_walker.rb +107 -0
- data/lib/rubik/ast/treesitter/rust_walker.rb +130 -0
- data/lib/rubik/ast/treesitter/typescript_walker.rb +182 -0
- data/lib/rubik/ast/treesitter/zig_walker.rb +109 -0
- data/lib/rubik/ast/treesitter_extractor.rb +93 -0
- data/lib/rubik/ast/typescript_extractor.rb +283 -0
- data/lib/rubik/ast/zig_extractor.rb +116 -0
- data/lib/rubik/ast.rb +13 -0
- data/lib/rubik/benchmark/runner.rb +224 -0
- data/lib/rubik/benchmark/synth_repo.rb +59 -0
- data/lib/rubik/benchmark.rb +9 -0
- data/lib/rubik/buffer/diff.rb +43 -0
- data/lib/rubik/buffer/manager.rb +427 -0
- data/lib/rubik/buffer/path_validator.rb +60 -0
- data/lib/rubik/buffer/snapshot.rb +99 -0
- data/lib/rubik/buffer.rb +10 -0
- data/lib/rubik/coeffects.rb +129 -0
- data/lib/rubik/concurrency/boundary.rb +74 -0
- data/lib/rubik/concurrency/dns_pool.rb +68 -0
- data/lib/rubik/concurrency/errors.rb +17 -0
- data/lib/rubik/concurrency/event_loop.rb +441 -0
- data/lib/rubik/concurrency/join.rb +55 -0
- data/lib/rubik/concurrency/process_waiter.rb +55 -0
- data/lib/rubik/concurrency/scheduler.rb +84 -0
- data/lib/rubik/concurrency/timers.rb +109 -0
- data/lib/rubik/concurrency.rb +132 -0
- data/lib/rubik/config/deep_ops.rb +80 -0
- data/lib/rubik/config/defaults.rb +248 -0
- data/lib/rubik/config/model_catalog.rb +93 -0
- data/lib/rubik/config/store.rb +155 -0
- data/lib/rubik/config.rb +216 -0
- data/lib/rubik/context/git_info.rb +84 -0
- data/lib/rubik/context/git_lister.rb +37 -0
- data/lib/rubik/context/index.rb +134 -0
- data/lib/rubik/context/repo_context_reader.rb +121 -0
- data/lib/rubik/context/scanner.rb +439 -0
- data/lib/rubik/context.rb +11 -0
- data/lib/rubik/diagnostics/collector.rb +200 -0
- data/lib/rubik/diagnostics/entry.rb +60 -0
- data/lib/rubik/diagnostics/parser.rb +107 -0
- data/lib/rubik/diagnostics.rb +10 -0
- data/lib/rubik/event_bus.rb +56 -0
- data/lib/rubik/kernel/dispatcher.rb +171 -0
- data/lib/rubik/kernel/dmesg.rb +116 -0
- data/lib/rubik/kernel/init.rb +177 -0
- data/lib/rubik/kernel/record.rb +85 -0
- data/lib/rubik/kernel/unit.rb +160 -0
- data/lib/rubik/llm/anthropic.rb +202 -0
- data/lib/rubik/llm/chunk_assembler.rb +285 -0
- data/lib/rubik/llm/client.rb +526 -0
- data/lib/rubik/llm/formatter.rb +450 -0
- data/lib/rubik/llm/message_sanitizer.rb +87 -0
- data/lib/rubik/llm/ollama.rb +167 -0
- data/lib/rubik/llm/openai.rb +21 -0
- data/lib/rubik/llm/openai_compatible.rb +216 -0
- data/lib/rubik/llm/provider.rb +125 -0
- data/lib/rubik/llm/registry.rb +336 -0
- data/lib/rubik/llm/router.rb +100 -0
- data/lib/rubik/llm.rb +18 -0
- data/lib/rubik/log.rb +38 -0
- data/lib/rubik/lsp/client.rb +475 -0
- data/lib/rubik/lsp/manager.rb +366 -0
- data/lib/rubik/lsp.rb +9 -0
- data/lib/rubik/mcp/boot_lifecycle.rb +114 -0
- data/lib/rubik/mcp/client.rb +169 -0
- data/lib/rubik/mcp/errors.rb +11 -0
- data/lib/rubik/mcp/manager.rb +619 -0
- data/lib/rubik/mcp/proxy_tool.rb +104 -0
- data/lib/rubik/mcp/transport/stdio.rb +255 -0
- data/lib/rubik/mcp.rb +12 -0
- data/lib/rubik/orchestrator/activity_log.rb +75 -0
- data/lib/rubik/orchestrator/auto_apply.rb +166 -0
- data/lib/rubik/orchestrator/budget_manager.rb +97 -0
- data/lib/rubik/orchestrator/continuation_manager.rb +161 -0
- data/lib/rubik/orchestrator/finish_message_selector.rb +55 -0
- data/lib/rubik/orchestrator/goal_ledger/drift.rb +71 -0
- data/lib/rubik/orchestrator/goal_ledger/ledger.rb +399 -0
- data/lib/rubik/orchestrator/goal_ledger/observation_projection.rb +179 -0
- data/lib/rubik/orchestrator/goal_ledger/patch.rb +268 -0
- data/lib/rubik/orchestrator/goal_ledger/schema.rb +123 -0
- data/lib/rubik/orchestrator/goal_ledger/validator.rb +361 -0
- data/lib/rubik/orchestrator/goal_ledger/verifier.rb +129 -0
- data/lib/rubik/orchestrator/goal_ledger/verify_spec.rb +50 -0
- data/lib/rubik/orchestrator/goal_ledger.rb +65 -0
- data/lib/rubik/orchestrator/goal_state.rb +184 -0
- data/lib/rubik/orchestrator/goal_wait.rb +125 -0
- data/lib/rubik/orchestrator/loop.rb +388 -0
- data/lib/rubik/orchestrator/lsp_feedback.rb +55 -0
- data/lib/rubik/orchestrator/maestro_gate.rb +65 -0
- data/lib/rubik/orchestrator/provider_recovery.rb +90 -0
- data/lib/rubik/orchestrator/provider_resolver.rb +69 -0
- data/lib/rubik/orchestrator/shell_deps.rb +85 -0
- data/lib/rubik/orchestrator/step_executor.rb +328 -0
- data/lib/rubik/orchestrator/stream_manager.rb +118 -0
- data/lib/rubik/orchestrator/subagent/activation.rb +65 -0
- data/lib/rubik/orchestrator/subagent/budget.rb +108 -0
- data/lib/rubik/orchestrator/subagent/completion.rb +61 -0
- data/lib/rubik/orchestrator/subagent/concurrency_gate.rb +37 -0
- data/lib/rubik/orchestrator/subagent/conversation_id.rb +67 -0
- data/lib/rubik/orchestrator/subagent/dependency.rb +398 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +384 -0
- data/lib/rubik/orchestrator/subagent/lifecycle.rb +118 -0
- data/lib/rubik/orchestrator/subagent/messaging.rb +61 -0
- data/lib/rubik/orchestrator/subagent/narration.rb +125 -0
- data/lib/rubik/orchestrator/subagent/notices.rb +209 -0
- data/lib/rubik/orchestrator/subagent/questioner.rb +80 -0
- data/lib/rubik/orchestrator/subagent/reaper.rb +98 -0
- data/lib/rubik/orchestrator/subagent/resilience.rb +70 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +127 -0
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +79 -0
- data/lib/rubik/orchestrator/subagent/step_runner.rb +240 -0
- data/lib/rubik/orchestrator/subagent/storage_id.rb +26 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +327 -0
- data/lib/rubik/orchestrator/subagent/transcript_formatter.rb +141 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +199 -0
- data/lib/rubik/orchestrator/subagent_notice_inbox.rb +80 -0
- data/lib/rubik/orchestrator/tool_call_id.rb +47 -0
- data/lib/rubik/orchestrator/tool_circuit_breaker.rb +91 -0
- data/lib/rubik/orchestrator/tool_history.rb +39 -0
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +102 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +172 -0
- data/lib/rubik/orchestrator/turn_executor.rb +376 -0
- data/lib/rubik/orchestrator.rb +11 -0
- data/lib/rubik/platform/filesystem.rb +64 -0
- data/lib/rubik/platform/io_drain.rb +53 -0
- data/lib/rubik/platform/path.rb +210 -0
- data/lib/rubik/platform/process.rb +38 -0
- data/lib/rubik/platform/sanitize.rb +157 -0
- data/lib/rubik/platform.rb +113 -0
- data/lib/rubik/plugin/dsl.rb +51 -0
- data/lib/rubik/plugin/manager.rb +247 -0
- data/lib/rubik/plugin/storage.rb +195 -0
- data/lib/rubik/plugin.rb +10 -0
- data/lib/rubik/prompt/context_builder.rb +377 -0
- data/lib/rubik/prompt/context_compactor.rb +326 -0
- data/lib/rubik/prompt/conversation_rows.rb +74 -0
- data/lib/rubik/prompt/doctrine.rb +160 -0
- data/lib/rubik/prompt/emergency_compactor.rb +81 -0
- data/lib/rubik/prompt/services.rb +60 -0
- data/lib/rubik/prompt/token_estimator.rb +63 -0
- data/lib/rubik/prompt/tool_teachings.rb +78 -0
- data/lib/rubik/prompt.rb +13 -0
- data/lib/rubik/repair/loop.rb +95 -0
- data/lib/rubik/repair.rb +8 -0
- data/lib/rubik/repl/ast_warmup.rb +129 -0
- data/lib/rubik/repl/command_context.rb +214 -0
- data/lib/rubik/repl/command_parser.rb +48 -0
- data/lib/rubik/repl/commands/agent.rb +30 -0
- data/lib/rubik/repl/commands/agent_context.rb +289 -0
- data/lib/rubik/repl/commands/agent_execution.rb +342 -0
- data/lib/rubik/repl/commands/agent_models.rb +212 -0
- data/lib/rubik/repl/commands/agent_providers.rb +232 -0
- data/lib/rubik/repl/commands/base.rb +55 -0
- data/lib/rubik/repl/commands/buffer.rb +124 -0
- data/lib/rubik/repl/commands/dmesg.rb +87 -0
- data/lib/rubik/repl/commands/lsp.rb +235 -0
- data/lib/rubik/repl/commands/mcp.rb +194 -0
- data/lib/rubik/repl/commands/plugins.rb +208 -0
- data/lib/rubik/repl/commands/pruner.rb +71 -0
- data/lib/rubik/repl/commands/session.rb +183 -0
- data/lib/rubik/repl/commands/skills.rb +111 -0
- data/lib/rubik/repl/commands/storage.rb +85 -0
- data/lib/rubik/repl/commands/subagents.rb +195 -0
- data/lib/rubik/repl/commands/system.rb +85 -0
- data/lib/rubik/repl/commands/tasks.rb +120 -0
- data/lib/rubik/repl/commands/workspace.rb +443 -0
- data/lib/rubik/repl/commands.rb +44 -0
- data/lib/rubik/repl/composition.rb +32 -0
- data/lib/rubik/repl/config_wiring.rb +142 -0
- data/lib/rubik/repl/confirmations.rb +56 -0
- data/lib/rubik/repl/hooks.rb +47 -0
- data/lib/rubik/repl/input_execution.rb +323 -0
- data/lib/rubik/repl/input_loop.rb +213 -0
- data/lib/rubik/repl/interrupt_handler.rb +217 -0
- data/lib/rubik/repl/lifecycle.rb +410 -0
- data/lib/rubik/repl/paste_support.rb +88 -0
- data/lib/rubik/repl/pending_notice_queue.rb +60 -0
- data/lib/rubik/repl/presentation.rb +132 -0
- data/lib/rubik/repl/pruner/brief.rb +101 -0
- data/lib/rubik/repl/pruner/gate.rb +279 -0
- data/lib/rubik/repl/pruner/harness.rb +236 -0
- data/lib/rubik/repl/pruner/rules.rb +316 -0
- data/lib/rubik/repl/pruner/service.rb +82 -0
- data/lib/rubik/repl/pruner.rb +39 -0
- data/lib/rubik/repl/service_catalog.rb +374 -0
- data/lib/rubik/repl/setup_factories.rb +66 -0
- data/lib/rubik/repl/shell.rb +231 -0
- data/lib/rubik/repl/step_formatter.rb +324 -0
- data/lib/rubik/repl/tee_io.rb +105 -0
- data/lib/rubik/repl/terminal_guard.rb +157 -0
- data/lib/rubik/repl/turn_recorder.rb +234 -0
- data/lib/rubik/repl.rb +14 -0
- data/lib/rubik/runtime/ast_service.rb +78 -0
- data/lib/rubik/runtime/context.rb +335 -0
- data/lib/rubik/runtime/diagnostic_engine.rb +188 -0
- data/lib/rubik/runtime/discovery.rb +342 -0
- data/lib/rubik/runtime/fiber.rb +142 -0
- data/lib/rubik/runtime/fiber_ordering.rb +52 -0
- data/lib/rubik/runtime/fiber_retry_gate.rb +84 -0
- data/lib/rubik/runtime/index_service.rb +58 -0
- data/lib/rubik/runtime/loop_service.rb +73 -0
- data/lib/rubik/runtime/manager.rb +241 -0
- data/lib/rubik/runtime/overlay_storage.rb +481 -0
- data/lib/rubik/runtime/service_definition.rb +54 -0
- data/lib/rubik/runtime/service_registration.rb +33 -0
- data/lib/rubik/runtime/task_manager.rb +302 -0
- data/lib/rubik/runtime.rb +19 -0
- data/lib/rubik/sandbox/driver/base.rb +30 -0
- data/lib/rubik/sandbox/driver/bubblewrap.rb +111 -0
- data/lib/rubik/sandbox/driver/docker.rb +113 -0
- data/lib/rubik/sandbox/driver/podman.rb +98 -0
- data/lib/rubik/sandbox/driver/process.rb +190 -0
- data/lib/rubik/sandbox/driver.rb +91 -0
- data/lib/rubik/sandbox/runner.rb +218 -0
- data/lib/rubik/sandbox/shell_resolver.rb +178 -0
- data/lib/rubik/sandbox.rb +8 -0
- data/lib/rubik/security/blocked_guidance.rb +187 -0
- data/lib/rubik/security/classifier/classifier.rb +440 -0
- data/lib/rubik/security/classifier/commands/agent-browser.yaml +9 -0
- data/lib/rubik/security/classifier/commands/apt.yaml +25 -0
- data/lib/rubik/security/classifier/commands/awk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/basename.yaml +7 -0
- data/lib/rubik/security/classifier/commands/bash-classify.yaml +8 -0
- data/lib/rubik/security/classifier/commands/bash.yaml +20 -0
- data/lib/rubik/security/classifier/commands/black.yaml +12 -0
- data/lib/rubik/security/classifier/commands/brew.yaml +31 -0
- data/lib/rubik/security/classifier/commands/builtin.yaml +8 -0
- data/lib/rubik/security/classifier/commands/cargo.yaml +114 -0
- data/lib/rubik/security/classifier/commands/cat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/chgrp.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chmod.yaml +10 -0
- data/lib/rubik/security/classifier/commands/chown.yaml +10 -0
- data/lib/rubik/security/classifier/commands/column.yaml +7 -0
- data/lib/rubik/security/classifier/commands/comm.yaml +7 -0
- data/lib/rubik/security/classifier/commands/command.yaml +12 -0
- data/lib/rubik/security/classifier/commands/cp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/crystal.yaml +63 -0
- data/lib/rubik/security/classifier/commands/curl.yaml +30 -0
- data/lib/rubik/security/classifier/commands/cut.yaml +7 -0
- data/lib/rubik/security/classifier/commands/date.yaml +10 -0
- data/lib/rubik/security/classifier/commands/df.yaml +7 -0
- data/lib/rubik/security/classifier/commands/diff.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dig.yaml +7 -0
- data/lib/rubik/security/classifier/commands/dirname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/docker.yaml +177 -0
- data/lib/rubik/security/classifier/commands/du.yaml +7 -0
- data/lib/rubik/security/classifier/commands/echo.yaml +7 -0
- data/lib/rubik/security/classifier/commands/env.yaml +15 -0
- data/lib/rubik/security/classifier/commands/eslint.yaml +12 -0
- data/lib/rubik/security/classifier/commands/eval.yaml +8 -0
- data/lib/rubik/security/classifier/commands/exec.yaml +12 -0
- data/lib/rubik/security/classifier/commands/expand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/false.yaml +7 -0
- data/lib/rubik/security/classifier/commands/file.yaml +7 -0
- data/lib/rubik/security/classifier/commands/find.yaml +25 -0
- data/lib/rubik/security/classifier/commands/fmt.yaml +7 -0
- data/lib/rubik/security/classifier/commands/fold.yaml +7 -0
- data/lib/rubik/security/classifier/commands/free.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gawk.yaml +10 -0
- data/lib/rubik/security/classifier/commands/gcloud.yaml +979 -0
- data/lib/rubik/security/classifier/commands/gh.yaml +615 -0
- data/lib/rubik/security/classifier/commands/git.yaml +524 -0
- data/lib/rubik/security/classifier/commands/glab-discussion.yaml +21 -0
- data/lib/rubik/security/classifier/commands/glab.yaml +807 -0
- data/lib/rubik/security/classifier/commands/go.yaml +44 -0
- data/lib/rubik/security/classifier/commands/gog.yaml +983 -0
- data/lib/rubik/security/classifier/commands/gradle.yaml +92 -0
- data/lib/rubik/security/classifier/commands/gradlew.yaml +92 -0
- data/lib/rubik/security/classifier/commands/grep.yaml +7 -0
- data/lib/rubik/security/classifier/commands/groups.yaml +7 -0
- data/lib/rubik/security/classifier/commands/gunzip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/gws.yaml +211 -0
- data/lib/rubik/security/classifier/commands/gzip.yaml +7 -0
- data/lib/rubik/security/classifier/commands/hadolint.yaml +9 -0
- data/lib/rubik/security/classifier/commands/halt.yaml +8 -0
- data/lib/rubik/security/classifier/commands/head.yaml +7 -0
- data/lib/rubik/security/classifier/commands/helm.yaml +129 -0
- data/lib/rubik/security/classifier/commands/hostname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/id.yaml +7 -0
- data/lib/rubik/security/classifier/commands/install.yaml +7 -0
- data/lib/rubik/security/classifier/commands/isort.yaml +14 -0
- data/lib/rubik/security/classifier/commands/java.yaml +35 -0
- data/lib/rubik/security/classifier/commands/javac.yaml +39 -0
- data/lib/rubik/security/classifier/commands/join.yaml +7 -0
- data/lib/rubik/security/classifier/commands/journalctl.yaml +14 -0
- data/lib/rubik/security/classifier/commands/jq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/killall.yaml +7 -0
- data/lib/rubik/security/classifier/commands/kubectl.yaml +211 -0
- data/lib/rubik/security/classifier/commands/less.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ln.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ls.yaml +7 -0
- data/lib/rubik/security/classifier/commands/make.yaml +21 -0
- data/lib/rubik/security/classifier/commands/md5sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mise.yaml +579 -0
- data/lib/rubik/security/classifier/commands/mkdir.yaml +8 -0
- data/lib/rubik/security/classifier/commands/more.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/mvn.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mvnw.yaml +83 -0
- data/lib/rubik/security/classifier/commands/mypy.yaml +9 -0
- data/lib/rubik/security/classifier/commands/netstat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/nice.yaml +11 -0
- data/lib/rubik/security/classifier/commands/nl.yaml +7 -0
- data/lib/rubik/security/classifier/commands/node.yaml +20 -0
- data/lib/rubik/security/classifier/commands/nohup.yaml +9 -0
- data/lib/rubik/security/classifier/commands/npm.yaml +71 -0
- data/lib/rubik/security/classifier/commands/npx.yaml +42 -0
- data/lib/rubik/security/classifier/commands/nslookup.yaml +7 -0
- data/lib/rubik/security/classifier/commands/paste.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ping.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pip.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pip3.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pipx.yaml +33 -0
- data/lib/rubik/security/classifier/commands/pkill.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pnpm.yaml +346 -0
- data/lib/rubik/security/classifier/commands/poetry.yaml +97 -0
- data/lib/rubik/security/classifier/commands/poweroff.yaml +8 -0
- data/lib/rubik/security/classifier/commands/prettier.yaml +13 -0
- data/lib/rubik/security/classifier/commands/printenv.yaml +7 -0
- data/lib/rubik/security/classifier/commands/printf.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ps.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pup.yaml +882 -0
- data/lib/rubik/security/classifier/commands/pwd.yaml +7 -0
- data/lib/rubik/security/classifier/commands/pytest.yaml +7 -0
- data/lib/rubik/security/classifier/commands/python.yaml +9 -0
- data/lib/rubik/security/classifier/commands/python3.yaml +9 -0
- data/lib/rubik/security/classifier/commands/read.yaml +7 -0
- data/lib/rubik/security/classifier/commands/readlink.yaml +7 -0
- data/lib/rubik/security/classifier/commands/realpath.yaml +7 -0
- data/lib/rubik/security/classifier/commands/reboot.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rev.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rg.yaml +7 -0
- data/lib/rubik/security/classifier/commands/rm.yaml +8 -0
- data/lib/rubik/security/classifier/commands/rsync.yaml +12 -0
- data/lib/rubik/security/classifier/commands/rtk.yaml +140 -0
- data/lib/rubik/security/classifier/commands/ruff.yaml +29 -0
- data/lib/rubik/security/classifier/commands/rustfmt.yaml +14 -0
- data/lib/rubik/security/classifier/commands/scp.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sed.yaml +14 -0
- data/lib/rubik/security/classifier/commands/seq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sh.yaml +14 -0
- data/lib/rubik/security/classifier/commands/sha1sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/sha256sum.yaml +7 -0
- data/lib/rubik/security/classifier/commands/shellcheck.yaml +10 -0
- data/lib/rubik/security/classifier/commands/shutdown.yaml +8 -0
- data/lib/rubik/security/classifier/commands/slack.yaml +113 -0
- data/lib/rubik/security/classifier/commands/sleep.yaml +8 -0
- data/lib/rubik/security/classifier/commands/snip.yaml +79 -0
- data/lib/rubik/security/classifier/commands/sort.yaml +10 -0
- data/lib/rubik/security/classifier/commands/ss.yaml +7 -0
- data/lib/rubik/security/classifier/commands/ssh.yaml +41 -0
- data/lib/rubik/security/classifier/commands/stat.yaml +7 -0
- data/lib/rubik/security/classifier/commands/stdbuf.yaml +15 -0
- data/lib/rubik/security/classifier/commands/strace.yaml +20 -0
- data/lib/rubik/security/classifier/commands/strings.yaml +11 -0
- data/lib/rubik/security/classifier/commands/sudo.yaml +24 -0
- data/lib/rubik/security/classifier/commands/systemctl.yaml +58 -0
- data/lib/rubik/security/classifier/commands/systemd-run.yaml +30 -0
- data/lib/rubik/security/classifier/commands/tac.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tail.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tar.yaml +25 -0
- data/lib/rubik/security/classifier/commands/tee.yaml +8 -0
- data/lib/rubik/security/classifier/commands/terraform.yaml +70 -0
- data/lib/rubik/security/classifier/commands/test.yaml +7 -0
- data/lib/rubik/security/classifier/commands/time.yaml +8 -0
- data/lib/rubik/security/classifier/commands/timeout.yaml +18 -0
- data/lib/rubik/security/classifier/commands/top.yaml +7 -0
- data/lib/rubik/security/classifier/commands/touch.yaml +8 -0
- data/lib/rubik/security/classifier/commands/tr.yaml +7 -0
- data/lib/rubik/security/classifier/commands/traceroute.yaml +7 -0
- data/lib/rubik/security/classifier/commands/true.yaml +7 -0
- data/lib/rubik/security/classifier/commands/tsc.yaml +16 -0
- data/lib/rubik/security/classifier/commands/type.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uname.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unexpand.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uniq.yaml +7 -0
- data/lib/rubik/security/classifier/commands/unzip.yaml +10 -0
- data/lib/rubik/security/classifier/commands/uptime.yaml +7 -0
- data/lib/rubik/security/classifier/commands/uv.yaml +156 -0
- data/lib/rubik/security/classifier/commands/wait.yaml +11 -0
- data/lib/rubik/security/classifier/commands/wc.yaml +7 -0
- data/lib/rubik/security/classifier/commands/wget.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whereis.yaml +7 -0
- data/lib/rubik/security/classifier/commands/which.yaml +7 -0
- data/lib/rubik/security/classifier/commands/whoami.yaml +7 -0
- data/lib/rubik/security/classifier/commands/xargs.yaml +26 -0
- data/lib/rubik/security/classifier/commands/xmllint.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yarn.yaml +72 -0
- data/lib/rubik/security/classifier/commands/yes.yaml +7 -0
- data/lib/rubik/security/classifier/commands/yq.yaml +10 -0
- data/lib/rubik/security/classifier/commands/zip.yaml +8 -0
- data/lib/rubik/security/classifier/commands/zsh.yaml +14 -0
- data/lib/rubik/security/classifier/database.rb +216 -0
- data/lib/rubik/security/classifier/matcher/delegation.rb +190 -0
- data/lib/rubik/security/classifier/matcher/options.rb +245 -0
- data/lib/rubik/security/classifier/matcher/subcommands.rb +103 -0
- data/lib/rubik/security/classifier/matcher.rb +249 -0
- data/lib/rubik/security/classifier/models.rb +170 -0
- data/lib/rubik/security/classifier/parser.rb +23 -0
- data/lib/rubik/security/classifier/parsers/bash.rb +280 -0
- data/lib/rubik/security/classifier/parsers/bash_extractors.rb +221 -0
- data/lib/rubik/security/classifier/parsers/powershell.rb +185 -0
- data/lib/rubik/security/classifier/pwsh_rules.rb +224 -0
- data/lib/rubik/security/classifier.rb +25 -0
- data/lib/rubik/security/errors.rb +39 -0
- data/lib/rubik/security/guard.rb +954 -0
- data/lib/rubik/security/redactor.rb +89 -0
- data/lib/rubik/security.rb +11 -0
- data/lib/rubik/session/manager.rb +331 -0
- data/lib/rubik/session/recorder.rb +149 -0
- data/lib/rubik/session/replayer.rb +65 -0
- data/lib/rubik/session.rb +10 -0
- data/lib/rubik/skills/frontmatter.rb +41 -0
- data/lib/rubik/skills/manager.rb +223 -0
- data/lib/rubik/skills/registry.rb +64 -0
- data/lib/rubik/skills/skill.rb +33 -0
- data/lib/rubik/skills/storage.rb +77 -0
- data/lib/rubik/skills/validator.rb +116 -0
- data/lib/rubik/skills.rb +20 -0
- data/lib/rubik/storage/folding.rb +23 -0
- data/lib/rubik/storage/layout.rb +145 -0
- data/lib/rubik/storage/render.rb +97 -0
- data/lib/rubik/storage/service.rb +42 -0
- data/lib/rubik/storage/store.rb +514 -0
- data/lib/rubik/storage/transcript_indexer.rb +116 -0
- data/lib/rubik/storage/uri_reader.rb +168 -0
- data/lib/rubik/storage/vakanuvis/annal_writer.rb +92 -0
- data/lib/rubik/storage/vakanuvis/distill_gate.rb +323 -0
- data/lib/rubik/storage/vakanuvis/gate.rb +250 -0
- data/lib/rubik/storage/vakanuvis/harness.rb +421 -0
- data/lib/rubik/storage/vakanuvis/memory_steward.rb +163 -0
- data/lib/rubik/storage/vakanuvis/service.rb +69 -0
- data/lib/rubik/storage/vakanuvis/vocabulary.rb +99 -0
- data/lib/rubik/storage/vakanuvis/wiki_skeleton.rb +67 -0
- data/lib/rubik/storage/vakanuvis.rb +77 -0
- data/lib/rubik/storage.rb +29 -0
- data/lib/rubik/tools/base.rb +107 -0
- data/lib/rubik/tools/builtins/dmesg.rb +74 -0
- data/lib/rubik/tools/builtins/exec.rb +293 -0
- data/lib/rubik/tools/builtins/files.rb +354 -0
- data/lib/rubik/tools/builtins/goal_ledger.rb +89 -0
- data/lib/rubik/tools/builtins/grep.rb +281 -0
- data/lib/rubik/tools/builtins/interactive.rb +175 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +359 -0
- data/lib/rubik/tools/builtins/lsp.rb +283 -0
- data/lib/rubik/tools/builtins/pruner.rb +88 -0
- data/lib/rubik/tools/builtins/runtime.rb +120 -0
- data/lib/rubik/tools/builtins/send_message.rb +109 -0
- data/lib/rubik/tools/builtins/show_file.rb +84 -0
- data/lib/rubik/tools/builtins/skill.rb +72 -0
- data/lib/rubik/tools/builtins/storage.rb +232 -0
- data/lib/rubik/tools/builtins/subagent.rb +356 -0
- data/lib/rubik/tools/builtins/subagent_lifecycle.rb +72 -0
- data/lib/rubik/tools/builtins/symbols.rb +209 -0
- data/lib/rubik/tools/builtins/synthesis.rb +314 -0
- data/lib/rubik/tools/builtins/task.rb +168 -0
- data/lib/rubik/tools/builtins/unsynthesize.rb +111 -0
- data/lib/rubik/tools/builtins/vakanuvis.rb +161 -0
- data/lib/rubik/tools/builtins.rb +30 -0
- data/lib/rubik/tools/catalog.rb +70 -0
- data/lib/rubik/tools/declaration.rb +90 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +283 -0
- data/lib/rubik/tools/fuzzy_matcher.rb +172 -0
- data/lib/rubik/tools/native_search.rb +180 -0
- data/lib/rubik/tools/output.rb +54 -0
- data/lib/rubik/tools/preflight_runner.rb +209 -0
- data/lib/rubik/tools/registry.rb +247 -0
- data/lib/rubik/tools/result.rb +71 -0
- data/lib/rubik/tools/services.rb +96 -0
- data/lib/rubik/tools/truncator.rb +78 -0
- data/lib/rubik/tools/uri_resolver.rb +195 -0
- data/lib/rubik/tools.rb +14 -0
- data/lib/rubik/ui/approvals.rb +52 -0
- data/lib/rubik/ui/banner.rb +43 -0
- data/lib/rubik/ui/capability.rb +122 -0
- data/lib/rubik/ui/dialog.rb +106 -0
- data/lib/rubik/ui/diff_viewer.rb +78 -0
- data/lib/rubik/ui/frame.rb +95 -0
- data/lib/rubik/ui/markdown_stream.rb +107 -0
- data/lib/rubik/ui/panel.rb +98 -0
- data/lib/rubik/ui/renderer.rb +206 -0
- data/lib/rubik/ui/spinner.rb +120 -0
- data/lib/rubik/ui/statusline.rb +134 -0
- data/lib/rubik/ui/theme.rb +118 -0
- data/lib/rubik/ui.rb +57 -0
- data/lib/rubik/uris.rb +101 -0
- data/lib/rubik/version.rb +5 -0
- data/lib/rubik.rb +65 -0
- metadata +770 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Context
|
|
7
|
+
# Reads repository-level context that the Prompt layer would otherwise
|
|
8
|
+
# have to handle inline. Pulls filesystem reads out of ContextBuilder so
|
|
9
|
+
# the prompt layer is a pure data-transformation layer (no I/O).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# reader = Rubik::Context::RepoContextReader.new(root_dir: Dir.pwd)
|
|
13
|
+
# ctx = reader.read # => { protocol_xml:, workspace_summary:, git_branch: }
|
|
14
|
+
#
|
|
15
|
+
# All methods are safe to call on a non-git / empty directory; they
|
|
16
|
+
# return nil / empty values rather than raising.
|
|
17
|
+
class RepoContextReader
|
|
18
|
+
DEFAULT_PROTOCOL_CANDIDATES = [
|
|
19
|
+
"AGENTS.md",
|
|
20
|
+
".rubik/AGENTS.md",
|
|
21
|
+
"RULES.md",
|
|
22
|
+
"CLAUDE.md",
|
|
23
|
+
".cursorrules"
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
# Entry-point filenames that the workspace-summary considers worth
|
|
27
|
+
# surfacing when the file count is large.
|
|
28
|
+
ENTRY_POINT_BASENAMES = [
|
|
29
|
+
"Gemfile", "package.json", "Cargo.toml", "go.mod", "Makefile", "pyproject.toml", "Rakefile", "index.html", "README.md"
|
|
30
|
+
].freeze
|
|
31
|
+
|
|
32
|
+
def initialize(root_dir: Dir.pwd, protocol_candidates: DEFAULT_PROTOCOL_CANDIDATES)
|
|
33
|
+
@root_dir = root_dir
|
|
34
|
+
@protocol_candidates = protocol_candidates
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns a Hash with three keys, each pre-extracted and pre-rendered
|
|
38
|
+
# so the prompt layer never has to touch the filesystem.
|
|
39
|
+
def read
|
|
40
|
+
{
|
|
41
|
+
protocol_xml: read_protocol_xml,
|
|
42
|
+
workspace_summary: read_workspace_summary,
|
|
43
|
+
git_branch: read_git_branch
|
|
44
|
+
}
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# List of files that match the protocol-file candidates.
|
|
48
|
+
def find_protocol_files
|
|
49
|
+
@protocol_candidates.filter_map do |candidate|
|
|
50
|
+
full_path = File.join(@root_dir, candidate)
|
|
51
|
+
{ path: candidate, full_path: full_path } if File.file?(full_path)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Render the matched protocol files into an XML block.
|
|
56
|
+
def read_protocol_xml
|
|
57
|
+
files = find_protocol_files
|
|
58
|
+
return "" if files.empty?
|
|
59
|
+
|
|
60
|
+
files.map do |f|
|
|
61
|
+
content = Rubik::Platform.read_text(f[:full_path]).strip
|
|
62
|
+
<<~XML
|
|
63
|
+
<protocol source="#{f[:path]}">
|
|
64
|
+
#{content}
|
|
65
|
+
</protocol>
|
|
66
|
+
XML
|
|
67
|
+
end.join("\n").strip
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns a Hash describing the project layout. The prompt layer
|
|
71
|
+
# can decide how to render it (the existing `build_workspace_xml`
|
|
72
|
+
# in ContextBuilder does, and the new path can be passed in).
|
|
73
|
+
def read_workspace_summary
|
|
74
|
+
{
|
|
75
|
+
project_name: File.basename(@root_dir),
|
|
76
|
+
file_count: nil, # filled in by the caller's context_index
|
|
77
|
+
languages: [], # filled in by the caller's context_index
|
|
78
|
+
files: {} # filled in by the caller's context_index
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Read the current git branch from .git/HEAD. Returns nil if not a
|
|
83
|
+
# git repo; in detached-HEAD state returns the raw commit SHA.
|
|
84
|
+
def read_git_branch
|
|
85
|
+
git_dir = resolve_git_dir
|
|
86
|
+
return nil unless git_dir
|
|
87
|
+
|
|
88
|
+
head_file = File.join(git_dir, "HEAD")
|
|
89
|
+
return nil unless File.exist?(head_file)
|
|
90
|
+
|
|
91
|
+
content = Rubik::Platform.read_text(head_file).strip
|
|
92
|
+
return nil if content.empty?
|
|
93
|
+
|
|
94
|
+
# HEAD is either a ref like "ref: refs/heads/main" or a raw SHA
|
|
95
|
+
prefix = "ref: refs/heads/"
|
|
96
|
+
if content.start_with?(prefix)
|
|
97
|
+
content[prefix.length..]
|
|
98
|
+
else
|
|
99
|
+
# detached HEAD: return the raw SHA
|
|
100
|
+
content
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
# Worktrees/submodules store a `.git` FILE: "gitdir: <path>" — HEAD
|
|
107
|
+
# lives in that directory. Plain clones have a .git directory.
|
|
108
|
+
def resolve_git_dir
|
|
109
|
+
dot_git = File.join(@root_dir, ".git")
|
|
110
|
+
if File.directory?(dot_git)
|
|
111
|
+
dot_git
|
|
112
|
+
elsif File.file?(dot_git)
|
|
113
|
+
target = Rubik::Platform.read_text(dot_git).strip.delete_prefix("gitdir:").strip
|
|
114
|
+
File.expand_path(target, @root_dir) unless target.empty?
|
|
115
|
+
end
|
|
116
|
+
rescue SystemCallError, ArgumentError
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require_relative "../platform"
|
|
6
|
+
require_relative "git_lister"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Context
|
|
10
|
+
class Scanner
|
|
11
|
+
EXTENSION_MAP = {
|
|
12
|
+
".rb" => "ruby",
|
|
13
|
+
".py" => "python",
|
|
14
|
+
".go" => "go",
|
|
15
|
+
".rs" => "rust",
|
|
16
|
+
".c" => "c",
|
|
17
|
+
".h" => "c",
|
|
18
|
+
".cpp" => "cpp",
|
|
19
|
+
".hpp" => "cpp",
|
|
20
|
+
".cc" => "cpp",
|
|
21
|
+
".zig" => "zig",
|
|
22
|
+
".js" => "javascript",
|
|
23
|
+
".jsx" => "javascript",
|
|
24
|
+
".ts" => "typescript",
|
|
25
|
+
".tsx" => "typescript",
|
|
26
|
+
".html" => "html",
|
|
27
|
+
".htm" => "html",
|
|
28
|
+
".css" => "css",
|
|
29
|
+
".json" => "json",
|
|
30
|
+
".yaml" => "yaml",
|
|
31
|
+
".yml" => "yaml",
|
|
32
|
+
".md" => "markdown",
|
|
33
|
+
".sh" => "shell",
|
|
34
|
+
".java" => "java",
|
|
35
|
+
".kt" => "kotlin",
|
|
36
|
+
".swift" => "swift"
|
|
37
|
+
}.freeze
|
|
38
|
+
|
|
39
|
+
BUILD_SIGNATURES = {
|
|
40
|
+
"Gemfile" => "ruby",
|
|
41
|
+
"pyproject.toml" => "python",
|
|
42
|
+
"go.mod" => "go",
|
|
43
|
+
"Cargo.toml" => "rust",
|
|
44
|
+
"Makefile" => "c",
|
|
45
|
+
"build.zig" => "zig",
|
|
46
|
+
"package.json" => "javascript"
|
|
47
|
+
}.freeze
|
|
48
|
+
|
|
49
|
+
DEFAULT_IGNORES = [
|
|
50
|
+
".rubik/**",
|
|
51
|
+
".git/**",
|
|
52
|
+
# GitNexus parse cache: thousands of binary index files; indexing
|
|
53
|
+
# them is pure cost and their non-UTF-8 bytes are a grep hazard.
|
|
54
|
+
".gitnexus/**",
|
|
55
|
+
".ua/**",
|
|
56
|
+
".trash*/**",
|
|
57
|
+
"**/.trash*/**",
|
|
58
|
+
".turbo/**",
|
|
59
|
+
".sst/**",
|
|
60
|
+
".next/**",
|
|
61
|
+
".nuxt/**",
|
|
62
|
+
"node_modules/**",
|
|
63
|
+
"vendor/**",
|
|
64
|
+
"tmp/**",
|
|
65
|
+
"dist/**",
|
|
66
|
+
"build/**",
|
|
67
|
+
".cache/**",
|
|
68
|
+
"coverage/**"
|
|
69
|
+
].freeze
|
|
70
|
+
|
|
71
|
+
# Subtrees whose ignore pattern is "<path>/**" (optionally with a
|
|
72
|
+
# "**/" prefix) are pruned whole when the stem's components occur
|
|
73
|
+
# contiguously in a directory's path. NOTE: this is intentionally
|
|
74
|
+
# STRONGER than the legacy per-file filter — Ruby's fnmatch treats a
|
|
75
|
+
# trailing `**` as a single segment, so patterns like "node_modules/**"
|
|
76
|
+
# historically rejected only first-level entries while deeper files
|
|
77
|
+
# leaked into the index. The walker applies the pattern's plain meaning
|
|
78
|
+
# (whole subtree ignored), matching real gitignore semantics.
|
|
79
|
+
PRUNABLE_PATTERN = %r{\A(?:\*\*/)?(.+)/\*\*\z}
|
|
80
|
+
# Also hard-rejected inside `ignored?`; pruned here before descending.
|
|
81
|
+
HARD_PRUNE_ROOTS = [".git", ".rubik", ".ua", ".turbo", ".sst", ".next", ".nuxt"].freeze
|
|
82
|
+
|
|
83
|
+
attr_reader :root_dir, :additional_roots
|
|
84
|
+
attr_accessor :custom_includes, :custom_excludes
|
|
85
|
+
|
|
86
|
+
def initialize(root_dir: Dir.pwd, additional_roots: [], includes: [], excludes: [], native: nil)
|
|
87
|
+
@root_dir = File.expand_path(root_dir)
|
|
88
|
+
@additional_roots = (additional_roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
|
|
89
|
+
@custom_includes = includes || []
|
|
90
|
+
@custom_excludes = excludes || []
|
|
91
|
+
@native = native
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def additional_roots=(roots)
|
|
95
|
+
@additional_roots = (roots || []).map { |r| File.expand_path(r.to_s, @root_dir) }.uniq
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Builds the file info index. Two listing strategies per root:
|
|
99
|
+
# - `git ls-files` fast path (when enabled and git answers): C-speed
|
|
100
|
+
# walk + gitignore handling; entries still pass the Ruby ignore
|
|
101
|
+
# filters (DEFAULT_IGNORES may cover paths absent from the target
|
|
102
|
+
# repo's .gitignore) and are ordered exactly like the walker.
|
|
103
|
+
# - the pruned recursive walker as fallback.
|
|
104
|
+
# Pass a previous scan result (rel_path => info hash) to enable the
|
|
105
|
+
# stat-dirty model: stat-clean entries reuse the previous hash (which
|
|
106
|
+
# may itself be nil — content never hashed), only stat-changed files
|
|
107
|
+
# are read and re-hashed. Cold entries start hash-less.
|
|
108
|
+
def scan(previous = {})
|
|
109
|
+
files = {}
|
|
110
|
+
|
|
111
|
+
# 1. Scan primary root
|
|
112
|
+
primary_ignores = DEFAULT_IGNORES + parse_gitignore(@root_dir) + @custom_excludes
|
|
113
|
+
files.merge!(scan_root(@root_dir, ".", primary_ignores, previous))
|
|
114
|
+
|
|
115
|
+
# 2. Scan additional roots
|
|
116
|
+
@additional_roots.each do |extra_root|
|
|
117
|
+
next unless File.directory?(extra_root)
|
|
118
|
+
|
|
119
|
+
extra_ignores = DEFAULT_IGNORES + parse_gitignore(extra_root) + @custom_excludes
|
|
120
|
+
files.merge!(scan_root(extra_root, relative_prefix(extra_root), extra_ignores, previous))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
files
|
|
124
|
+
rescue StandardError => e
|
|
125
|
+
Rubik::Log.warn(:context, "context scan aborted partway (#{e.class}: #{e.message}); returning partial index")
|
|
126
|
+
files || {}
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def detect_languages(files_hash)
|
|
130
|
+
languages = Set.new
|
|
131
|
+
|
|
132
|
+
files_hash.each_value do |info|
|
|
133
|
+
languages << info[:language] if info[:language]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
BUILD_SIGNATURES.each do |build_file, lang|
|
|
137
|
+
languages << lang if File.exist?(File.join(@root_dir, build_file))
|
|
138
|
+
@additional_roots.each do |extra_root|
|
|
139
|
+
languages << lang if File.exist?(File.join(extra_root, build_file))
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
languages.to_a.sort
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Public single-file probe. Delegates to entry_for so the persisted
|
|
147
|
+
# info (notably the microsecond mtime) stays unit-identical with scan
|
|
148
|
+
# results — mixed units would make incremental_scan flag every
|
|
149
|
+
# update_file-touched entry as modified. The probe always computes a
|
|
150
|
+
# real content hash: its caller (Index#update_file) compares hashes to
|
|
151
|
+
# decide reindexing, and a lazy nil would mask every change.
|
|
152
|
+
def inspect_file(rel_path, full_path)
|
|
153
|
+
entry_for(rel_path, full_path, {}, force_hash: true)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
# Stat-dirty model (à la git): a previous entry whose size and mtime
|
|
159
|
+
# (microsecond precision) both match is NEVER read from disk — its
|
|
160
|
+
# content hash is reused verbatim, including when it is nil (content
|
|
161
|
+
# never hashed). Only a stat change triggers a read + SHA-256, and a
|
|
162
|
+
# cold entry (no previous) starts hash-less. force_hash: true computes
|
|
163
|
+
# a real hash regardless (single-file probes that must detect content
|
|
164
|
+
# changes). Residual race — a same-size, same-microsecond-mtime rewrite
|
|
165
|
+
# between scans — is the deliberate tradeoff the git model makes.
|
|
166
|
+
def entry_for(rel_path, full_path, previous, stat: nil, force_hash: false)
|
|
167
|
+
stat ||= File.stat(full_path)
|
|
168
|
+
ext = File.extname(full_path)
|
|
169
|
+
lang = EXTENSION_MAP[ext] || BUILD_SIGNATURES[File.basename(full_path)]
|
|
170
|
+
mtime_us = mtime_micros(stat)
|
|
171
|
+
|
|
172
|
+
prev = previous.is_a?(Hash) ? previous[rel_path] : nil
|
|
173
|
+
# Cold entry (no previous): content unknown — defer the read until a
|
|
174
|
+
# future scan observes a stat change (git's stat-dirty model), so the
|
|
175
|
+
# implicit else is nil.
|
|
176
|
+
hash =
|
|
177
|
+
if !force_hash && prev.is_a?(Hash) &&
|
|
178
|
+
prev[:size] == stat.size && prev[:mtime] == mtime_us
|
|
179
|
+
# Stat-clean: reuse verbatim — including a nil (never hashed).
|
|
180
|
+
prev[:hash]
|
|
181
|
+
elsif force_hash || prev.is_a?(Hash)
|
|
182
|
+
# Stat-dirty (or a probe that must see content): hash eagerly.
|
|
183
|
+
content = begin
|
|
184
|
+
Rubik::Platform.read_text(full_path)
|
|
185
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
186
|
+
""
|
|
187
|
+
end
|
|
188
|
+
Digest::SHA256.hexdigest(content)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
{
|
|
192
|
+
size: stat.size,
|
|
193
|
+
mtime: mtime_us,
|
|
194
|
+
hash: hash,
|
|
195
|
+
language: lang
|
|
196
|
+
}
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def mtime_micros(stat)
|
|
200
|
+
(stat.mtime.to_r * 1_000_000).to_i
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def walk_dir(base_dir, rel_prefix, key_prefix, ignore_patterns, previous, needles)
|
|
204
|
+
current = rel_prefix.empty? ? base_dir : "#{base_dir}/#{rel_prefix}"
|
|
205
|
+
out = {}
|
|
206
|
+
|
|
207
|
+
# Sorted like the historical recursive Dir.glob walk so that index
|
|
208
|
+
# insertion order (and anything downstream relying on it, e.g.
|
|
209
|
+
# workspace_xml entry_points listing) stays deterministic.
|
|
210
|
+
entries = begin
|
|
211
|
+
Dir.foreach(current).sort - [".", ".."]
|
|
212
|
+
rescue SystemCallError
|
|
213
|
+
[]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
entries.each do |entry|
|
|
217
|
+
entry = entry.tr("\\", "/")
|
|
218
|
+
rel = rel_prefix.empty? ? entry : "#{rel_prefix}/#{entry}"
|
|
219
|
+
|
|
220
|
+
begin
|
|
221
|
+
lst = File.lstat("#{base_dir}/#{rel}")
|
|
222
|
+
rescue Errno::ENOENT
|
|
223
|
+
next
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if lst.directory? && !lst.symlink?
|
|
227
|
+
next if subtree_ignored?(rel, needles)
|
|
228
|
+
|
|
229
|
+
out.merge!(walk_dir(base_dir, rel, key_prefix, ignore_patterns, previous, needles))
|
|
230
|
+
else
|
|
231
|
+
# Symlinked FILES are still listed (File.stat follows the link),
|
|
232
|
+
# matching the old Dir.glob behavior; only directory descent is
|
|
233
|
+
# withheld — glob never followed dir symlinks either, and doing
|
|
234
|
+
# so would allow root escapes and cyclic recursion.
|
|
235
|
+
st = begin
|
|
236
|
+
File.stat("#{base_dir}/#{rel}")
|
|
237
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
238
|
+
nil
|
|
239
|
+
end
|
|
240
|
+
next unless st&.file?
|
|
241
|
+
next if ignored?(rel, ignore_patterns)
|
|
242
|
+
next unless included?(rel, @custom_includes)
|
|
243
|
+
|
|
244
|
+
rel_key = key_prefix == "." ? rel : "#{key_prefix}/#{rel}"
|
|
245
|
+
out[rel_key] = entry_for(rel_key, "#{base_dir}/#{rel}", previous, stat: st)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
out
|
|
250
|
+
rescue StandardError => e
|
|
251
|
+
Rubik::Log.warn(:context, "directory walk failed under #{base_dir} (#{e.class}: #{e.message}); keeping partial results")
|
|
252
|
+
out
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def subtree_ignored?(dir_rel, needles)
|
|
256
|
+
comps = dir_rel.split("/")
|
|
257
|
+
return true if HARD_PRUNE_ROOTS.include?(comps.first)
|
|
258
|
+
|
|
259
|
+
needles.any? { |needle| contiguous_components?(comps, needle) }
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# "<dir>/**" ignore patterns (see PRUNABLE_PATTERN) pre-split into
|
|
263
|
+
# needle component arrays, once per scan root. Matching a directory
|
|
264
|
+
# then costs plain array compares — no per-ancestor regex evaluation
|
|
265
|
+
# and no re-splitting of the pattern's stem for every file visited.
|
|
266
|
+
def prunable_needles(ignore_patterns)
|
|
267
|
+
ignore_patterns.filter_map do |pattern|
|
|
268
|
+
next unless (m = PRUNABLE_PATTERN.match(pattern))
|
|
269
|
+
|
|
270
|
+
m[1].split("/")
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def contiguous_components?(haystack, needle)
|
|
275
|
+
return true if haystack == needle
|
|
276
|
+
return false if haystack.size < needle.size
|
|
277
|
+
|
|
278
|
+
(0..(haystack.size - needle.size)).any? do |offset|
|
|
279
|
+
haystack[offset, needle.size] == needle
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Key prefix for files found under an additional root, mirroring the
|
|
284
|
+
# historical layout: subtree roots get their path relative to the main
|
|
285
|
+
# root; unrelated roots fall back to their absolute path.
|
|
286
|
+
def relative_prefix(dir)
|
|
287
|
+
Pathname.new(dir).relative_path_from(Pathname.new(@root_dir)).to_s.gsub("\\", "/")
|
|
288
|
+
rescue ArgumentError
|
|
289
|
+
dir.gsub("\\", "/")
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# One recursive directory walk with EARLY SUBTREE PRUNING: instead of
|
|
293
|
+
# enumerating ignored trees (.git/, node_modules/, vendor/ – thousands
|
|
294
|
+
# of entries on real projects) only to reject each file afterwards,
|
|
295
|
+
# descent into a matching subtree never happens. Pruning applies the
|
|
296
|
+
# ignore pattern's plain whole-subtree meaning (see PRUNABLE_PATTERN)
|
|
297
|
+
# and `ignored?` still filters every file that IS visited. `prefix` is
|
|
298
|
+
# the key namespace for this root ("." for the primary project root).
|
|
299
|
+
def scan_root(base_dir, prefix, ignore_patterns, previous)
|
|
300
|
+
needles = prunable_needles(ignore_patterns)
|
|
301
|
+
if git_listing? && @custom_includes.empty? && (listed = GitLister.files(base_dir))
|
|
302
|
+
return index_listed(base_dir, prefix, listed, ignore_patterns, previous, needles)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
walk_dir(base_dir, "", prefix, ignore_patterns, previous, needles)
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
Rubik::Log.warn(:context, "root scan failed for #{base_dir} (#{e.class}: #{e.message}); root omitted from index")
|
|
308
|
+
{}
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Fast path over a `git ls-files` listing. The listing is stat-filled,
|
|
312
|
+
# re-filtered through the Ruby-side ignore rules (ls-files only knows
|
|
313
|
+
# the repo's own gitignore: DEFAULT_IGNORES subtrees may be missing
|
|
314
|
+
# from it, and tracked files ignore gitignore entirely), ordered
|
|
315
|
+
# identically to walk_dir's depth-first-per-directory sort, and
|
|
316
|
+
# ancestor-pruned with the same whole-subtree semantics as the walker.
|
|
317
|
+
# Residual drift the OTHER way: gitignore features the walker's crude
|
|
318
|
+
# fnmatch translation lacks (negations, anchored patterns) can make the
|
|
319
|
+
# fast path list fewer files.
|
|
320
|
+
def index_listed(base_dir, key_prefix, rel_paths, ignore_patterns, previous, needles)
|
|
321
|
+
out = {}
|
|
322
|
+
# Verdict per ancestor directory, computed on first sight: walker
|
|
323
|
+
# order keeps siblings adjacent, so each distinct directory's ignore
|
|
324
|
+
# status is evaluated once per scan instead of once per file per
|
|
325
|
+
# depth. subtree_ignored? is a pure function of (dir, needles), so
|
|
326
|
+
# the memo is exact.
|
|
327
|
+
dir_verdicts = Hash.new { |h, dir| h[dir] = subtree_ignored?(dir, needles) }
|
|
328
|
+
order_like_walker(rel_paths).each do |rel|
|
|
329
|
+
# PERF: zero-allocation ancestor check using string slicing instead of Array#split and Array#join
|
|
330
|
+
ignored_dir = false
|
|
331
|
+
idx = 0
|
|
332
|
+
while (idx = rel.index("/", idx))
|
|
333
|
+
if dir_verdicts[rel[0, idx]]
|
|
334
|
+
ignored_dir = true
|
|
335
|
+
break
|
|
336
|
+
end
|
|
337
|
+
idx += 1
|
|
338
|
+
end
|
|
339
|
+
next if ignored_dir
|
|
340
|
+
next if ignored?(rel, ignore_patterns)
|
|
341
|
+
next unless included?(rel, @custom_includes)
|
|
342
|
+
|
|
343
|
+
full = "#{base_dir}/#{rel}"
|
|
344
|
+
st = begin
|
|
345
|
+
File.stat(full)
|
|
346
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
347
|
+
nil
|
|
348
|
+
end
|
|
349
|
+
next unless st&.file?
|
|
350
|
+
|
|
351
|
+
rel_key = key_prefix == "." ? rel : "#{key_prefix}/#{rel}"
|
|
352
|
+
out[rel_key] = entry_for(rel_key, full, previous, stat: st)
|
|
353
|
+
end
|
|
354
|
+
out
|
|
355
|
+
rescue StandardError => e
|
|
356
|
+
Rubik::Log.warn(:context, "git ls-files fast path failed for #{base_dir} (#{e.class}: #{e.message}); keeping partial index")
|
|
357
|
+
out
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def git_listing?
|
|
361
|
+
GitLister.enabled?(@native)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Sorts flat relative paths into the exact emission order of walk_dir:
|
|
365
|
+
# every level name-sorts its entries and a directory descends in place,
|
|
366
|
+
# so whole subtrees interleave positionally — which is precisely an
|
|
367
|
+
# element-wise compare of the component arrays ("a/x" before "a.txt",
|
|
368
|
+
# since "a" < "a.txt" at the first component; "lib/deep/core.rb" before
|
|
369
|
+
# "lib/util.rb" via "deep" < "util").
|
|
370
|
+
def order_like_walker(paths)
|
|
371
|
+
paths.sort_by { |p| p.split("/") }
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def parse_gitignore(dir = @root_dir)
|
|
375
|
+
gitignore_path = File.join(dir, ".gitignore")
|
|
376
|
+
return [] unless File.exist?(gitignore_path)
|
|
377
|
+
|
|
378
|
+
patterns = File.readlines(gitignore_path, chomp: true).map(&:strip).reject do |line|
|
|
379
|
+
line.empty? || line.start_with?("#")
|
|
380
|
+
end
|
|
381
|
+
patterns.map do |pattern|
|
|
382
|
+
pattern.end_with?("/") ? "#{pattern}**" : pattern
|
|
383
|
+
end
|
|
384
|
+
rescue Errno::ENOENT, Errno::EACCES
|
|
385
|
+
[]
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def ignored?(path, ignore_patterns)
|
|
389
|
+
return true if path == ".rubik" || path.start_with?(".rubik/")
|
|
390
|
+
return true if path == ".git" || path.start_with?(".git/")
|
|
391
|
+
|
|
392
|
+
# PERF: precompile simple globs to memoized Regexp objects to avoid
|
|
393
|
+
# repeated File.fnmatch? overhead across thousands of files; exotic
|
|
394
|
+
# globs keep the fnmatch semantics untouched.
|
|
395
|
+
compile_ignore_regexes(ignore_patterns).any? { |r| pattern_match?(r, path) }
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Translates a glob into a Regexp only where the mapping is exact: no
|
|
399
|
+
# character classes, no leading-slash anchoring, and `**` only ever a
|
|
400
|
+
# whole path segment (leading "**/", mid "/**/", trailing "/**").
|
|
401
|
+
# Anything else returns nil and pattern_match? falls back to
|
|
402
|
+
# File.fnmatch?, whose segment rules the Regexp form cannot express
|
|
403
|
+
# (fnmatch "a**b" never crosses "/" and "**/x" also matches bare "x").
|
|
404
|
+
def compile_ignore_regexes(ignore_patterns)
|
|
405
|
+
@compiled_ignore_regexes ||= {}
|
|
406
|
+
@compiled_ignore_regexes[ignore_patterns] ||= ignore_patterns.map do |pat|
|
|
407
|
+
simple_glob_regex(pat) || pat
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def simple_glob_regex(pat)
|
|
412
|
+
return nil if pat.empty? || pat.start_with?("/") || pat.include?("[")
|
|
413
|
+
return nil unless pat.split("/").all? { |seg| !seg.include?("*") || seg == "**" || !seg.include?("**") }
|
|
414
|
+
|
|
415
|
+
src = Regexp.escape(pat)
|
|
416
|
+
src = src.sub(%r{\A\\\*\\\*/}, "") # leading "**/": the (?:.*/)? wrapper matches zero dirs too
|
|
417
|
+
src = src.gsub("/\\*\\*/", "/(?:.*/)?") # mid "/**/": zero or more directories
|
|
418
|
+
src = src.gsub("\\*\\*", ".*") # trailing "/**" or lone "**"
|
|
419
|
+
src = src.gsub("\\*", "[^/]*").gsub("\\?", "[^/]")
|
|
420
|
+
Regexp.new("\\A(?:.*/)?#{src}\\z")
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def pattern_match?(pattern_or_regex, path)
|
|
424
|
+
if pattern_or_regex.is_a?(Regexp)
|
|
425
|
+
pattern_or_regex.match?(path)
|
|
426
|
+
else
|
|
427
|
+
File.fnmatch?(pattern_or_regex, path, File::FNM_PATHNAME | File::FNM_DOTMATCH) ||
|
|
428
|
+
File.fnmatch?("**/#{pattern_or_regex}", path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def included?(path, patterns)
|
|
433
|
+
return true if patterns.nil? || patterns.empty?
|
|
434
|
+
|
|
435
|
+
patterns.any? { |pattern| pattern_match?(pattern, path) }
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|