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,40 @@
|
|
|
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 Compiler
|
|
8
|
+
# Backward-compatible wrapper around CompilationPipeline.
|
|
9
|
+
#
|
|
10
|
+
# Preserves the original 3-argument constructor and defaults the
|
|
11
|
+
# backend to the Claude engine backend. Existing callers that instantiate
|
|
12
|
+
# BehaviourCompiler continue to work without modification.
|
|
13
|
+
class BehaviourCompiler < CompilationPipeline
|
|
14
|
+
GENERATED_MARKER = '<!-- rosett-ai-claude-managed'
|
|
15
|
+
|
|
16
|
+
# Backward-compatible wrapper that defaults to the Claude engine backend.
|
|
17
|
+
#
|
|
18
|
+
# @param source_dir [String, Pathname] directory containing behaviour YAML files
|
|
19
|
+
# @param target_dir [String, Pathname] directory for compiled output files
|
|
20
|
+
# @param schema_dir [String, Pathname] directory containing JSON Schema files
|
|
21
|
+
def initialize(source_dir:, target_dir:, schema_dir:)
|
|
22
|
+
super(
|
|
23
|
+
source_dir: source_dir,
|
|
24
|
+
target_dir: target_dir,
|
|
25
|
+
schema_dir: schema_dir,
|
|
26
|
+
backend: Backend.for('claude')
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Delegates render to the backend for backward compatibility.
|
|
31
|
+
#
|
|
32
|
+
# @param category [String] the behaviour category name
|
|
33
|
+
# @param data [Hash] behaviour data to render
|
|
34
|
+
# @return [String] rendered output from the backend
|
|
35
|
+
def render(category, data)
|
|
36
|
+
backend.render(data.merge('category' => category))
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
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 Compiler
|
|
8
|
+
# Checks compiled output against engine capability manifest and emits
|
|
9
|
+
# warnings when the source YAML uses features the target engine does
|
|
10
|
+
# not support (e.g. sensitive filtering, rule metadata).
|
|
11
|
+
#
|
|
12
|
+
# Called between compiler.compile and process_compiled in the CLI.
|
|
13
|
+
# With --strict, warnings become CompileErrors.
|
|
14
|
+
class CapabilityChecker
|
|
15
|
+
def initialize(engine_name)
|
|
16
|
+
@engine_name = engine_name.to_s
|
|
17
|
+
@manifest = load_manifest
|
|
18
|
+
@capabilities = @manifest['capabilities'] || {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Check compiled results against engine capabilities.
|
|
22
|
+
#
|
|
23
|
+
# @param source_dir [Pathname] conf/ directory to scan source YAML
|
|
24
|
+
# @return [Array<Hash>] warning hashes with :feature, :file, :message keys
|
|
25
|
+
def check(source_dir)
|
|
26
|
+
behaviour_dir = source_dir.join('behaviour')
|
|
27
|
+
return [] unless behaviour_dir.exist?
|
|
28
|
+
|
|
29
|
+
files = load_behaviour_files(behaviour_dir)
|
|
30
|
+
warnings = []
|
|
31
|
+
warnings.concat(sensitive_warnings(files))
|
|
32
|
+
warnings.concat(metadata_warnings(files))
|
|
33
|
+
warnings
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Summarized check — groups warnings by feature type.
|
|
37
|
+
#
|
|
38
|
+
# @param source_dir [Pathname] conf/ directory to scan source YAML
|
|
39
|
+
# @return [Array<Hash>] summary hashes with :feature, :count, :message keys
|
|
40
|
+
def check_summarized(source_dir)
|
|
41
|
+
warnings = check(source_dir)
|
|
42
|
+
warnings.group_by { |w| w[:feature] }.map do |feature, group|
|
|
43
|
+
{
|
|
44
|
+
feature: feature,
|
|
45
|
+
count: group.size,
|
|
46
|
+
message: "Engine '#{@engine_name}' does not support #{feature} — " \
|
|
47
|
+
"#{group.size} rule(s) compiled without it"
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Load engine manifest from plugin registry. Returns an empty hash
|
|
55
|
+
# for builtin backends (claude, agents_md) that don't have plugin
|
|
56
|
+
# registration — builtin backends support all capabilities.
|
|
57
|
+
def load_manifest
|
|
58
|
+
RosettAi::Engines::Registry.manifest(@engine_name)
|
|
59
|
+
rescue RosettAi::Plugins::Registry::PluginNotFoundError
|
|
60
|
+
{ 'capabilities' => { 'sensitive_filtering' => true, 'rule_metadata' => true } }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def load_behaviour_files(behaviour_dir)
|
|
64
|
+
Dir.glob(behaviour_dir.join('*.yml')).filter_map do |path|
|
|
65
|
+
data = RosettAi::YamlLoader.load_file(path)
|
|
66
|
+
next unless data.is_a?(Hash)
|
|
67
|
+
|
|
68
|
+
{ path: path, basename: File.basename(path), data: data }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sensitive_warnings(files)
|
|
73
|
+
return [] if @capabilities['sensitive_filtering']
|
|
74
|
+
|
|
75
|
+
files.filter_map do |entry|
|
|
76
|
+
next unless entry[:data]['sensitive'] == true
|
|
77
|
+
|
|
78
|
+
build_warning('sensitive_filtering', entry[:basename],
|
|
79
|
+
"is marked sensitive but engine '#{@engine_name}' does not support sensitive filtering")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def metadata_warnings(files)
|
|
84
|
+
return [] if @capabilities['rule_metadata']
|
|
85
|
+
|
|
86
|
+
files.filter_map do |entry|
|
|
87
|
+
next unless rules_have_metadata?(entry[:data])
|
|
88
|
+
|
|
89
|
+
build_warning('rule_metadata', entry[:basename],
|
|
90
|
+
"uses rule IDs/priority but engine '#{@engine_name}' does not preserve rule metadata")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rules_have_metadata?(data)
|
|
95
|
+
rules = data['rules'] || []
|
|
96
|
+
rules.any? { |r| r['id'] || r['priority'] }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def build_warning(feature, filename, detail)
|
|
100
|
+
{ feature: feature, file: filename, message: "#{filename} #{detail}" }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,361 @@
|
|
|
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 'yaml'
|
|
7
|
+
require 'json'
|
|
8
|
+
require 'digest'
|
|
9
|
+
require 'json_schemer'
|
|
10
|
+
|
|
11
|
+
module RosettAi
|
|
12
|
+
module Compiler
|
|
13
|
+
# Core compilation pipeline that transforms YAML configuration files
|
|
14
|
+
# into target-specific output formats.
|
|
15
|
+
#
|
|
16
|
+
# Delegates rendering to a pluggable Backend instance. Owns discovery,
|
|
17
|
+
# validation, checksumming, diffing, lockfile generation, and orphan
|
|
18
|
+
# management.
|
|
19
|
+
class CompilationPipeline # rubocop:disable Metrics/ClassLength
|
|
20
|
+
# Categories excluded from compilation. These directories contain
|
|
21
|
+
# configuration files validated by their respective modules
|
|
22
|
+
# (e.g., Policy::DenyList, Build::Package) rather than the
|
|
23
|
+
# compile pipeline.
|
|
24
|
+
NON_COMPILABLE_CATEGORIES = ['packaging', 'policy', 'tooling'].freeze
|
|
25
|
+
|
|
26
|
+
# Categories compiled only in project scope. Design documents are
|
|
27
|
+
# project specifications, not operational rules — compiling them
|
|
28
|
+
# globally would pollute ~/.claude/rules/ with empty files.
|
|
29
|
+
PROJECT_ONLY_CATEGORIES = ['design'].freeze
|
|
30
|
+
|
|
31
|
+
VALID_SCOPES = [:global, :project].freeze
|
|
32
|
+
|
|
33
|
+
attr_reader :source_dir, :target_dir, :schema_dir, :backend, :scope,
|
|
34
|
+
:additional_source_dirs
|
|
35
|
+
|
|
36
|
+
# @param source_dir [String, Pathname] primary conf/ root (project scope when in project)
|
|
37
|
+
# @param target_dir [String, Pathname] output directory
|
|
38
|
+
# @param schema_dir [String, Pathname] JSON Schema directory
|
|
39
|
+
# @param backend [RosettAi::Compiler::Backend] rendering backend
|
|
40
|
+
# @param scope [Symbol] compilation scope (:global or :project)
|
|
41
|
+
# @param additional_source_dirs [Array<String, Pathname>] extra source directories
|
|
42
|
+
# (e.g. global conf/ when compiling in project scope). Files from additional
|
|
43
|
+
# sources are included unless overridden by a file with the same basename
|
|
44
|
+
# in the primary source_dir. Empty by default for backward compatibility.
|
|
45
|
+
# @param source_dir [String, Pathname] primary conf/ root
|
|
46
|
+
# @param target_dir [String, Pathname] output directory
|
|
47
|
+
# @param schema_dir [String, Pathname] JSON Schema directory
|
|
48
|
+
# @param backend [RosettAi::Compiler::Backend] rendering backend
|
|
49
|
+
# @param scope [Symbol] compilation scope (:global or :project)
|
|
50
|
+
# @param additional_source_dirs [Array<String, Pathname>] extra source
|
|
51
|
+
# directories (e.g. global conf/ when compiling in project scope)
|
|
52
|
+
def initialize(source_dir:, target_dir:, schema_dir:, backend:, # rubocop:disable Metrics/ParameterLists
|
|
53
|
+
scope: :global, additional_source_dirs: [])
|
|
54
|
+
validate_scope!(scope)
|
|
55
|
+
@source_dir = Pathname.new(source_dir)
|
|
56
|
+
@target_dir = Pathname.new(target_dir)
|
|
57
|
+
@schema_dir = Pathname.new(schema_dir)
|
|
58
|
+
@backend = backend
|
|
59
|
+
@scope = scope
|
|
60
|
+
@additional_source_dirs = additional_source_dirs.map { |d| Pathname.new(d) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns project-only categories that were skipped in this scope.
|
|
64
|
+
#
|
|
65
|
+
# @return [Array<String>] skipped category names (empty for project scope)
|
|
66
|
+
def skipped_project_categories
|
|
67
|
+
scope == :global ? PROJECT_ONLY_CATEGORIES : []
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Discovers compilable config categories across all source directories.
|
|
71
|
+
#
|
|
72
|
+
# @return [Array<String>] category names with matching schemas
|
|
73
|
+
def discover_categories
|
|
74
|
+
skip = ['schemas', 'targets'] + NON_COMPILABLE_CATEGORIES
|
|
75
|
+
skip += PROJECT_ONLY_CATEGORIES if scope == :global
|
|
76
|
+
|
|
77
|
+
categories = Set.new
|
|
78
|
+
all_source_dirs.each do |sdir|
|
|
79
|
+
next unless sdir.exist?
|
|
80
|
+
|
|
81
|
+
Dir.children(sdir).each do |entry|
|
|
82
|
+
next unless sdir.join(entry).directory?
|
|
83
|
+
next if skip.include?(entry)
|
|
84
|
+
|
|
85
|
+
categories << entry
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
categories.select do |category|
|
|
90
|
+
schema_dir.join("#{category}_schema.json").exist?
|
|
91
|
+
end.sort
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Compiles all discovered categories into rendered output.
|
|
95
|
+
# When additional_source_dirs are present, files are collected from
|
|
96
|
+
# all sources. The primary source_dir takes precedence: a file with
|
|
97
|
+
# the same basename in source_dir overrides one from additional sources.
|
|
98
|
+
#
|
|
99
|
+
# After compilation, runs duplicate rule ID detection across all
|
|
100
|
+
# behaviour files. Access warnings via {#conflict_warnings}.
|
|
101
|
+
#
|
|
102
|
+
# @return [Hash{String => CompiledOutput}] keyed by output filename
|
|
103
|
+
def compile
|
|
104
|
+
results = {}
|
|
105
|
+
all_category_files = {}
|
|
106
|
+
discover_categories.each do |category|
|
|
107
|
+
yaml_files = collect_category_files(category)
|
|
108
|
+
yaml_files.concat(premium_files_for(category))
|
|
109
|
+
all_category_files[category] = yaml_files
|
|
110
|
+
yaml_files.each do |file|
|
|
111
|
+
data = load_yaml(file)
|
|
112
|
+
next unless data
|
|
113
|
+
|
|
114
|
+
validate!(category, data, file)
|
|
115
|
+
key, output = compile_file(category, data, file)
|
|
116
|
+
results[key] = output
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
@conflict_warnings = detect_duplicate_rule_ids(all_category_files)
|
|
120
|
+
results
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns conflict warnings from the most recent compile run.
|
|
124
|
+
#
|
|
125
|
+
# @return [Array<String>] conflict warning messages
|
|
126
|
+
def conflict_warnings
|
|
127
|
+
@conflict_warnings || []
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Validates data against the JSON Schema for the given category.
|
|
131
|
+
#
|
|
132
|
+
# @param category [String] configuration category name
|
|
133
|
+
# @param data [Hash] parsed YAML data to validate
|
|
134
|
+
# @param file [String] source file path (used in error messages)
|
|
135
|
+
# @raise [RosettAi::CompileError] when validation fails
|
|
136
|
+
# @return [void]
|
|
137
|
+
def validate!(category, data, file)
|
|
138
|
+
schema_file = schema_dir.join("#{category}_schema.json")
|
|
139
|
+
schema = JSON.parse(schema_file.read)
|
|
140
|
+
schemer = JSONSchemer.schema(schema)
|
|
141
|
+
errors = schemer.validate(data).to_a
|
|
142
|
+
return if errors.empty?
|
|
143
|
+
|
|
144
|
+
messages = errors.map do |err|
|
|
145
|
+
path = err['data_pointer'].empty? ? 'root' : err['data_pointer']
|
|
146
|
+
"#{path}: #{err['type']}"
|
|
147
|
+
end
|
|
148
|
+
raise RosettAi::CompileError, "Validation failed for #{file}: #{messages.join(', ')}"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Computes a SHA-256 hex digest of the given content.
|
|
152
|
+
#
|
|
153
|
+
# @param content [String] content to hash
|
|
154
|
+
# @return [String] hex-encoded SHA-256 digest
|
|
155
|
+
def checksum(content)
|
|
156
|
+
Digest::SHA256.hexdigest(content)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Checks whether the file at path was generated by the compiler.
|
|
160
|
+
#
|
|
161
|
+
# @param path [String] file path to check
|
|
162
|
+
# @return [Boolean]
|
|
163
|
+
def managed_file?(path)
|
|
164
|
+
backend.managed_file?(path)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Finds managed target files not present in the compiled set.
|
|
168
|
+
#
|
|
169
|
+
# @param compiled_names [Array<String>] basenames of compiled outputs
|
|
170
|
+
# @return [Array<String>] full paths of orphaned files
|
|
171
|
+
def orphaned_files(compiled_names)
|
|
172
|
+
return [] unless target_dir.exist?
|
|
173
|
+
|
|
174
|
+
Dir.glob(target_dir.join("*#{backend.file_extension}")).select do |file|
|
|
175
|
+
managed_file?(file) && !compiled_names.include?(File.basename(file))
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Generates a unified diff between an existing file and new content.
|
|
180
|
+
#
|
|
181
|
+
# @param existing_path [String] path to the existing file
|
|
182
|
+
# @param new_content [String] proposed new content
|
|
183
|
+
# @return [String, nil] unified diff string, or nil if unchanged/missing
|
|
184
|
+
def diff(existing_path, new_content)
|
|
185
|
+
return nil unless File.exist?(existing_path)
|
|
186
|
+
|
|
187
|
+
existing = File.read(existing_path, encoding: 'utf-8')
|
|
188
|
+
return nil if existing == new_content
|
|
189
|
+
|
|
190
|
+
generate_unified_diff(existing_path, existing, new_content)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Builds a lockfile hash from compiled outputs.
|
|
194
|
+
#
|
|
195
|
+
# @param compiled [Hash] compiled output map (filename => info)
|
|
196
|
+
# @return [Hash] lockfile structure suitable for YAML serialization
|
|
197
|
+
def lockfile_data(compiled)
|
|
198
|
+
timestamp = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
|
|
199
|
+
categories = {}
|
|
200
|
+
compiled.each_value do |info|
|
|
201
|
+
cat = info[:category]
|
|
202
|
+
categories[cat] ||= { 'schema' => "conf/schemas/#{cat}_schema.json", 'files' => {} }
|
|
203
|
+
categories[cat]['files'][info[:name]] = info.to_lockfile_hash(
|
|
204
|
+
display_path: RosettAi.paths.rules_display_path,
|
|
205
|
+
source_path: relative_path(info[:source]),
|
|
206
|
+
content_checksum: checksum(info[:content]),
|
|
207
|
+
timestamp: timestamp
|
|
208
|
+
)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
{
|
|
212
|
+
'generated_at' => timestamp,
|
|
213
|
+
'generator' => 'rosett-ai',
|
|
214
|
+
'generator_version' => RosettAi::VERSION,
|
|
215
|
+
'target_directory' => RosettAi.paths.rules_display_path,
|
|
216
|
+
'categories' => categories
|
|
217
|
+
}
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
private
|
|
221
|
+
|
|
222
|
+
def compile_file(category, data, file)
|
|
223
|
+
name = RosettAi::TextSanitizer.normalize_nfc(File.basename(file, '.yml'))
|
|
224
|
+
key = "#{category}-#{name}#{backend.file_extension}"
|
|
225
|
+
rules = data['rules'] || []
|
|
226
|
+
output = CompiledOutput.new(
|
|
227
|
+
filename: key,
|
|
228
|
+
content: backend.render(data.merge('category' => category)),
|
|
229
|
+
source: file,
|
|
230
|
+
category: category,
|
|
231
|
+
name: name,
|
|
232
|
+
version: data['version'] || 'unknown',
|
|
233
|
+
rules_count: rules.size,
|
|
234
|
+
enabled_rules_count: rules.count { |r| r.fetch('enabled', true) },
|
|
235
|
+
scope: data['scope']
|
|
236
|
+
)
|
|
237
|
+
[key, output]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def validate_scope!(scope)
|
|
241
|
+
return if VALID_SCOPES.include?(scope)
|
|
242
|
+
|
|
243
|
+
raise ArgumentError, "Invalid scope: #{scope.inspect} (valid: #{VALID_SCOPES.join(', ')})"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Returns all source directories in merge order: additional sources
|
|
247
|
+
# first (global), then primary (project). Later entries in the array
|
|
248
|
+
# override earlier ones when files share the same basename.
|
|
249
|
+
#
|
|
250
|
+
# @return [Array<Pathname>]
|
|
251
|
+
def all_source_dirs
|
|
252
|
+
@additional_source_dirs + [@source_dir]
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Collects YAML files for a category from all source directories.
|
|
256
|
+
# Files from later sources (project) override earlier sources (global)
|
|
257
|
+
# when they share the same basename. PROJECT_ONLY_CATEGORIES are
|
|
258
|
+
# collected only from the primary source_dir.
|
|
259
|
+
#
|
|
260
|
+
# @param category [String] category name
|
|
261
|
+
# @return [Array<String>] deduplicated file paths
|
|
262
|
+
def collect_category_files(category)
|
|
263
|
+
sources = project_only_category?(category) ? [@source_dir] : all_source_dirs
|
|
264
|
+
seen = {}
|
|
265
|
+
sources.each do |sdir|
|
|
266
|
+
Dir.glob(sdir.join(category, '*.yml')).each do |file|
|
|
267
|
+
seen[File.basename(file, '.yml')] = file
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
seen.values
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def project_only_category?(category)
|
|
274
|
+
PROJECT_ONLY_CATEGORIES.include?(category)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def premium_files_for(category)
|
|
278
|
+
premium_dir = RosettAi.paths.premium_content_dir
|
|
279
|
+
return [] unless premium_dir.exist?
|
|
280
|
+
return [] unless RosettAi::Licensing::LicenseValidator.new.validate[:valid]
|
|
281
|
+
|
|
282
|
+
Dir.glob(premium_dir.join('*', "#{category}s", '*.yml'))
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def load_yaml(file)
|
|
286
|
+
RosettAi::YamlLoader.load_file(file)
|
|
287
|
+
rescue Psych::SyntaxError => e
|
|
288
|
+
raise RosettAi::CompileError, "YAML syntax error in #{file}: #{e.message}"
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def relative_path(path)
|
|
292
|
+
Pathname.new(path).relative_path_from(RosettAi.root).to_s
|
|
293
|
+
rescue ArgumentError
|
|
294
|
+
path.to_s
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Detects duplicate rule IDs within each behaviour file.
|
|
298
|
+
# Rule IDs are scoped per-file (e.g. criticalthinking/rule_001 vs
|
|
299
|
+
# operational_discipline/rule_001 are independent), so only
|
|
300
|
+
# duplicates within a single file are flagged.
|
|
301
|
+
#
|
|
302
|
+
# @param category_files [Hash{String => Array<String>}] category => file paths
|
|
303
|
+
# @return [Array<String>] warning messages for duplicate rule IDs
|
|
304
|
+
def detect_duplicate_rule_ids(category_files)
|
|
305
|
+
duplicates = []
|
|
306
|
+
|
|
307
|
+
category_files.each_value do |files|
|
|
308
|
+
files.each do |path|
|
|
309
|
+
data = load_yaml(path)
|
|
310
|
+
next unless data
|
|
311
|
+
|
|
312
|
+
basename = File.basename(path, '.yml')
|
|
313
|
+
seen = {}
|
|
314
|
+
(data['rules'] || []).each do |rule|
|
|
315
|
+
rule_id = rule['id']
|
|
316
|
+
next unless rule_id
|
|
317
|
+
|
|
318
|
+
if seen.key?(rule_id)
|
|
319
|
+
duplicates << "Duplicate rule ID '#{rule_id}' in #{basename}.yml " \
|
|
320
|
+
'(appears more than once)'
|
|
321
|
+
else
|
|
322
|
+
seen[rule_id] = true
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
duplicates
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def generate_unified_diff(path, old_content, new_content)
|
|
332
|
+
old_lines = old_content.lines
|
|
333
|
+
new_lines = new_content.lines
|
|
334
|
+
basename = File.basename(path)
|
|
335
|
+
|
|
336
|
+
lines = []
|
|
337
|
+
lines << "--- a/#{basename}"
|
|
338
|
+
lines << "+++ b/#{basename}"
|
|
339
|
+
|
|
340
|
+
max = [old_lines.size, new_lines.size].max
|
|
341
|
+
changes = []
|
|
342
|
+
(0...max).each do |i|
|
|
343
|
+
old_line = old_lines[i]
|
|
344
|
+
new_line = new_lines[i]
|
|
345
|
+
next if old_line == new_line
|
|
346
|
+
|
|
347
|
+
changes << { index: i, old: old_line, new: new_line }
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
changes.each do |change|
|
|
351
|
+
i = change[:index]
|
|
352
|
+
lines << "@@ -#{i + 1} +#{i + 1} @@"
|
|
353
|
+
lines << "-#{change[:old]&.chomp}" if change[:old]
|
|
354
|
+
lines << "+#{change[:new]&.chomp}" if change[:new]
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
lines.join("\n")
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
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 Compiler
|
|
8
|
+
# Value object representing a single compiled output file.
|
|
9
|
+
#
|
|
10
|
+
# Replaces the raw Hash previously returned by BehaviourCompiler#compile.
|
|
11
|
+
# Struct supports both .field and [:field] access, so callers using
|
|
12
|
+
# info[:content] continue to work without modification.
|
|
13
|
+
CompiledOutput = Struct.new(
|
|
14
|
+
:filename, :content, :source, :category, :name, :version,
|
|
15
|
+
:rules_count, :enabled_rules_count, :scope
|
|
16
|
+
) do
|
|
17
|
+
# Builds a lockfile entry hash for this compiled output.
|
|
18
|
+
#
|
|
19
|
+
# @param display_path [String] display prefix for output path
|
|
20
|
+
# @param source_path [String] relative source path
|
|
21
|
+
# @param content_checksum [String] SHA-256 hex digest of content
|
|
22
|
+
# @param timestamp [String] ISO 8601 compilation timestamp
|
|
23
|
+
# @return [Hash] lockfile entry
|
|
24
|
+
def to_lockfile_hash(display_path:, source_path:, content_checksum:, timestamp:)
|
|
25
|
+
entry = {
|
|
26
|
+
'version' => version,
|
|
27
|
+
'source' => source_path,
|
|
28
|
+
'output' => "#{display_path}/#{filename}",
|
|
29
|
+
'checksum' => "sha256:#{content_checksum}",
|
|
30
|
+
'compiled_at' => timestamp,
|
|
31
|
+
'rules_count' => rules_count,
|
|
32
|
+
'enabled_rules_count' => enabled_rules_count
|
|
33
|
+
}
|
|
34
|
+
entry['scope'] = scope if scope
|
|
35
|
+
entry
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|