rosett-ai 1.3.3
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/.ai-provenance.yml +119 -0
- data/.debride_whitelist +186 -0
- data/.fasterer.yml +29 -0
- data/.mdl_style.rb +10 -0
- data/.mdlrc +3 -0
- data/.mutant.yml +49 -0
- data/.namespace-allowlist +42 -0
- data/.reek.yml +1040 -0
- data/.rosett-ai/config.yml +3 -0
- data/.rspec +5 -0
- data/.rubocop.yml +380 -0
- data/.ruby-version +1 -0
- data/.yamllint +51 -0
- data/.yardopts +12 -0
- data/AI-DISCLOSURE.md +48 -0
- data/CHANGELOG.md +519 -0
- data/CLAUDE.md +141 -0
- data/CONTRIBUTING.md +734 -0
- data/INSTALL.md +154 -0
- data/LICENSE +674 -0
- data/LICENSE.md +675 -0
- data/QUICKSTART.md +73 -0
- data/README.md +366 -0
- data/Rakefile +200 -0
- data/SECURITY.md +114 -0
- data/bin/rai +1 -0
- data/cliff.toml +52 -0
- data/conf/adopt_redactions.yml +8 -0
- data/conf/behaviour/.gitkeep +0 -0
- data/conf/compliance/cra_rules.yml +25 -0
- data/conf/compliance/license_rules.yml +20 -0
- data/conf/design/aaif_alignment.yml +181 -0
- data/conf/design/ab_testing.yml +172 -0
- data/conf/design/accessibility.yml +84 -0
- data/conf/design/ai_authorship.yml +210 -0
- data/conf/design/ai_provenance.yml +224 -0
- data/conf/design/ai_tool_configuration.yml +207 -0
- data/conf/design/architecture.yml +139 -0
- data/conf/design/autocompletion.yml +115 -0
- data/conf/design/backward_compatibility.yml +112 -0
- data/conf/design/behaviour_composition.yml +246 -0
- data/conf/design/build_rake_extraction.yml +57 -0
- data/conf/design/ci_pipeline.yml +100 -0
- data/conf/design/claude_code_configuration.yml +157 -0
- data/conf/design/compiler.yml +128 -0
- data/conf/design/comply.yml +153 -0
- data/conf/design/content_packs.yml +84 -0
- data/conf/design/desktop_integration.yml +289 -0
- data/conf/design/distribution.yml +216 -0
- data/conf/design/doctor.yml +184 -0
- data/conf/design/documentation.yml +152 -0
- data/conf/design/engine_architecture.yml +257 -0
- data/conf/design/error_handling.yml +103 -0
- data/conf/design/feature_flags.yml +142 -0
- data/conf/design/git_hooks.yml +165 -0
- data/conf/design/gui_plugins.yml +475 -0
- data/conf/design/i18n.yml +84 -0
- data/conf/design/integration_testing.yml +56 -0
- data/conf/design/licensing_system.yml +88 -0
- data/conf/design/lifecycle_management.yml +208 -0
- data/conf/design/mcp_integration.yml +207 -0
- data/conf/design/mcp_settings.yml +126 -0
- data/conf/design/migration.yml +56 -0
- data/conf/design/monitoring_observability.yml +194 -0
- data/conf/design/namespace_cleanup.yml +145 -0
- data/conf/design/plugin_test_segregation.yml +145 -0
- data/conf/design/policy_management.yml +229 -0
- data/conf/design/project_management.yml +183 -0
- data/conf/design/rai_mcp_asset_discovery.yml +164 -0
- data/conf/design/rai_mcp_server.yml +605 -0
- data/conf/design/release_management.yml +117 -0
- data/conf/design/retrofit.yml +199 -0
- data/conf/design/retrospective_analyzer.yml +79 -0
- data/conf/design/scope_hierarchy.yml +352 -0
- data/conf/design/security.yml +115 -0
- data/conf/design/session_retrospective.yml +85 -0
- data/conf/design/smart_ui_feedback.yml +89 -0
- data/conf/design/structured_logging.yml +148 -0
- data/conf/design/styles.yml +123 -0
- data/conf/design/test_peer_review.yml +89 -0
- data/conf/design/testing.yml +136 -0
- data/conf/design/threat_model.yml +108 -0
- data/conf/design/ui_framework.yml +111 -0
- data/conf/design/usage_optimization.yml +122 -0
- data/conf/design/version_management.yml +60 -0
- data/conf/design/workflow.yml +227 -0
- data/conf/mcp/server_defaults.yml +42 -0
- data/conf/mcp/trust.yml +21 -0
- data/conf/packaging/core.yml +12 -0
- data/conf/packaging/gtk4.yml +11 -0
- data/conf/packaging/qt6.yml +11 -0
- data/conf/policy/default_deny_list.yml +197 -0
- data/conf/review/cli-command-audit.yml +857 -0
- data/conf/review/design-docs.yml +1064 -0
- data/conf/review/design-questionnaire.yml +153 -0
- data/conf/review/questionnaire.yml +146 -0
- data/conf/review/rosett-ai-core.yml +2919 -0
- data/conf/schemas/ai_config_schema.json +73 -0
- data/conf/schemas/behaviour_schema.json +132 -0
- data/conf/schemas/compliance_rule_schema.json +63 -0
- data/conf/schemas/content_pack_manifest_schema.json +51 -0
- data/conf/schemas/design_schema.json +210 -0
- data/conf/schemas/engine_manifest_schema.json +144 -0
- data/conf/schemas/lockfile_schema.json +74 -0
- data/conf/schemas/mcp_server_schema.json +48 -0
- data/conf/schemas/packaging_schema.json +70 -0
- data/conf/schemas/policy_schema.json +85 -0
- data/conf/schemas/provenance_schema.json +84 -0
- data/conf/schemas/rai_config_schema.json +56 -0
- data/conf/schemas/rai_project_schema.json +20 -0
- data/conf/schemas/scope_hierarchy_schema.json +49 -0
- data/conf/schemas/target_schema.json +67 -0
- data/conf/schemas/tooling_schema.json +65 -0
- data/conf/schemas/workflow_schema.json +112 -0
- data/conf/targets/agents_md.yml +17 -0
- data/conf/targets/claude.yml +12 -0
- data/conf/tooling/tools.yml +58 -0
- data/dist/rosett-ai-mcp.service +48 -0
- data/dist/rosett-ai-mcp.yml.default +45 -0
- data/doc/AAIF_POSITIONING.md +58 -0
- data/doc/ADOPT.md +224 -0
- data/doc/AI_PROVENANCE.md +139 -0
- data/doc/ARCHITECTURE.md +920 -0
- data/doc/BEHAVIOUR.md +409 -0
- data/doc/BUILD.md +138 -0
- data/doc/CI_CD_RECIPES.md +171 -0
- data/doc/CLAUDE_SESSIONS_MOVED.md +16 -0
- data/doc/COMMAND_ANALYSIS.md +229 -0
- data/doc/CONFIGURATION.md +281 -0
- data/doc/DESIGN_AUDIT.md +235 -0
- data/doc/DESIGN_PEER_REVIEW.md +771 -0
- data/doc/DESKTOP.md +447 -0
- data/doc/ENGINES.md +567 -0
- data/doc/ENGINE_DEVELOPMENT_GUIDE.md +417 -0
- data/doc/FEATURE_AUDIT.md +218 -0
- data/doc/IMPLEMENTATION_PLAN.md +669 -0
- data/doc/INCIDENT_REPORT_2026-02-02.md +251 -0
- data/doc/MIGRATION_GUIDE.md +88 -0
- data/doc/PACKAGING.md +232 -0
- data/doc/PROJECT_DASHBOARD.md +153 -0
- data/doc/PULP_DEPLOYMENT.md +164 -0
- data/doc/QUALITY_FIX_SUMMARY.md +110 -0
- data/doc/QUICK_START.md +162 -0
- data/doc/REEK_CONFIGURATION.md +166 -0
- data/doc/REFERENCE.md +253 -0
- data/doc/REFERENCES.md +324 -0
- data/doc/SECURITY_REVIEW_CHECKLIST.md +72 -0
- data/doc/SESSION_2026-02-28_GTK4_HARDENING.md +359 -0
- data/doc/SETUP.md +202 -0
- data/doc/TEST_PEER_REVIEW.md +152 -0
- data/doc/THREAT_MODEL.md +230 -0
- data/doc/USAGE.md +545 -0
- data/doc/USER_MANUAL.md +585 -0
- data/doc/ai_test_review_checklist.md +110 -0
- data/doc/changes/2026-02-18-packaging-fpm.md +155 -0
- data/doc/changes/2026-02-19-testing-infrastructure.md +221 -0
- data/doc/changes/2026-02-20-security-implementation.md +281 -0
- data/doc/changes/2026-02-20-styles-implementation.md +220 -0
- data/doc/changes/2026-02-21-architecture-completion.md +95 -0
- data/doc/changes/2026-02-21-architecture-ui-layer.md +253 -0
- data/doc/changes/2026-02-21-cc-config-implementation.md +108 -0
- data/doc/changes/2026-02-21-ci-pipeline-implementation.md +214 -0
- data/doc/changes/2026-02-21-compiler-multi-target-pipeline.md +241 -0
- data/doc/changes/2026-02-21-config-design-show-commands.md +61 -0
- data/doc/changes/2026-02-21-design-implementation-overview.md +455 -0
- data/doc/changes/2026-02-21-lifecycle-management.md +196 -0
- data/doc/changes/2026-02-21-path-resolver.md +128 -0
- data/doc/changes/2026-02-24-ci-tmpdir-mutant-fetch.md +45 -0
- data/doc/changes/2026-03-01-ci-bundler-strategy.md +120 -0
- data/doc/changes/2026-03-20-security-hardening-phase2.md +163 -0
- data/doc/context/SESSION-HANDOFF.md +69 -0
- data/doc/context/ai-engine-usage-trends-2026.md +80 -0
- data/doc/context/plan-pluggable-engines.md +590 -0
- data/doc/decisions/001-flog-deferred.md +32 -0
- data/doc/decisions/002-path-resolution-strategy.md +158 -0
- data/doc/decisions/003-ui-adapter-selection.md +193 -0
- data/doc/decisions/004-design-document-validation.md +179 -0
- data/doc/decisions/005-package-splitting-strategy.md +200 -0
- data/doc/decisions/006-multi-engine-architecture.md +147 -0
- data/doc/decisions/007-engine-agnostic-pivot.md +219 -0
- data/doc/decisions/008-ci-bundler-strategy.md +129 -0
- data/doc/decisions/009-core-only-v1-release.md +60 -0
- data/doc/decisions/010-engine-debian-packaging.md +66 -0
- data/doc/decisions/011-context-aware-cli.md +71 -0
- data/doc/dependency_decisions.yml +247 -0
- data/doc/issues/001-wrapper-missing-environment-variables.md +197 -0
- data/doc/issues/002-embedded-ruby-wrong-prefix.md +217 -0
- data/doc/issues/003-smoke-test-false-positive.md +127 -0
- data/doc/issues/004-market-research-design-updates.md +109 -0
- data/doc/issues/005-compile-scope-coexistence.md +161 -0
- data/doc/locales/.gitkeep +0 -0
- data/doc/man/rai.1.ronn +505 -0
- data/doc/operations/packaging.md +133 -0
- data/doc/operations/rosett-ai-release.md +65 -0
- data/doc/reference/error-catalog.md +107 -0
- data/doc/reference/rosett-ai-technical-reference.pdf +0 -0
- data/doc/reference/src/Pictures/cover.jpg +0 -0
- data/doc/reference/src/Pictures/head1.jpg +0 -0
- data/doc/reference/src/Pictures/head2.jpg +0 -0
- data/doc/reference/src/Pictures/head3.jpg +0 -0
- data/doc/reference/src/Pictures/head4.jpg +0 -0
- data/doc/reference/src/Pictures/head5.jpg +0 -0
- data/doc/reference/src/Pictures/head6.jpg +0 -0
- data/doc/reference/src/Pictures/head7.jpg +0 -0
- data/doc/reference/src/Pictures/head8.jpg +0 -0
- data/doc/reference/src/StyleInd.ist +4 -0
- data/doc/reference/src/bibliography.bib +79 -0
- data/doc/reference/src/main.tex +1288 -0
- data/doc/reference/src/structure.tex +303 -0
- data/doc/rosett-ai-bookmarks.html +301 -0
- data/kitchen.yml +46 -0
- data/lib/rosett_ai/adopter/executor_resolver.rb +77 -0
- data/lib/rosett_ai/adopter/local_analysis_collector.rb +154 -0
- data/lib/rosett_ai/adopter/rule_adopter.rb +254 -0
- data/lib/rosett_ai/ai_config/config_compiler.rb +111 -0
- data/lib/rosett_ai/ai_config/context_window.rb +55 -0
- data/lib/rosett_ai/ai_config/cost_controls.rb +44 -0
- data/lib/rosett_ai/ai_config/fallback_chain.rb +64 -0
- data/lib/rosett_ai/ai_config/model_router.rb +121 -0
- data/lib/rosett_ai/ai_config/validator.rb +45 -0
- data/lib/rosett_ai/authorship/attribution_compiler.rb +99 -0
- data/lib/rosett_ai/authorship/disclosure_policy.rb +81 -0
- data/lib/rosett_ai/authorship/review_validator.rb +39 -0
- data/lib/rosett_ai/authorship/trailer_generator.rb +88 -0
- data/lib/rosett_ai/backup/compressor.rb +180 -0
- data/lib/rosett_ai/backup/destination.rb +91 -0
- data/lib/rosett_ai/behaviour/manager.rb +156 -0
- data/lib/rosett_ai/compiler/backend.rb +86 -0
- data/lib/rosett_ai/compiler/backends/agents_md_backend.rb +80 -0
- data/lib/rosett_ai/compiler/backends/claude_backend.rb +88 -0
- data/lib/rosett_ai/compiler/backends/generic_backend.rb +15 -0
- data/lib/rosett_ai/compiler/behaviour_compiler.rb +40 -0
- data/lib/rosett_ai/compiler/capability_checker.rb +104 -0
- data/lib/rosett_ai/compiler/compilation_pipeline.rb +361 -0
- data/lib/rosett_ai/compiler/compiled_output.rb +39 -0
- data/lib/rosett_ai/compiler/locale_compiler.rb +250 -0
- data/lib/rosett_ai/compiler/target_profile.rb +112 -0
- data/lib/rosett_ai/completion/generator.rb +101 -0
- data/lib/rosett_ai/completion/shells/bash_generator.rb +126 -0
- data/lib/rosett_ai/completion/shells/fish_generator.rb +78 -0
- data/lib/rosett_ai/completion/shells/zsh_generator.rb +126 -0
- data/lib/rosett_ai/comply/checkers/cra_checker.rb +102 -0
- data/lib/rosett_ai/comply/checkers/license_checker.rb +85 -0
- data/lib/rosett_ai/comply/checkers/spdx_header_checker.rb +98 -0
- data/lib/rosett_ai/comply/reporter.rb +113 -0
- data/lib/rosett_ai/comply/runner.rb +50 -0
- data/lib/rosett_ai/composition/circular_dependency_detector.rb +56 -0
- data/lib/rosett_ai/composition/composer.rb +158 -0
- data/lib/rosett_ai/composition/composition_result.rb +64 -0
- data/lib/rosett_ai/composition/conflict_detector.rb +53 -0
- data/lib/rosett_ai/composition/lockfile.rb +103 -0
- data/lib/rosett_ai/composition/merge_strategy.rb +131 -0
- data/lib/rosett_ai/composition/priority_sorter.rb +29 -0
- data/lib/rosett_ai/composition/scope_resolver.rb +55 -0
- data/lib/rosett_ai/config/compile_result.rb +37 -0
- data/lib/rosett_ai/config/compiler.rb +13 -0
- data/lib/rosett_ai/config/domain_transformer.rb +13 -0
- data/lib/rosett_ai/config/key_map.rb +13 -0
- data/lib/rosett_ai/config/masking_secret_resolver.rb +40 -0
- data/lib/rosett_ai/config/scope_router.rb +13 -0
- data/lib/rosett_ai/config/secret_resolver.rb +125 -0
- data/lib/rosett_ai/configuration.rb +119 -0
- data/lib/rosett_ai/content/content_client.rb +60 -0
- data/lib/rosett_ai/content/pack_installer.rb +117 -0
- data/lib/rosett_ai/content/pack_manifest.rb +50 -0
- data/lib/rosett_ai/content/pack_registry.rb +68 -0
- data/lib/rosett_ai/content_packs/manager.rb +50 -0
- data/lib/rosett_ai/dbus/compositor_detector.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/base.rb +59 -0
- data/lib/rosett_ai/dbus/focus_adapters/gnome_adapter.rb +172 -0
- data/lib/rosett_ai/dbus/focus_adapters/hyprland_adapter.rb +77 -0
- data/lib/rosett_ai/dbus/focus_adapters/i3_adapter.rb +65 -0
- data/lib/rosett_ai/dbus/focus_adapters/kwin_adapter.rb +103 -0
- data/lib/rosett_ai/dbus/focus_adapters/x11_adapter.rb +105 -0
- data/lib/rosett_ai/dbus/focus_monitor_interface.rb +103 -0
- data/lib/rosett_ai/dbus/manager_interface.rb +213 -0
- data/lib/rosett_ai/dbus/plugin_manager_interface.rb +169 -0
- data/lib/rosett_ai/dbus/rate_limiter.rb +89 -0
- data/lib/rosett_ai/dbus/service.rb +121 -0
- data/lib/rosett_ai/dbus/status_notifier_interface.rb +79 -0
- data/lib/rosett_ai/deprecation.rb +79 -0
- data/lib/rosett_ai/desktop/dbus_client.rb +259 -0
- data/lib/rosett_ai/desktop/gtk4_app.rb +371 -0
- data/lib/rosett_ai/desktop/gtk4_preferences.rb +331 -0
- data/lib/rosett_ai/desktop/gui_logger.rb +236 -0
- data/lib/rosett_ai/doctor/check.rb +92 -0
- data/lib/rosett_ai/doctor/checks/cache_health_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/dbus_availability_check.rb +39 -0
- data/lib/rosett_ai/doctor/checks/engine_detection_check.rb +46 -0
- data/lib/rosett_ai/doctor/checks/file_permission_check.rb +44 -0
- data/lib/rosett_ai/doctor/checks/gem_dependency_check.rb +55 -0
- data/lib/rosett_ai/doctor/checks/ruby_version_check.rb +50 -0
- data/lib/rosett_ai/doctor/checks/stale_config_nncc_check.rb +57 -0
- data/lib/rosett_ai/doctor/checks/stale_home_nncc_check.rb +59 -0
- data/lib/rosett_ai/doctor.rb +81 -0
- data/lib/rosett_ai/documentation/reference_compiler.rb +122 -0
- data/lib/rosett_ai/documentation/translator.rb +62 -0
- data/lib/rosett_ai/engines/base_config_compiler.rb +203 -0
- data/lib/rosett_ai/engines/detector.rb +63 -0
- data/lib/rosett_ai/engines/registry.rb +50 -0
- data/lib/rosett_ai/error_handler.rb +139 -0
- data/lib/rosett_ai/exit_codes.rb +76 -0
- data/lib/rosett_ai/feature_flags.rb +102 -0
- data/lib/rosett_ai/formatting.rb +33 -0
- data/lib/rosett_ai/gem_consistency_checker.rb +199 -0
- data/lib/rosett_ai/git_hooks/chain_detector.rb +86 -0
- data/lib/rosett_ai/git_hooks/installer.rb +175 -0
- data/lib/rosett_ai/git_hooks/script_generator.rb +125 -0
- data/lib/rosett_ai/gitlab/validators/supplementary_gitlab_ci_yaml_validator.rb +79 -0
- data/lib/rosett_ai/i18n/locale_resolver.rb +46 -0
- data/lib/rosett_ai/i18n/utf8_checker.rb +32 -0
- data/lib/rosett_ai/init/config_file_writer.rb +24 -0
- data/lib/rosett_ai/init/directory_builder.rb +38 -0
- data/lib/rosett_ai/init/file_copier.rb +95 -0
- data/lib/rosett_ai/init/global_initializer.rb +28 -0
- data/lib/rosett_ai/init/local_initializer.rb +27 -0
- data/lib/rosett_ai/init/mcp_registrar.rb +109 -0
- data/lib/rosett_ai/init/project_initializer.rb +38 -0
- data/lib/rosett_ai/licensing/license_key.rb +139 -0
- data/lib/rosett_ai/licensing/license_store.rb +64 -0
- data/lib/rosett_ai/licensing/license_validator.rb +60 -0
- data/lib/rosett_ai/licensing/tier.rb +42 -0
- data/lib/rosett_ai/mcp/admin/auditor.rb +88 -0
- data/lib/rosett_ai/mcp/admin/health_checker.rb +81 -0
- data/lib/rosett_ai/mcp/admin/registry.rb +100 -0
- data/lib/rosett_ai/mcp/admin/schema_validator.rb +63 -0
- data/lib/rosett_ai/mcp/enforcement/.gitkeep +0 -0
- data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +197 -0
- data/lib/rosett_ai/mcp/enforcement/validator.rb +215 -0
- data/lib/rosett_ai/mcp/governance.rb +160 -0
- data/lib/rosett_ai/mcp/http_security_config.rb +158 -0
- data/lib/rosett_ai/mcp/instructions.rb +266 -0
- data/lib/rosett_ai/mcp/key_hasher.rb +66 -0
- data/lib/rosett_ai/mcp/keyfile.rb +221 -0
- data/lib/rosett_ai/mcp/middleware/authentication.rb +146 -0
- data/lib/rosett_ai/mcp/middleware/content_type.rb +56 -0
- data/lib/rosett_ai/mcp/middleware/cors.rb +83 -0
- data/lib/rosett_ai/mcp/middleware/origin_validation.rb +73 -0
- data/lib/rosett_ai/mcp/middleware/rate_limit.rb +106 -0
- data/lib/rosett_ai/mcp/middleware/request_size.rb +51 -0
- data/lib/rosett_ai/mcp/plugins.rb +143 -0
- data/lib/rosett_ai/mcp/prompts/compilation_prompt.rb +40 -0
- data/lib/rosett_ai/mcp/prompts/compliance_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/diagnostics_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/prompts/validation_prompt.rb +41 -0
- data/lib/rosett_ai/mcp/resources/behaviour_resource.rb +127 -0
- data/lib/rosett_ai/mcp/resources/config_resource.rb +72 -0
- data/lib/rosett_ai/mcp/resources/design_resource.rb +58 -0
- data/lib/rosett_ai/mcp/resources/hooks_resource.rb +74 -0
- data/lib/rosett_ai/mcp/resources/provenance_resource.rb +51 -0
- data/lib/rosett_ai/mcp/resources/rules_resource.rb +60 -0
- data/lib/rosett_ai/mcp/resources/schema_resource.rb +72 -0
- data/lib/rosett_ai/mcp/response_helper.rb +46 -0
- data/lib/rosett_ai/mcp/security_logger.rb +60 -0
- data/lib/rosett_ai/mcp/server.rb +212 -0
- data/lib/rosett_ai/mcp/settings/server_installer.rb +112 -0
- data/lib/rosett_ai/mcp/settings/trust_manager.rb +142 -0
- data/lib/rosett_ai/mcp/tools/adopt_tool.rb +70 -0
- data/lib/rosett_ai/mcp/tools/backup_tool.rb +64 -0
- data/lib/rosett_ai/mcp/tools/behaviour_display_tool.rb +72 -0
- data/lib/rosett_ai/mcp/tools/behaviour_list_tool.rb +56 -0
- data/lib/rosett_ai/mcp/tools/behaviour_manage_tool.rb +114 -0
- data/lib/rosett_ai/mcp/tools/behaviour_show_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/compile_status_tool.rb +122 -0
- data/lib/rosett_ai/mcp/tools/compile_tool.rb +191 -0
- data/lib/rosett_ai/mcp/tools/comply_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/config_compile_tool.rb +71 -0
- data/lib/rosett_ai/mcp/tools/config_status_tool.rb +79 -0
- data/lib/rosett_ai/mcp/tools/content_tool.rb +78 -0
- data/lib/rosett_ai/mcp/tools/context_query_tool.rb +156 -0
- data/lib/rosett_ai/mcp/tools/design_list_tool.rb +57 -0
- data/lib/rosett_ai/mcp/tools/design_show_tool.rb +69 -0
- data/lib/rosett_ai/mcp/tools/doctor_tool.rb +62 -0
- data/lib/rosett_ai/mcp/tools/documentation_status_tool.rb +45 -0
- data/lib/rosett_ai/mcp/tools/engines_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/hook_install_tool.rb +190 -0
- data/lib/rosett_ai/mcp/tools/hook_preview_tool.rb +173 -0
- data/lib/rosett_ai/mcp/tools/hooks_status_tool.rb +84 -0
- data/lib/rosett_ai/mcp/tools/init_tool.rb +87 -0
- data/lib/rosett_ai/mcp/tools/license_status_tool.rb +44 -0
- data/lib/rosett_ai/mcp/tools/project_tool.rb +117 -0
- data/lib/rosett_ai/mcp/tools/provenance_tool.rb +97 -0
- data/lib/rosett_ai/mcp/tools/provenance_write_tool.rb +40 -0
- data/lib/rosett_ai/mcp/tools/retrofit_tool.rb +81 -0
- data/lib/rosett_ai/mcp/tools/rule_search_tool.rb +163 -0
- data/lib/rosett_ai/mcp/tools/schema_get_tool.rb +94 -0
- data/lib/rosett_ai/mcp/tools/tooling_tool.rb +86 -0
- data/lib/rosett_ai/mcp/tools/validate_tool.rb +105 -0
- data/lib/rosett_ai/mcp/tools/workflow_execute_tool.rb +74 -0
- data/lib/rosett_ai/mcp/tools/workflow_tool.rb +78 -0
- data/lib/rosett_ai/migration/detector.rb +117 -0
- data/lib/rosett_ai/migration/nncc_config_migrator.rb +94 -0
- data/lib/rosett_ai/migration/nncc_project_migrator.rb +90 -0
- data/lib/rosett_ai/migration/xdg_migrator.rb +123 -0
- data/lib/rosett_ai/package_manager/apt.rb +108 -0
- data/lib/rosett_ai/package_manager/base.rb +68 -0
- data/lib/rosett_ai/package_manager/gem_backend.rb +90 -0
- data/lib/rosett_ai/packaging/variant_config.rb +92 -0
- data/lib/rosett_ai/path_resolver.rb +115 -0
- data/lib/rosett_ai/plugins/contract.rb +43 -0
- data/lib/rosett_ai/plugins/engine_contract.rb +60 -0
- data/lib/rosett_ai/plugins/gui_contract.rb +74 -0
- data/lib/rosett_ai/plugins/mcp_contract.rb +48 -0
- data/lib/rosett_ai/plugins/registry.rb +150 -0
- data/lib/rosett_ai/policy/auditor.rb +41 -0
- data/lib/rosett_ai/policy/deny_list.rb +71 -0
- data/lib/rosett_ai/policy/opt_out_scanner.rb +37 -0
- data/lib/rosett_ai/policy/policy_compiler.rb +84 -0
- data/lib/rosett_ai/policy/protected_files.rb +47 -0
- data/lib/rosett_ai/policy/tier_hierarchy.rb +48 -0
- data/lib/rosett_ai/policy/validator.rb +35 -0
- data/lib/rosett_ai/profiler.rb +79 -0
- data/lib/rosett_ai/project/drift_detector.rb +126 -0
- data/lib/rosett_ai/project/manager.rb +115 -0
- data/lib/rosett_ai/project/sync_manager.rb +138 -0
- data/lib/rosett_ai/project/template_applier.rb +105 -0
- data/lib/rosett_ai/project_context.rb +82 -0
- data/lib/rosett_ai/provenance/entry.rb +63 -0
- data/lib/rosett_ai/provenance/file_source.rb +32 -0
- data/lib/rosett_ai/provenance/source.rb +62 -0
- data/lib/rosett_ai/provenance/store.rb +153 -0
- data/lib/rosett_ai/provenance/tracker.rb +62 -0
- data/lib/rosett_ai/provenance/trailer_generator.rb +43 -0
- data/lib/rosett_ai/provenance/validator.rb +45 -0
- data/lib/rosett_ai/quorum/collector.rb +59 -0
- data/lib/rosett_ai/quorum/comparator.rb +81 -0
- data/lib/rosett_ai/quorum/dispatcher.rb +57 -0
- data/lib/rosett_ai/quorum/strategies/adopt.rb +56 -0
- data/lib/rosett_ai/rai_config.rb +107 -0
- data/lib/rosett_ai/retrofit/base_parser.rb +66 -0
- data/lib/rosett_ai/retrofit/engine.rb +171 -0
- data/lib/rosett_ai/retrofit/parsers/agents_md_parser.rb +50 -0
- data/lib/rosett_ai/retrofit/parsers/claude_parser.rb +69 -0
- data/lib/rosett_ai/retrofit/parsers/cursor_parser.rb +82 -0
- data/lib/rosett_ai/retrofit/round_trip_validator.rb +65 -0
- data/lib/rosett_ai/retrofit/scanner.rb +47 -0
- data/lib/rosett_ai/retrofit/secret_detector.rb +87 -0
- data/lib/rosett_ai/secrets_resolver.rb +71 -0
- data/lib/rosett_ai/smart_feedback/suggester.rb +83 -0
- data/lib/rosett_ai/smart_feedback/thor_middleware.rb +84 -0
- data/lib/rosett_ai/structured_logger.rb +110 -0
- data/lib/rosett_ai/telemetry/json_lines_writer.rb +50 -0
- data/lib/rosett_ai/telemetry/log_rotator.rb +67 -0
- data/lib/rosett_ai/telemetry/provider.rb +26 -0
- data/lib/rosett_ai/telemetry/reporter.rb +144 -0
- data/lib/rosett_ai/telemetry.rb +47 -0
- data/lib/rosett_ai/text_sanitizer.rb +62 -0
- data/lib/rosett_ai/thor/cli.rb +269 -0
- data/lib/rosett_ai/thor/tasks/adopt.rb +250 -0
- data/lib/rosett_ai/thor/tasks/backup.rb +420 -0
- data/lib/rosett_ai/thor/tasks/behaviour.rb +474 -0
- data/lib/rosett_ai/thor/tasks/build.rb +1162 -0
- data/lib/rosett_ai/thor/tasks/compile.rb +415 -0
- data/lib/rosett_ai/thor/tasks/completion.rb +123 -0
- data/lib/rosett_ai/thor/tasks/comply.rb +82 -0
- data/lib/rosett_ai/thor/tasks/config.rb +265 -0
- data/lib/rosett_ai/thor/tasks/content.rb +193 -0
- data/lib/rosett_ai/thor/tasks/dbus.rb +321 -0
- data/lib/rosett_ai/thor/tasks/design.rb +258 -0
- data/lib/rosett_ai/thor/tasks/desktop.rb +129 -0
- data/lib/rosett_ai/thor/tasks/doctor.rb +127 -0
- data/lib/rosett_ai/thor/tasks/documentation.rb +321 -0
- data/lib/rosett_ai/thor/tasks/engines.rb +167 -0
- data/lib/rosett_ai/thor/tasks/hooks.rb +219 -0
- data/lib/rosett_ai/thor/tasks/init.rb +259 -0
- data/lib/rosett_ai/thor/tasks/license.rb +120 -0
- data/lib/rosett_ai/thor/tasks/mcp.rb +535 -0
- data/lib/rosett_ai/thor/tasks/migrate.rb +121 -0
- data/lib/rosett_ai/thor/tasks/plugins.rb +157 -0
- data/lib/rosett_ai/thor/tasks/project.rb +260 -0
- data/lib/rosett_ai/thor/tasks/provenance.rb +195 -0
- data/lib/rosett_ai/thor/tasks/release.rb +314 -0
- data/lib/rosett_ai/thor/tasks/retrofit.rb +90 -0
- data/lib/rosett_ai/thor/tasks/tooling.rb +308 -0
- data/lib/rosett_ai/thor/tasks/validate.rb +108 -0
- data/lib/rosett_ai/thor/tasks/workflow.rb +196 -0
- data/lib/rosett_ai/tooling/ci_yaml_validator.rb +37 -0
- data/lib/rosett_ai/tooling/version_checker.rb +35 -0
- data/lib/rosett_ai/ui/accessible_tui.rb +61 -0
- data/lib/rosett_ai/ui/base.rb +46 -0
- data/lib/rosett_ai/ui/gtk4.rb +98 -0
- data/lib/rosett_ai/ui/kde.rb +40 -0
- data/lib/rosett_ai/ui/qt6.rb +40 -0
- data/lib/rosett_ai/ui/registry.rb +60 -0
- data/lib/rosett_ai/ui/tty_helper.rb +74 -0
- data/lib/rosett_ai/ui/tui.rb +59 -0
- data/lib/rosett_ai/validators/behaviour_validator.rb +20 -0
- data/lib/rosett_ai/validators/design_validator.rb +17 -0
- data/lib/rosett_ai/validators/schema_validator.rb +84 -0
- data/lib/rosett_ai/validators/tooling_validator.rb +17 -0
- data/lib/rosett_ai/version.rb +8 -0
- data/lib/rosett_ai/version_consistency_checker.rb +129 -0
- data/lib/rosett_ai/workflow/audit_log.rb +86 -0
- data/lib/rosett_ai/workflow/engine.rb +142 -0
- data/lib/rosett_ai/workflow/manager.rb +82 -0
- data/lib/rosett_ai/workflow/schema_validator.rb +71 -0
- data/lib/rosett_ai/workflow/step_runner.rb +61 -0
- data/lib/rosett_ai/workflow/steps/prompt_step.rb +62 -0
- data/lib/rosett_ai/workflow/steps/rai_step.rb +74 -0
- data/lib/rosett_ai/workflow/steps/shell_step.rb +53 -0
- data/lib/rosett_ai/yaml_loader.rb +78 -0
- data/lib/rosett_ai.rb +221 -0
- data/lib/rubocop/cop/rosett_ai/shell_interpolation.rb +54 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_const_get.rb +60 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_send.rb +50 -0
- data/lib/rubocop/cop/rosett_ai/unsafe_yaml_load.rb +40 -0
- data/lib/rubocop/rosett_ai.rb +9 -0
- data/lib/scripts/generated/docker_hub_tags.rb +126 -0
- data/locales/.gitkeep +0 -0
- data/locales/ar.yml +579 -0
- data/locales/en.yml +571 -0
- data/locales/fr.yml +567 -0
- data/packaging/build-engine-deb.sh +81 -0
- data/packaging/scripts/postinst +17 -0
- data/packaging/scripts/postrm +19 -0
- data/packaging/scripts/prerm +10 -0
- data/packaging/wrapper.sh.template +38 -0
- data/rosett-ai.gemspec +63 -0
- data/rules/.gitkeep +0 -0
- data/scripts/publish/pulp_upload.sh +123 -0
- data/settings.json +29 -0
- data/share/applications/be.neatnerds.rosettai.desktop +29 -0
- data/share/dbus-1/interfaces/be.neatnerds.rosettai.xml +103 -0
- data/share/dbus-1/services/be.neatnerds.rosettai.service +3 -0
- data/share/templates/behaviour/criticalthinking.yml +69 -0
- metadata +810 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
# Centralized error handler for CLI commands.
|
|
8
|
+
#
|
|
9
|
+
# Catches +RosettAi::Error+ (and subclasses), formats the error message with
|
|
10
|
+
# what/why/fix structure, and exits with the mapped exit code. Stack traces
|
|
11
|
+
# are only shown when +RAI_LOG_LEVEL=DEBUG+.
|
|
12
|
+
#
|
|
13
|
+
# TTY-aware: uses colour and formatting when stdout is a TTY, plain text
|
|
14
|
+
# when piped.
|
|
15
|
+
#
|
|
16
|
+
# @author hugo
|
|
17
|
+
# @author claude
|
|
18
|
+
# @see ExitCodes
|
|
19
|
+
# @see conf/design/error_handling.yml
|
|
20
|
+
class ErrorHandler
|
|
21
|
+
# Wraps a block, rescuing +RosettAi::Error+ and formatting the output.
|
|
22
|
+
#
|
|
23
|
+
# @param stderr [IO] output stream for errors (default: $stderr)
|
|
24
|
+
# @yield the CLI command block to execute
|
|
25
|
+
# @return [Integer] exit code (0 on success)
|
|
26
|
+
def self.handle(stderr: $stderr, &)
|
|
27
|
+
new(stderr: stderr).handle(&)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @param stderr [IO] output stream for errors
|
|
31
|
+
def initialize(stderr: $stderr)
|
|
32
|
+
@stderr = stderr
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Executes the block and handles errors.
|
|
36
|
+
#
|
|
37
|
+
# @yield the CLI command block
|
|
38
|
+
# @return [Integer] exit code
|
|
39
|
+
def handle
|
|
40
|
+
yield
|
|
41
|
+
ExitCodes::SUCCESS
|
|
42
|
+
rescue RosettAi::Error => e
|
|
43
|
+
report(e)
|
|
44
|
+
ExitCodes.for(e)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Formats and writes the error to stderr.
|
|
50
|
+
#
|
|
51
|
+
# @param error [RosettAi::Error] the caught error
|
|
52
|
+
# @return [void]
|
|
53
|
+
def report(error)
|
|
54
|
+
exit_code = ExitCodes.for(error)
|
|
55
|
+
parts = parse_message(error.message)
|
|
56
|
+
|
|
57
|
+
if tty?
|
|
58
|
+
report_tty(error, parts, exit_code)
|
|
59
|
+
else
|
|
60
|
+
report_plain(error, parts, exit_code)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
report_backtrace(error) if debug?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Parses a message for what/why/fix structure.
|
|
67
|
+
#
|
|
68
|
+
# Messages can use the convention:
|
|
69
|
+
# "What failed. Why: reason. Fix: suggestion"
|
|
70
|
+
# or just be a plain string.
|
|
71
|
+
#
|
|
72
|
+
# @param message [String]
|
|
73
|
+
# @return [Hash{Symbol => String, nil}]
|
|
74
|
+
def parse_message(message)
|
|
75
|
+
parts = { what: message, why: nil, fix: nil }
|
|
76
|
+
|
|
77
|
+
if message.include?(' Fix: ')
|
|
78
|
+
before_fix, parts[:fix] = message.split(' Fix: ', 2)
|
|
79
|
+
if before_fix.include?(' Why: ')
|
|
80
|
+
parts[:what], parts[:why] = before_fix.split(' Why: ', 2)
|
|
81
|
+
else
|
|
82
|
+
parts[:what] = before_fix
|
|
83
|
+
end
|
|
84
|
+
elsif message.include?(' Why: ')
|
|
85
|
+
parts[:what], parts[:why] = message.split(' Why: ', 2)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
parts
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# TTY-formatted error output with colour.
|
|
92
|
+
#
|
|
93
|
+
# @param error [RosettAi::Error]
|
|
94
|
+
# @param parts [Hash]
|
|
95
|
+
# @param exit_code [Integer]
|
|
96
|
+
# @return [void]
|
|
97
|
+
def report_tty(error, parts, exit_code)
|
|
98
|
+
@stderr.puts Rainbow("Error [#{exit_code}]: #{parts[:what]}").red.bright
|
|
99
|
+
@stderr.puts Rainbow(" Why: #{parts[:why]}").yellow if parts[:why]
|
|
100
|
+
@stderr.puts Rainbow(" Fix: #{parts[:fix]}").green if parts[:fix]
|
|
101
|
+
@stderr.puts Rainbow(" Type: #{error.class}").faint
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Plain text error output for pipes.
|
|
105
|
+
#
|
|
106
|
+
# @param error [RosettAi::Error]
|
|
107
|
+
# @param parts [Hash]
|
|
108
|
+
# @param exit_code [Integer]
|
|
109
|
+
# @return [void]
|
|
110
|
+
def report_plain(error, parts, exit_code)
|
|
111
|
+
@stderr.puts "Error [#{exit_code}]: #{parts[:what]}"
|
|
112
|
+
@stderr.puts " Why: #{parts[:why]}" if parts[:why]
|
|
113
|
+
@stderr.puts " Fix: #{parts[:fix]}" if parts[:fix]
|
|
114
|
+
@stderr.puts " Type: #{error.class}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Outputs backtrace when RAI_LOG_LEVEL=DEBUG.
|
|
118
|
+
#
|
|
119
|
+
# @param error [RosettAi::Error]
|
|
120
|
+
# @return [void]
|
|
121
|
+
def report_backtrace(error)
|
|
122
|
+
return unless error.backtrace
|
|
123
|
+
|
|
124
|
+
@stderr.puts ''
|
|
125
|
+
@stderr.puts 'Backtrace (first 10 frames):'
|
|
126
|
+
error.backtrace.first(10).each { |frame| @stderr.puts " #{frame}" }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# @return [Boolean] true if stderr is a TTY
|
|
130
|
+
def tty?
|
|
131
|
+
@stderr.respond_to?(:tty?) && @stderr.tty?
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [Boolean] true if RAI_LOG_LEVEL is DEBUG
|
|
135
|
+
def debug?
|
|
136
|
+
ENV.fetch('RAI_LOG_LEVEL', '').casecmp('debug').zero?
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
# Maps error categories to numeric exit codes for CLI process exit.
|
|
8
|
+
#
|
|
9
|
+
# Exit codes follow Unix conventions:
|
|
10
|
+
# - 0: success
|
|
11
|
+
# - 1: general/runtime error
|
|
12
|
+
# - 2: validation/input error
|
|
13
|
+
# - 3: configuration error
|
|
14
|
+
# - 4: permission/access error
|
|
15
|
+
# - 5: dependency/environment error
|
|
16
|
+
#
|
|
17
|
+
# @author hugo
|
|
18
|
+
# @author claude
|
|
19
|
+
module ExitCodes
|
|
20
|
+
SUCCESS = 0
|
|
21
|
+
GENERAL = 1
|
|
22
|
+
VALIDATION = 2
|
|
23
|
+
CONFIG = 3
|
|
24
|
+
PERMISSION = 4
|
|
25
|
+
DEPENDENCY = 5
|
|
26
|
+
|
|
27
|
+
# Maps each RosettAi error class to its exit code.
|
|
28
|
+
#
|
|
29
|
+
# @return [Hash{Class => Integer}]
|
|
30
|
+
MAPPING = {
|
|
31
|
+
RosettAi::Error => GENERAL,
|
|
32
|
+
RosettAi::ValidationError => VALIDATION,
|
|
33
|
+
RosettAi::BehaviourError => VALIDATION,
|
|
34
|
+
RosettAi::DesignError => VALIDATION,
|
|
35
|
+
RosettAi::CompositionError => VALIDATION,
|
|
36
|
+
RosettAi::CompileError => VALIDATION,
|
|
37
|
+
RosettAi::ConfigurationError => CONFIG,
|
|
38
|
+
RosettAi::AiConfigError => CONFIG,
|
|
39
|
+
RosettAi::PolicyError => CONFIG,
|
|
40
|
+
RosettAi::InitError => CONFIG,
|
|
41
|
+
RosettAi::AdoptError => GENERAL,
|
|
42
|
+
RosettAi::BackupError => GENERAL,
|
|
43
|
+
RosettAi::BuildError => GENERAL,
|
|
44
|
+
RosettAi::DocumentationError => GENERAL,
|
|
45
|
+
RosettAi::ReleaseError => GENERAL,
|
|
46
|
+
RosettAi::ToolingError => VALIDATION,
|
|
47
|
+
RosettAi::LicenseError => CONFIG,
|
|
48
|
+
RosettAi::ContentError => GENERAL,
|
|
49
|
+
RosettAi::ProvenanceError => GENERAL,
|
|
50
|
+
RosettAi::AuthorshipError => GENERAL,
|
|
51
|
+
RosettAi::DoctorError => GENERAL,
|
|
52
|
+
RosettAi::FeatureFlagError => GENERAL,
|
|
53
|
+
RosettAi::ComplianceError => VALIDATION,
|
|
54
|
+
RosettAi::WorkflowError => GENERAL,
|
|
55
|
+
RosettAi::ProjectError => GENERAL,
|
|
56
|
+
RosettAi::RetrofitError => CONFIG,
|
|
57
|
+
RosettAi::GitHooksError => GENERAL,
|
|
58
|
+
RosettAi::McpError => GENERAL
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Resolves the exit code for a given error.
|
|
62
|
+
#
|
|
63
|
+
# Walks the class hierarchy to find the most specific mapping.
|
|
64
|
+
# Falls back to GENERAL (1) for unmapped error classes.
|
|
65
|
+
#
|
|
66
|
+
# @param error [Exception] the error to map
|
|
67
|
+
# @return [Integer] the exit code (0-5)
|
|
68
|
+
def self.for(error)
|
|
69
|
+
klass = error.is_a?(Class) ? error : error.class
|
|
70
|
+
MAPPING.fetch(klass) do
|
|
71
|
+
ancestor = klass.ancestors.find { |ancestor_class| MAPPING.key?(ancestor_class) }
|
|
72
|
+
ancestor ? MAPPING[ancestor] : GENERAL
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
# Environment-variable-based feature gating for experimental features.
|
|
8
|
+
#
|
|
9
|
+
# Setting +RAI_EXPERIMENTAL=workflow,mcp+ enables only the listed features;
|
|
10
|
+
# all others remain hidden. Flags are parsed once at module load time into
|
|
11
|
+
# a frozen Set for O(1) membership checks.
|
|
12
|
+
#
|
|
13
|
+
# @example Check if a feature is enabled
|
|
14
|
+
# RosettAi::FeatureFlags.enabled?(:workflow) # => true if RAI_EXPERIMENTAL includes "workflow"
|
|
15
|
+
#
|
|
16
|
+
# @example Gate a command (raises if not enabled)
|
|
17
|
+
# RosettAi::FeatureFlags.gate!(:workflow)
|
|
18
|
+
#
|
|
19
|
+
# @author hugo
|
|
20
|
+
# @author claude
|
|
21
|
+
module FeatureFlags
|
|
22
|
+
# All recognised experimental feature names. When a feature is promoted
|
|
23
|
+
# to stable, remove it from this set.
|
|
24
|
+
KNOWN_FLAGS = Set['workflow', 'mcp', 'comply', 'retrofit'].freeze
|
|
25
|
+
|
|
26
|
+
# Raised when a gated command is invoked without its feature flag enabled.
|
|
27
|
+
class ExperimentalFeatureError < RosettAi::Error
|
|
28
|
+
# @return [String] the feature name that was not enabled
|
|
29
|
+
attr_reader :feature
|
|
30
|
+
|
|
31
|
+
def initialize(feature)
|
|
32
|
+
@feature = feature.to_s
|
|
33
|
+
super(::I18n.t('rosett_ai.feature_flags.not_enabled',
|
|
34
|
+
feature: @feature,
|
|
35
|
+
env_example: "RAI_EXPERIMENTAL=#{@feature}"))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
# Returns true if the given feature is currently enabled.
|
|
41
|
+
#
|
|
42
|
+
# @param feature [Symbol, String] the feature flag name
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def enabled?(feature)
|
|
45
|
+
active_flags.include?(feature.to_s)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Raises {ExperimentalFeatureError} unless the given feature is enabled.
|
|
49
|
+
# Used as a guard in CLI commands that are experimental.
|
|
50
|
+
#
|
|
51
|
+
# @param feature [Symbol, String] the feature flag name
|
|
52
|
+
# @raise [ExperimentalFeatureError] if the feature is not enabled
|
|
53
|
+
# @return [void]
|
|
54
|
+
def gate!(feature)
|
|
55
|
+
return if enabled?(feature)
|
|
56
|
+
|
|
57
|
+
raise ExperimentalFeatureError, feature
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Returns the Set of currently active (enabled) flags.
|
|
61
|
+
#
|
|
62
|
+
# @return [Set<String>]
|
|
63
|
+
def all_enabled
|
|
64
|
+
active_flags
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Re-parses RAI_EXPERIMENTAL from the environment. Intended for
|
|
68
|
+
# test isolation only.
|
|
69
|
+
#
|
|
70
|
+
# @return [void]
|
|
71
|
+
def reset!
|
|
72
|
+
@active_flags = nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def active_flags
|
|
78
|
+
@active_flags ||= parse_flags
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_flags
|
|
82
|
+
raw = ENV.fetch('RAI_EXPERIMENTAL', '')
|
|
83
|
+
return Set.new.freeze if raw.empty?
|
|
84
|
+
|
|
85
|
+
requested = raw.split(',').map(&:strip).reject(&:empty?).to_set
|
|
86
|
+
unknown = requested - KNOWN_FLAGS
|
|
87
|
+
|
|
88
|
+
warn_unknown(unknown) unless unknown.empty?
|
|
89
|
+
|
|
90
|
+
(requested & KNOWN_FLAGS).freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def warn_unknown(unknown)
|
|
94
|
+
unknown.sort.each do |flag|
|
|
95
|
+
warn ::I18n.t('rosett_ai.feature_flags.unknown_flag',
|
|
96
|
+
flag: flag,
|
|
97
|
+
known: KNOWN_FLAGS.sort.join(', '))
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
# Human-readable formatting for byte sizes and durations.
|
|
8
|
+
# Include in any class that needs to display file sizes or elapsed times.
|
|
9
|
+
module Formatting
|
|
10
|
+
SIZE_UNITS = ['B', 'KB', 'MB', 'GB'].freeze
|
|
11
|
+
|
|
12
|
+
# Formats a byte count as a human-readable string (e.g. "2.0 KB").
|
|
13
|
+
def format_size(bytes)
|
|
14
|
+
unit_index = 0
|
|
15
|
+
size = bytes.to_f
|
|
16
|
+
while size >= 1024 && unit_index < SIZE_UNITS.length - 1
|
|
17
|
+
size /= 1024
|
|
18
|
+
unit_index += 1
|
|
19
|
+
end
|
|
20
|
+
format('%<size>.1f %<unit>s', size: size, unit: SIZE_UNITS[unit_index])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Formats a duration in seconds as a human-readable string.
|
|
24
|
+
# Returns "Xs" for < 60s, "Xm Ys" for >= 60s.
|
|
25
|
+
def format_duration(seconds)
|
|
26
|
+
if seconds >= 60
|
|
27
|
+
format('%<min>dm %<sec>.0fs', min: (seconds / 60).to_i, sec: seconds % 60)
|
|
28
|
+
else
|
|
29
|
+
format('%<sec>.1fs', sec: seconds)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
require 'pathname'
|
|
7
|
+
|
|
8
|
+
module RosettAi
|
|
9
|
+
# Checks that gem version references across the codebase are consistent
|
|
10
|
+
# with the resolved versions in Gemfile.lock and the constraints in the gemspec.
|
|
11
|
+
#
|
|
12
|
+
# Parses Gemfile.lock for resolved runtime dependency versions, compares them
|
|
13
|
+
# against gemspec constraints, and scans project files for stale gem version
|
|
14
|
+
# references.
|
|
15
|
+
class GemConsistencyChecker
|
|
16
|
+
EXCLUDED_DIRS = ['vendor', 'tmp', 'coverage', '.git', '.bundle'].freeze
|
|
17
|
+
EXCLUDED_FILES = ['Gemfile.lock', 'Gemfile', 'rosett_ai.gemspec',
|
|
18
|
+
'spec/rosett_ai/gem_consistency_checker_spec.rb',
|
|
19
|
+
'spec/examples.txt'].freeze
|
|
20
|
+
EXCLUDED_PREFIXES = ['doc/changes/', 'doc/claude-sessions/', 'conf/design/'].freeze
|
|
21
|
+
EXCLUDED_EXTENSIONS = ['.aux', '.toc', '.out', '.ptc', '.log', '.idx', '.ind',
|
|
22
|
+
'.ilg', '.bbl', '.blg', '.bcf', '.fls', '.json'].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :results
|
|
25
|
+
|
|
26
|
+
def initialize(project_dir: Dir.pwd)
|
|
27
|
+
@project_dir = Pathname.new(project_dir)
|
|
28
|
+
@results = { gems: [], stale_references: [], consistent: true }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def check
|
|
32
|
+
constraints = parse_gemspec_constraints
|
|
33
|
+
locked = parse_lockfile_versions
|
|
34
|
+
|
|
35
|
+
gems = build_gem_audit(constraints, locked)
|
|
36
|
+
dependency_versions = locked.select { |name, _| constraints.key?(name) }
|
|
37
|
+
stale = scan_for_stale_references(dependency_versions)
|
|
38
|
+
|
|
39
|
+
@results = {
|
|
40
|
+
gems: gems,
|
|
41
|
+
stale_references: stale,
|
|
42
|
+
consistent: gems.all? { |entry| entry[:status] == :ok } && stale.empty?
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def consistent?
|
|
47
|
+
check if @results[:gems].empty? && @results[:stale_references].empty?
|
|
48
|
+
@results[:consistent]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def parse_gemspec_constraints
|
|
54
|
+
gemspec_path = find_gemspec
|
|
55
|
+
return {} unless gemspec_path
|
|
56
|
+
|
|
57
|
+
constraints = {}
|
|
58
|
+
File.readlines(gemspec_path, encoding: 'UTF-8').each do |line|
|
|
59
|
+
match = line.match(/add_dependency\s+['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/)
|
|
60
|
+
next unless match
|
|
61
|
+
|
|
62
|
+
constraints[match[1]] = match[2]
|
|
63
|
+
end
|
|
64
|
+
constraints
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def find_gemspec
|
|
68
|
+
pattern = @project_dir.join('*.gemspec')
|
|
69
|
+
files = Dir.glob(pattern)
|
|
70
|
+
files.first
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_lockfile_versions
|
|
74
|
+
lockfile = @project_dir.join('Gemfile.lock')
|
|
75
|
+
return {} unless lockfile.exist?
|
|
76
|
+
|
|
77
|
+
gem_specs_section = extract_gem_specs(File.readlines(lockfile, encoding: 'UTF-8'))
|
|
78
|
+
parse_specs_lines(gem_specs_section)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_gem_specs(lines)
|
|
82
|
+
in_gem = false
|
|
83
|
+
in_specs = false
|
|
84
|
+
specs = []
|
|
85
|
+
|
|
86
|
+
lines.each do |line|
|
|
87
|
+
in_gem = true if line.strip == 'GEM'
|
|
88
|
+
in_specs = true if in_gem && line.strip == 'specs:'
|
|
89
|
+
break if in_gem && in_specs && !line.start_with?(' ')
|
|
90
|
+
|
|
91
|
+
specs << line if in_specs
|
|
92
|
+
end
|
|
93
|
+
specs
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_specs_lines(lines)
|
|
97
|
+
versions = {}
|
|
98
|
+
lines.each do |line|
|
|
99
|
+
match = line.match(/^\s{4}(\S+)\s+\((\d+\.\d+[\d.]*)\)/)
|
|
100
|
+
versions[match[1]] = match[2] if match
|
|
101
|
+
end
|
|
102
|
+
versions
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_gem_audit(constraints, locked)
|
|
106
|
+
constraints.map do |gem_name, constraint|
|
|
107
|
+
resolved = locked[gem_name]
|
|
108
|
+
status = gem_status(constraint, resolved)
|
|
109
|
+
|
|
110
|
+
{ name: gem_name, constraint: constraint, resolved: resolved, status: status }
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def gem_status(constraint, resolved)
|
|
115
|
+
return :missing unless resolved
|
|
116
|
+
|
|
117
|
+
requirement = Gem::Requirement.new(constraint)
|
|
118
|
+
version = Gem::Version.new(resolved)
|
|
119
|
+
requirement.satisfied_by?(version) ? :ok : :conflict
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def scan_for_stale_references(locked)
|
|
123
|
+
return [] if locked.empty?
|
|
124
|
+
|
|
125
|
+
stale = []
|
|
126
|
+
project_files.each do |file|
|
|
127
|
+
scan_file_for_gems(file, locked, stale)
|
|
128
|
+
end
|
|
129
|
+
stale
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def scan_file_for_gems(file, locked, stale)
|
|
133
|
+
relative = Pathname.new(file).relative_path_from(@project_dir).to_s
|
|
134
|
+
|
|
135
|
+
File.readlines(file, encoding: 'UTF-8').each_with_index do |line, index|
|
|
136
|
+
next if constraint_line?(line)
|
|
137
|
+
next if suppressed_line?(line)
|
|
138
|
+
|
|
139
|
+
check_line_for_stale_gems(line, locked, relative, index, stale)
|
|
140
|
+
end
|
|
141
|
+
rescue ArgumentError, Encoding::InvalidByteSequenceError
|
|
142
|
+
# Skip files with encoding issues
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def check_line_for_stale_gems(line, locked, relative, index, stale)
|
|
146
|
+
locked.each do |gem_name, locked_version|
|
|
147
|
+
pattern = /\b#{Regexp.escape(gem_name)}\b\s+v?(\d+\.\d+[\d.]*)\b/
|
|
148
|
+
line.scan(pattern).each do |match|
|
|
149
|
+
found_version = match[0]
|
|
150
|
+
next if found_version == locked_version
|
|
151
|
+
|
|
152
|
+
stale << {
|
|
153
|
+
file: relative,
|
|
154
|
+
line: index + 1,
|
|
155
|
+
gem: gem_name,
|
|
156
|
+
found: found_version,
|
|
157
|
+
expected: locked_version
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def project_files
|
|
164
|
+
Dir.glob(@project_dir.join('**', '*'), File::FNM_DOTMATCH)
|
|
165
|
+
.select { |f| File.file?(f) }
|
|
166
|
+
.reject { |f| excluded?(f) }
|
|
167
|
+
.reject { |f| binary?(f) }
|
|
168
|
+
.sort
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def excluded?(file)
|
|
172
|
+
relative = Pathname.new(file).relative_path_from(@project_dir).to_s
|
|
173
|
+
|
|
174
|
+
return true if EXCLUDED_DIRS.any? { |dir| relative.start_with?("#{dir}/") }
|
|
175
|
+
return true if EXCLUDED_FILES.include?(relative)
|
|
176
|
+
return true if EXCLUDED_PREFIXES.any? { |prefix| relative.start_with?(prefix) }
|
|
177
|
+
return true if EXCLUDED_EXTENSIONS.any? { |ext| relative.end_with?(ext) }
|
|
178
|
+
|
|
179
|
+
false
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def binary?(file)
|
|
183
|
+
sample = File.read(file, 512)
|
|
184
|
+
return true if sample.nil?
|
|
185
|
+
|
|
186
|
+
sample.include?("\x00")
|
|
187
|
+
rescue ArgumentError
|
|
188
|
+
true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def constraint_line?(line)
|
|
192
|
+
line.match?(/[><=!~]{1,2}\s*\d+\.\d+/)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def suppressed_line?(line)
|
|
196
|
+
line.include?('rai:no-version-check')
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-only
|
|
4
|
+
# Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
|
|
5
|
+
|
|
6
|
+
module RosettAi
|
|
7
|
+
module GitHooks
|
|
8
|
+
# Detects existing git hook managers and hook scripts in a repository.
|
|
9
|
+
#
|
|
10
|
+
# Identifies overcommit, husky, lefthook, and plain shell hooks
|
|
11
|
+
# to inform the installer about chaining requirements.
|
|
12
|
+
#
|
|
13
|
+
# @author hugo
|
|
14
|
+
# @author claude
|
|
15
|
+
class ChainDetector
|
|
16
|
+
KNOWN_MANAGERS = {
|
|
17
|
+
'overcommit' => { marker: /overcommit/i, config: '.overcommit.yml' },
|
|
18
|
+
'husky' => { marker: /husky/i, config: '.husky' },
|
|
19
|
+
'lefthook' => { marker: /lefthook/i, config: 'lefthook.yml' }
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
# @param project_root [Pathname] path to the project root
|
|
23
|
+
def initialize(project_root:)
|
|
24
|
+
@project_root = Pathname.new(project_root)
|
|
25
|
+
@hooks_dir = @project_root.join('.git', 'hooks')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Detects which hook managers are present in the project.
|
|
29
|
+
#
|
|
30
|
+
# @return [Array<Hash>] list of detected managers with :name and :type
|
|
31
|
+
def detect
|
|
32
|
+
managers = []
|
|
33
|
+
|
|
34
|
+
KNOWN_MANAGERS.each do |name, info|
|
|
35
|
+
config_path = @project_root.join(info[:config])
|
|
36
|
+
managers << { name: name, type: :config } if config_path.exist?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
managers
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Checks if a specific hook has an existing non-rosett-ai script.
|
|
43
|
+
#
|
|
44
|
+
# @param hook_name [String] the git hook name
|
|
45
|
+
# @return [Hash, nil] info about the existing hook or nil
|
|
46
|
+
def existing_hook(hook_name)
|
|
47
|
+
path = @hooks_dir.join(hook_name)
|
|
48
|
+
return nil unless path.exist?
|
|
49
|
+
return nil if rai_managed?(path)
|
|
50
|
+
|
|
51
|
+
content = File.read(path)
|
|
52
|
+
manager = identify_manager(content)
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
path: path.to_s,
|
|
56
|
+
manager: manager,
|
|
57
|
+
symlink: path.symlink?,
|
|
58
|
+
executable: path.executable?
|
|
59
|
+
}
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Checks all supported hooks for existing non-rosett-ai scripts.
|
|
63
|
+
#
|
|
64
|
+
# @return [Hash{String => Hash}] map of hook names to existing hook info
|
|
65
|
+
def all_existing
|
|
66
|
+
Installer::SUPPORTED_HOOKS.each_with_object({}) do |hook, result|
|
|
67
|
+
info = existing_hook(hook)
|
|
68
|
+
result[hook] = info if info
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def rai_managed?(path)
|
|
75
|
+
File.read(path).include?(Installer::RAI_MARKER)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def identify_manager(content)
|
|
79
|
+
KNOWN_MANAGERS.each do |name, info|
|
|
80
|
+
return name if content.match?(info[:marker])
|
|
81
|
+
end
|
|
82
|
+
'unknown'
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|