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,158 @@
|
|
|
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 Composition
|
|
8
|
+
# Orchestrates behaviour composition: discovers behaviour files,
|
|
9
|
+
# resolves scopes, sorts by priority, detects conflicts and
|
|
10
|
+
# circular dependencies, and produces a deterministic result.
|
|
11
|
+
#
|
|
12
|
+
# Supports configurable merge strategies for rule ID collisions:
|
|
13
|
+
# - first_wins: highest-priority rule wins (default)
|
|
14
|
+
# - deep_merge: recursively merge hash fields from all scopes
|
|
15
|
+
# - array_union: combine array fields from all scopes
|
|
16
|
+
#
|
|
17
|
+
# @author hugo
|
|
18
|
+
# @author claude
|
|
19
|
+
class Composer
|
|
20
|
+
# @param scope_resolver [ScopeResolver] scope resolution strategy
|
|
21
|
+
# @param priority_sorter [PrioritySorter] priority sorting strategy
|
|
22
|
+
# @param conflict_detector [ConflictDetector] conflict detection
|
|
23
|
+
# @param cycle_detector [CircularDependencyDetector] cycle detection
|
|
24
|
+
def initialize(
|
|
25
|
+
scope_resolver: ScopeResolver.new,
|
|
26
|
+
priority_sorter: PrioritySorter.new,
|
|
27
|
+
conflict_detector: ConflictDetector.new,
|
|
28
|
+
cycle_detector: CircularDependencyDetector.new
|
|
29
|
+
)
|
|
30
|
+
@scope_resolver = scope_resolver
|
|
31
|
+
@priority_sorter = priority_sorter
|
|
32
|
+
@conflict_detector = conflict_detector
|
|
33
|
+
@cycle_detector = cycle_detector
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Composes all behaviour files into a single result.
|
|
37
|
+
#
|
|
38
|
+
# @param behaviour_files [Array<String>] paths to behaviour YAML files
|
|
39
|
+
# @param project_root [Pathname, nil] project root for scope resolution
|
|
40
|
+
# @param strict [Boolean] treat warnings as errors
|
|
41
|
+
# @param target [String, nil] compilation target (for sensitive filtering)
|
|
42
|
+
# @param merge_strategy [String] merge strategy name (first_wins, deep_merge, array_union)
|
|
43
|
+
# @return [CompositionResult]
|
|
44
|
+
# @raise [RosettAi::CompositionError] on circular dependencies or strict conflicts
|
|
45
|
+
def compose(behaviour_files, project_root: nil, strict: false, target: nil,
|
|
46
|
+
merge_strategy: 'first_wins')
|
|
47
|
+
behaviours = load_behaviours(behaviour_files, project_root: project_root)
|
|
48
|
+
check_circular_dependencies!(behaviours)
|
|
49
|
+
|
|
50
|
+
annotated_rules = annotate_rules(behaviours)
|
|
51
|
+
annotated_rules = filter_sensitive(annotated_rules, target) if target
|
|
52
|
+
|
|
53
|
+
sorted_rules = @priority_sorter.sort_rules(annotated_rules)
|
|
54
|
+
conflicts = @conflict_detector.detect(sorted_rules)
|
|
55
|
+
|
|
56
|
+
if strict && !conflicts.empty?
|
|
57
|
+
raise RosettAi::CompositionError,
|
|
58
|
+
"Composition conflicts in strict mode:\n#{conflicts.join("\n")}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
strategy = MergeStrategy.resolve(merge_strategy)
|
|
62
|
+
merged_rules = strategy.new.apply(sorted_rules)
|
|
63
|
+
|
|
64
|
+
trace = build_trace(merged_rules)
|
|
65
|
+
warnings = build_warnings(conflicts, behaviours)
|
|
66
|
+
sources = behaviours.to_h { |behaviour| [behaviour[:name], behaviour[:scope]] }
|
|
67
|
+
|
|
68
|
+
CompositionResult.new(
|
|
69
|
+
rules: merged_rules,
|
|
70
|
+
trace: trace,
|
|
71
|
+
warnings: warnings,
|
|
72
|
+
conflicts: conflicts,
|
|
73
|
+
sources: sources,
|
|
74
|
+
merge_strategy: merge_strategy
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def load_behaviours(files, project_root:)
|
|
81
|
+
files.map do |file|
|
|
82
|
+
data = RosettAi::YamlLoader.load_file(file)
|
|
83
|
+
scope = @scope_resolver.resolve(file, project_root: project_root)
|
|
84
|
+
{
|
|
85
|
+
name: data['name'],
|
|
86
|
+
path: file,
|
|
87
|
+
scope: scope,
|
|
88
|
+
scope_weight: @scope_resolver.weight(scope),
|
|
89
|
+
domain: data.fetch('domain', nil),
|
|
90
|
+
sensitive: data.fetch('sensitive', false),
|
|
91
|
+
depends_on: Array(data['depends_on']),
|
|
92
|
+
rules: Array(data['rules'])
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def check_circular_dependencies!(behaviours)
|
|
98
|
+
graph = behaviours.to_h { |behaviour| [behaviour[:name], behaviour[:depends_on]] }
|
|
99
|
+
cycle = @cycle_detector.detect(graph)
|
|
100
|
+
|
|
101
|
+
return unless cycle
|
|
102
|
+
|
|
103
|
+
raise RosettAi::CompositionError,
|
|
104
|
+
"Circular dependency detected: #{cycle.join(' -> ')}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def annotate_rules(behaviours)
|
|
108
|
+
behaviours.flat_map do |behaviour|
|
|
109
|
+
behaviour[:rules].map do |rule|
|
|
110
|
+
{
|
|
111
|
+
id: rule['id'],
|
|
112
|
+
description: rule['description'],
|
|
113
|
+
priority: rule.fetch('priority', 50),
|
|
114
|
+
enabled: rule.fetch('enabled', true),
|
|
115
|
+
behaviour_name: behaviour[:name],
|
|
116
|
+
scope: behaviour[:scope],
|
|
117
|
+
scope_weight: behaviour[:scope_weight],
|
|
118
|
+
domain: behaviour[:domain],
|
|
119
|
+
sensitive: behaviour[:sensitive]
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def filter_sensitive(rules, target)
|
|
126
|
+
return rules if target == 'claude' || target.nil?
|
|
127
|
+
|
|
128
|
+
rules.reject { |r| r[:sensitive] }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def build_trace(sorted_rules)
|
|
132
|
+
sorted_rules.each_with_index.map do |rule, index|
|
|
133
|
+
{
|
|
134
|
+
position: index + 1,
|
|
135
|
+
rule_id: rule[:id],
|
|
136
|
+
behaviour: rule[:behaviour_name],
|
|
137
|
+
scope: rule[:scope],
|
|
138
|
+
priority: rule[:priority]
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def build_warnings(conflicts, behaviours)
|
|
144
|
+
warnings = conflicts.dup
|
|
145
|
+
|
|
146
|
+
behaviours.each do |behaviour|
|
|
147
|
+
ids = behaviour[:rules].map { |r| r['id'] }
|
|
148
|
+
duplicates = ids.select { |id| ids.count(id) > 1 }.uniq
|
|
149
|
+
duplicates.each do |dup_id|
|
|
150
|
+
warnings << "Duplicate rule ID #{dup_id} within #{behaviour[:name]}.yml"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
warnings
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
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 Composition
|
|
8
|
+
# Value object representing the final result of behaviour composition.
|
|
9
|
+
# Contains the merged rules, metadata about how they were composed,
|
|
10
|
+
# and any warnings or conflicts detected during composition.
|
|
11
|
+
class CompositionResult
|
|
12
|
+
# @return [Array<Hash>] final merged rules in priority order
|
|
13
|
+
attr_reader :rules
|
|
14
|
+
|
|
15
|
+
# @return [Array<Hash>] trace entries showing composition decisions
|
|
16
|
+
attr_reader :trace
|
|
17
|
+
|
|
18
|
+
# @return [Array<String>] warnings emitted during composition
|
|
19
|
+
attr_reader :warnings
|
|
20
|
+
|
|
21
|
+
# @return [Array<String>] conflicts detected (rule ID collisions)
|
|
22
|
+
attr_reader :conflicts
|
|
23
|
+
|
|
24
|
+
# @return [Hash{String => String}] behaviour name => scope mapping
|
|
25
|
+
attr_reader :sources
|
|
26
|
+
|
|
27
|
+
# @return [String] merge strategy used during composition
|
|
28
|
+
attr_reader :merge_strategy
|
|
29
|
+
|
|
30
|
+
# @param rules [Array<Hash>] composed rules
|
|
31
|
+
# @param trace [Array<Hash>] composition trace entries
|
|
32
|
+
# @param warnings [Array<String>] composition warnings
|
|
33
|
+
# @param conflicts [Array<String>] detected conflicts
|
|
34
|
+
# @param sources [Hash{String => String}] behaviour source mapping
|
|
35
|
+
# @param merge_strategy [String] merge strategy name
|
|
36
|
+
# rubocop:disable Metrics/ParameterLists -- value object with all-keyword defaults
|
|
37
|
+
def initialize(rules:, trace: [], warnings: [], conflicts: [], sources: {},
|
|
38
|
+
merge_strategy: 'first_wins')
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
40
|
+
@rules = rules.freeze
|
|
41
|
+
@trace = trace.freeze
|
|
42
|
+
@warnings = warnings.freeze
|
|
43
|
+
@conflicts = conflicts.freeze
|
|
44
|
+
@sources = sources.freeze
|
|
45
|
+
@merge_strategy = merge_strategy.freeze
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [Integer] total number of composed rules
|
|
49
|
+
def rule_count
|
|
50
|
+
rules.size
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [Boolean] true if any conflicts were detected
|
|
54
|
+
def conflicts?
|
|
55
|
+
!conflicts.empty?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @return [Boolean] true if any warnings were emitted
|
|
59
|
+
def warnings?
|
|
60
|
+
!warnings.empty?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
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 Composition
|
|
8
|
+
# Detects rule ID collisions across behaviour files.
|
|
9
|
+
# When two behaviours define the same rule ID at the same scope
|
|
10
|
+
# and priority, this is a conflict that must be reported.
|
|
11
|
+
class ConflictDetector
|
|
12
|
+
# Detects rule ID collisions in a list of annotated rules.
|
|
13
|
+
#
|
|
14
|
+
# @param rules [Array<Hash>] rules with :id, :behaviour_name, :scope, :priority
|
|
15
|
+
# @return [Array<String>] conflict descriptions
|
|
16
|
+
def detect(rules)
|
|
17
|
+
conflicts = []
|
|
18
|
+
seen = {}
|
|
19
|
+
|
|
20
|
+
rules.each do |rule|
|
|
21
|
+
rule_id = rule[:id]
|
|
22
|
+
key = rule_id.to_s
|
|
23
|
+
|
|
24
|
+
if seen.key?(key)
|
|
25
|
+
existing = seen[key]
|
|
26
|
+
conflicts << format_conflict(rule_id, existing, rule)
|
|
27
|
+
else
|
|
28
|
+
seen[key] = rule
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
conflicts
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Checks if a security-domain rule would be overridden by a
|
|
36
|
+
# non-security rule. This is never allowed.
|
|
37
|
+
#
|
|
38
|
+
# @param existing [Hash] the existing rule
|
|
39
|
+
# @param candidate [Hash] the candidate replacement
|
|
40
|
+
# @return [Boolean] true if this is an invalid override
|
|
41
|
+
def security_override?(existing, candidate)
|
|
42
|
+
existing[:domain] == 'security' && candidate[:domain] != 'security'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format_conflict(rule_id, existing, candidate)
|
|
48
|
+
"Rule ID collision: #{rule_id} defined in both " \
|
|
49
|
+
"#{existing[:behaviour_name]}.yml and #{candidate[:behaviour_name]}.yml"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
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 'digest'
|
|
7
|
+
require 'yaml'
|
|
8
|
+
|
|
9
|
+
module RosettAi
|
|
10
|
+
module Composition
|
|
11
|
+
# Manages the `.rosett-ai-lock.yml` lockfile for reproducible behaviour
|
|
12
|
+
# composition. Writes the exact composition result so that
|
|
13
|
+
# `--frozen` compilations can reproduce the same output.
|
|
14
|
+
class Lockfile
|
|
15
|
+
FILENAME = '.rosett-ai-lock.yml'
|
|
16
|
+
|
|
17
|
+
# @param root [Pathname] project root directory
|
|
18
|
+
def initialize(root:)
|
|
19
|
+
@root = Pathname.new(root)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Pathname] full path to the lockfile
|
|
23
|
+
def path
|
|
24
|
+
@root.join(FILENAME)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] true if the lockfile exists
|
|
28
|
+
def exist?
|
|
29
|
+
path.exist?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Writes the lockfile from a CompositionResult.
|
|
33
|
+
#
|
|
34
|
+
# @param result [CompositionResult] composition result
|
|
35
|
+
# @param behaviours [Array<Hash>] source behaviour metadata
|
|
36
|
+
# @return [Pathname] path to the written lockfile
|
|
37
|
+
def write(result, behaviours:)
|
|
38
|
+
data = build_lockfile_data(result, behaviours)
|
|
39
|
+
path.write(YAML.dump(data))
|
|
40
|
+
path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Reads and parses the lockfile.
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash] lockfile data
|
|
46
|
+
# @raise [RosettAi::CompositionError] if lockfile is missing or invalid
|
|
47
|
+
def read
|
|
48
|
+
raise RosettAi::CompositionError, "Lockfile not found: #{path}" unless exist?
|
|
49
|
+
|
|
50
|
+
RosettAi::YamlLoader.load_file(path.to_s)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Checks if behaviour files have changed since the lockfile was written.
|
|
54
|
+
#
|
|
55
|
+
# @param behaviour_files [Array<String>] current behaviour file paths
|
|
56
|
+
# @return [Boolean] true if files match the lockfile
|
|
57
|
+
def fresh?(behaviour_files)
|
|
58
|
+
return false unless exist?
|
|
59
|
+
|
|
60
|
+
data = read
|
|
61
|
+
locked_checksums = data.fetch('behaviours', {})
|
|
62
|
+
current_checksums = compute_checksums(behaviour_files)
|
|
63
|
+
|
|
64
|
+
locked_checksums == current_checksums
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def build_lockfile_data(result, behaviours)
|
|
70
|
+
{
|
|
71
|
+
'lockfile_version' => 1,
|
|
72
|
+
'generated_at' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
|
|
73
|
+
'generator' => 'rosett-ai',
|
|
74
|
+
'generator_version' => RosettAi::VERSION,
|
|
75
|
+
'merge_strategy' => result.merge_strategy,
|
|
76
|
+
'rule_count' => result.rule_count,
|
|
77
|
+
'behaviours' => compute_checksums(behaviours.filter_map { |entry| entry[:path] }),
|
|
78
|
+
'composition' => result.rules.map do |rule|
|
|
79
|
+
{
|
|
80
|
+
'id' => rule[:id],
|
|
81
|
+
'behaviour' => rule[:behaviour_name],
|
|
82
|
+
'scope' => rule[:scope],
|
|
83
|
+
'priority' => rule[:priority]
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def compute_checksums(file_paths)
|
|
90
|
+
file_paths.sort.to_h do |fp|
|
|
91
|
+
content = File.read(fp, encoding: 'utf-8')
|
|
92
|
+
[relative_path(fp), "sha256:#{Digest::SHA256.hexdigest(content)}"]
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def relative_path(file_path)
|
|
97
|
+
Pathname.new(file_path).relative_path_from(@root).to_s
|
|
98
|
+
rescue ArgumentError
|
|
99
|
+
file_path.to_s
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
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 Composition
|
|
8
|
+
# Configurable merge strategies for resolving rule ID collisions across
|
|
9
|
+
# scopes. When the same rule ID exists in multiple behaviours, the merge
|
|
10
|
+
# strategy determines which value(s) appear in the final composition.
|
|
11
|
+
#
|
|
12
|
+
# @author hugo
|
|
13
|
+
# @author claude
|
|
14
|
+
module MergeStrategy
|
|
15
|
+
STRATEGIES = ['first_wins', 'deep_merge', 'array_union'].freeze
|
|
16
|
+
|
|
17
|
+
# Resolves the strategy class from a name string.
|
|
18
|
+
#
|
|
19
|
+
# @param name [String] strategy name (first_wins, deep_merge, array_union)
|
|
20
|
+
# @return [Class] strategy class
|
|
21
|
+
# @raise [RosettAi::CompositionError] if strategy name is unknown
|
|
22
|
+
def self.resolve(name)
|
|
23
|
+
case name.to_s
|
|
24
|
+
when 'first_wins' then FirstWins
|
|
25
|
+
when 'deep_merge' then DeepMerge
|
|
26
|
+
when 'array_union' then ArrayUnion
|
|
27
|
+
else
|
|
28
|
+
raise RosettAi::CompositionError,
|
|
29
|
+
"Unknown merge strategy: #{name}. Valid: #{STRATEGIES.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Groups rules by ID while preserving insertion order.
|
|
34
|
+
#
|
|
35
|
+
# @param rules [Array<Hash>] rules with :id key
|
|
36
|
+
# @return [Array<Array(String, Array<Hash>)>] ordered [id, group] pairs
|
|
37
|
+
def self.group_by_id(rules)
|
|
38
|
+
groups = rules.group_by { |r| r[:id] }
|
|
39
|
+
seen = {}
|
|
40
|
+
rules.filter_map do |rule|
|
|
41
|
+
id = rule[:id]
|
|
42
|
+
next if seen.key?(id)
|
|
43
|
+
|
|
44
|
+
seen[id] = true
|
|
45
|
+
[id, groups[id]]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# First-wins strategy: the highest-priority (first in sorted order)
|
|
50
|
+
# rule wins. Duplicate IDs from lower-priority scopes are dropped.
|
|
51
|
+
# This is the default behaviour and matches pre-merge-strategy semantics.
|
|
52
|
+
class FirstWins
|
|
53
|
+
# @param rules [Array<Hash>] sorted rules (highest priority first)
|
|
54
|
+
# @return [Array<Hash>] deduplicated rules (first occurrence wins)
|
|
55
|
+
def apply(rules)
|
|
56
|
+
MergeStrategy.group_by_id(rules).map { |_id, group| group.first }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Deep-merge strategy: when the same rule ID exists in multiple scopes,
|
|
61
|
+
# hash fields from all occurrences are recursively merged. Higher-priority
|
|
62
|
+
# values take precedence for scalar fields.
|
|
63
|
+
class DeepMerge
|
|
64
|
+
# @param rules [Array<Hash>] sorted rules (highest priority first)
|
|
65
|
+
# @return [Array<Hash>] merged rules (hashes deep-merged by ID)
|
|
66
|
+
def apply(rules)
|
|
67
|
+
MergeStrategy.group_by_id(rules).map do |_id, group|
|
|
68
|
+
next group.first if group.size == 1
|
|
69
|
+
|
|
70
|
+
merge_group(group)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def merge_group(group)
|
|
77
|
+
# Reverse so lowest-priority is merged first, highest-priority wins
|
|
78
|
+
group.reverse.reduce({}) { |merged, rule| deep_merge_hashes(merged, rule) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deep_merge_hashes(base, override)
|
|
82
|
+
base.merge(override) do |_key, old_val, new_val|
|
|
83
|
+
if old_val.is_a?(Hash) && new_val.is_a?(Hash)
|
|
84
|
+
deep_merge_hashes(old_val, new_val)
|
|
85
|
+
else
|
|
86
|
+
new_val
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Array-union strategy: when the same rule ID exists in multiple scopes,
|
|
93
|
+
# array fields are combined (union) and deduplicated. Scalar fields use
|
|
94
|
+
# highest-priority value.
|
|
95
|
+
class ArrayUnion
|
|
96
|
+
# @param rules [Array<Hash>] sorted rules (highest priority first)
|
|
97
|
+
# @return [Array<Hash>] merged rules (arrays unioned by ID)
|
|
98
|
+
def apply(rules)
|
|
99
|
+
MergeStrategy.group_by_id(rules).map do |_id, group|
|
|
100
|
+
next group.first if group.size == 1
|
|
101
|
+
|
|
102
|
+
merge_group(group)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def merge_group(group)
|
|
109
|
+
# Start with highest-priority, union arrays from lower-priority
|
|
110
|
+
group.reduce({}) do |merged, rule|
|
|
111
|
+
if merged.empty?
|
|
112
|
+
rule
|
|
113
|
+
else
|
|
114
|
+
union_merge(merged, rule)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def union_merge(base, other)
|
|
120
|
+
base.merge(other) do |_key, old_val, new_val|
|
|
121
|
+
if old_val.is_a?(Array) && new_val.is_a?(Array)
|
|
122
|
+
(old_val | new_val)
|
|
123
|
+
else
|
|
124
|
+
old_val # higher-priority wins for non-arrays
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
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 Composition
|
|
8
|
+
# Sorts rules by priority within their scope.
|
|
9
|
+
# Lower priority numbers are higher importance (1 = most important).
|
|
10
|
+
# Equal-priority rules within the same scope are ordered by behaviour
|
|
11
|
+
# name alphabetically for deterministic output.
|
|
12
|
+
class PrioritySorter
|
|
13
|
+
# Sorts an array of annotated rules by scope weight, then priority,
|
|
14
|
+
# then behaviour name.
|
|
15
|
+
#
|
|
16
|
+
# @param rules [Array<Hash>] rules with :scope_weight, :priority, :behaviour_name keys
|
|
17
|
+
# @return [Array<Hash>] sorted rules (stable, deterministic)
|
|
18
|
+
def sort_rules(rules)
|
|
19
|
+
rules.sort_by do |rule|
|
|
20
|
+
[
|
|
21
|
+
-(rule[:scope_weight] || 0), # higher scope weight first (negate for ascending sort)
|
|
22
|
+
rule[:priority] || 50, # lower priority number first
|
|
23
|
+
rule[:behaviour_name].to_s # alphabetical tie-break
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
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 Composition
|
|
8
|
+
# Resolves the scope hierarchy for behaviour files.
|
|
9
|
+
# Scopes are ordered: global > organisation > project > local.
|
|
10
|
+
# Later scopes override earlier ones at equal priority.
|
|
11
|
+
class ScopeResolver
|
|
12
|
+
# Ordered scope names from lowest to highest precedence.
|
|
13
|
+
SCOPE_ORDER = ['global', 'organisation', 'project', 'local'].freeze
|
|
14
|
+
|
|
15
|
+
# Numeric weight for each scope (higher = takes precedence).
|
|
16
|
+
SCOPE_WEIGHTS = SCOPE_ORDER.each_with_index.to_h { |scope, i| [scope, i] }.freeze
|
|
17
|
+
|
|
18
|
+
# Resolves the scope for a given behaviour file path.
|
|
19
|
+
#
|
|
20
|
+
# @param file_path [String, Pathname] path to the behaviour file
|
|
21
|
+
# @param project_root [Pathname, nil] detected project root
|
|
22
|
+
# @return [String] one of SCOPE_ORDER values
|
|
23
|
+
def resolve(file_path, project_root: nil)
|
|
24
|
+
path = file_path.to_s
|
|
25
|
+
|
|
26
|
+
if project_root && path.start_with?(project_root.join('.rosett-ai', 'conf', 'behaviour').to_s)
|
|
27
|
+
'project'
|
|
28
|
+
elsif path.include?('/organisation/') || path.include?('/org/')
|
|
29
|
+
'organisation'
|
|
30
|
+
elsif path.include?('/local/')
|
|
31
|
+
'local'
|
|
32
|
+
else
|
|
33
|
+
'global'
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the numeric weight for a scope name.
|
|
38
|
+
#
|
|
39
|
+
# @param scope [String] scope name
|
|
40
|
+
# @return [Integer] weight (higher = overrides lower)
|
|
41
|
+
def weight(scope)
|
|
42
|
+
SCOPE_WEIGHTS.fetch(scope, 0)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Compares two scopes. Returns true if scope_a overrides scope_b.
|
|
46
|
+
#
|
|
47
|
+
# @param scope_a [String] candidate overriding scope
|
|
48
|
+
# @param scope_b [String] candidate overridden scope
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def overrides?(scope_a, scope_b)
|
|
51
|
+
weight(scope_a) > weight(scope_b)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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 Config
|
|
8
|
+
# Structured result from compiling a single scope file.
|
|
9
|
+
#
|
|
10
|
+
# @!attribute scope [String] The scope name (managed, user, project, local)
|
|
11
|
+
# @!attribute source_path [Pathname] Path to the source YAML file
|
|
12
|
+
# @!attribute target_path [Pathname] Path to the target JSON file
|
|
13
|
+
# @!attribute json_data [Hash] The compiled JSON data (before serialization)
|
|
14
|
+
# @!attribute warnings [Array<String>] Advisory messages (version mismatch, missing env vars, etc.)
|
|
15
|
+
# @!attribute action [Symbol] One of :created, :updated, :unchanged, :skipped
|
|
16
|
+
# @!attribute diff [String, nil] Unified diff string (for simulate mode)
|
|
17
|
+
CompileResult = Struct.new(
|
|
18
|
+
:scope, :source_path, :target_path, :json_data,
|
|
19
|
+
:warnings, :action, :diff
|
|
20
|
+
) do
|
|
21
|
+
def initialize(**)
|
|
22
|
+
super
|
|
23
|
+
self.warnings ||= []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Boolean] true if action is :created, :updated, or :unchanged
|
|
27
|
+
def success?
|
|
28
|
+
[:created, :updated, :unchanged].include?(action)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @return [Boolean] true if action is :created or :updated
|
|
32
|
+
def changed?
|
|
33
|
+
[:created, :updated].include?(action)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
# Configuration subsystem.
|
|
8
|
+
module Config
|
|
9
|
+
# Deprecated — use RosettAiEngine::Claude::ConfigCompiler. Will be removed in v1.1.0.
|
|
10
|
+
# Only defined when rosett-ai-engine-claude gem is installed.
|
|
11
|
+
Compiler = RosettAiEngine::Claude::ConfigCompiler if defined?(RosettAiEngine::Claude::ConfigCompiler)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
# Configuration subsystem.
|
|
8
|
+
module Config
|
|
9
|
+
# Deprecated — use RosettAiEngine::Claude::DomainTransformer. Will be removed in v1.1.0.
|
|
10
|
+
# Only defined when rosett-ai-engine-claude gem is installed.
|
|
11
|
+
DomainTransformer = RosettAiEngine::Claude::DomainTransformer if defined?(RosettAiEngine::Claude::DomainTransformer)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
# Configuration subsystem.
|
|
8
|
+
module Config
|
|
9
|
+
# Deprecated — use RosettAiEngine::Claude::KeyMap. Will be removed in v1.1.0.
|
|
10
|
+
# Only defined when rosett-ai-engine-claude gem is installed.
|
|
11
|
+
KeyMap = RosettAiEngine::Claude::KeyMap if defined?(RosettAiEngine::Claude::KeyMap)
|
|
12
|
+
end
|
|
13
|
+
end
|