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,138 @@
|
|
|
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
|
+
|
|
8
|
+
module RosettAi
|
|
9
|
+
module Project
|
|
10
|
+
# Handles upstream synchronization for rosett-ai-managed projects.
|
|
11
|
+
#
|
|
12
|
+
# Detects breaking changes when upstream templates are updated and
|
|
13
|
+
# applies updates with user confirmation or --force flag.
|
|
14
|
+
#
|
|
15
|
+
# @author hugo
|
|
16
|
+
# @author claude
|
|
17
|
+
class SyncManager
|
|
18
|
+
# @param project_root [Pathname] root directory of the project
|
|
19
|
+
# @param force [Boolean] apply breaking changes without prompting
|
|
20
|
+
def initialize(project_root:, force: false)
|
|
21
|
+
@project_root = project_root
|
|
22
|
+
@project_dir = project_root.join('.rosett-ai')
|
|
23
|
+
@force = force
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Synchronizes the project with upstream template updates.
|
|
27
|
+
#
|
|
28
|
+
# @return [Hash] result with :updated, :conflicts, :skipped arrays
|
|
29
|
+
def sync
|
|
30
|
+
validate!
|
|
31
|
+
origin = load_origin
|
|
32
|
+
template_name = origin['template']
|
|
33
|
+
template_dir = RosettAi.root.join(TemplateApplier::TEMPLATES_DIR, template_name)
|
|
34
|
+
|
|
35
|
+
unless template_dir.directory?
|
|
36
|
+
return { updated: [], conflicts: [], skipped: [],
|
|
37
|
+
error: 'Template no longer available' }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
results = { updated: [], conflicts: [], skipped: [] }
|
|
41
|
+
|
|
42
|
+
Dir.glob(template_dir.join('**', '*').to_s).each do |tmpl_path|
|
|
43
|
+
tmpl = Pathname.new(tmpl_path)
|
|
44
|
+
next if tmpl.directory?
|
|
45
|
+
|
|
46
|
+
relative = tmpl.relative_path_from(template_dir)
|
|
47
|
+
dest = @project_dir.join(relative)
|
|
48
|
+
|
|
49
|
+
sync_file(tmpl, dest, relative, results)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
update_origin_timestamp(origin) unless results[:updated].empty?
|
|
53
|
+
results
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Preview what sync would change without applying.
|
|
57
|
+
#
|
|
58
|
+
# @return [Hash] same structure as sync but without writing files
|
|
59
|
+
def simulate
|
|
60
|
+
validate!
|
|
61
|
+
origin = load_origin
|
|
62
|
+
template_name = origin['template']
|
|
63
|
+
template_dir = RosettAi.root.join(TemplateApplier::TEMPLATES_DIR, template_name)
|
|
64
|
+
|
|
65
|
+
return { updated: [], conflicts: [], skipped: [] } unless template_dir.directory?
|
|
66
|
+
|
|
67
|
+
results = { updated: [], conflicts: [], skipped: [] }
|
|
68
|
+
|
|
69
|
+
Dir.glob(template_dir.join('**', '*').to_s).each do |tmpl_path|
|
|
70
|
+
tmpl = Pathname.new(tmpl_path)
|
|
71
|
+
next if tmpl.directory?
|
|
72
|
+
|
|
73
|
+
relative = tmpl.relative_path_from(template_dir)
|
|
74
|
+
dest = @project_dir.join(relative)
|
|
75
|
+
|
|
76
|
+
classify_change(tmpl, dest, relative, results)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
results
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def validate!
|
|
85
|
+
raise RosettAi::ProjectError, 'Project .rosett-ai/ directory not found' unless @project_dir.directory?
|
|
86
|
+
|
|
87
|
+
origin_file = @project_dir.join('.template_origin.yml')
|
|
88
|
+
return if origin_file.exist?
|
|
89
|
+
|
|
90
|
+
raise RosettAi::ProjectError,
|
|
91
|
+
'No template origin found. Run rai project apply-template first.'
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_origin
|
|
95
|
+
YAML.safe_load(@project_dir.join('.template_origin.yml').read)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def sync_file(tmpl, dest, relative, results)
|
|
99
|
+
unless dest.exist?
|
|
100
|
+
dest.dirname.mkpath
|
|
101
|
+
FileUtils.cp(tmpl.to_s, dest.to_s)
|
|
102
|
+
results[:updated] << relative.to_s
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return if files_identical?(tmpl, dest)
|
|
107
|
+
|
|
108
|
+
if @force
|
|
109
|
+
FileUtils.cp(tmpl.to_s, dest.to_s)
|
|
110
|
+
results[:updated] << relative.to_s
|
|
111
|
+
else
|
|
112
|
+
results[:conflicts] << relative.to_s
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def classify_change(tmpl, dest, relative, results)
|
|
117
|
+
unless dest.exist?
|
|
118
|
+
results[:updated] << relative.to_s
|
|
119
|
+
return
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
return if files_identical?(tmpl, dest)
|
|
123
|
+
|
|
124
|
+
results[:conflicts] << relative.to_s
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def files_identical?(file_a, file_b)
|
|
128
|
+
Digest::SHA256.file(file_a.to_s).hexdigest == Digest::SHA256.file(file_b.to_s).hexdigest
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def update_origin_timestamp(origin)
|
|
132
|
+
origin['synced_at'] = Time.now.utc.iso8601
|
|
133
|
+
origin['rai_version'] = RosettAi::VERSION
|
|
134
|
+
File.write(@project_dir.join('.template_origin.yml'), YAML.dump(origin))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
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 'fileutils'
|
|
8
|
+
|
|
9
|
+
module RosettAi
|
|
10
|
+
module Project
|
|
11
|
+
# Applies project templates to populate .rosett-ai/ with starter configs.
|
|
12
|
+
#
|
|
13
|
+
# Templates are directories under conf/templates/ containing behaviour
|
|
14
|
+
# and design YAML files. Application never overwrites user-modified
|
|
15
|
+
# files without explicit --force flag.
|
|
16
|
+
#
|
|
17
|
+
# @author hugo
|
|
18
|
+
# @author claude
|
|
19
|
+
class TemplateApplier
|
|
20
|
+
TEMPLATES_DIR = 'conf/templates'
|
|
21
|
+
|
|
22
|
+
# @param project_root [Pathname] root directory of the project
|
|
23
|
+
# @param template_name [String] name of the template to apply
|
|
24
|
+
# @param force [Boolean] overwrite existing files
|
|
25
|
+
def initialize(project_root:, template_name:, force: false)
|
|
26
|
+
@project_root = project_root
|
|
27
|
+
@template_name = template_name
|
|
28
|
+
@force = force
|
|
29
|
+
@project_dir = project_root.join('.rosett-ai')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Applies the template to the project.
|
|
33
|
+
#
|
|
34
|
+
# @return [Hash] result with :applied, :skipped, :errors arrays
|
|
35
|
+
def apply
|
|
36
|
+
validate!
|
|
37
|
+
results = { applied: [], skipped: [], errors: [] }
|
|
38
|
+
|
|
39
|
+
template_files.each do |src_path|
|
|
40
|
+
relative = src_path.relative_path_from(template_dir)
|
|
41
|
+
dest = @project_dir.join(relative)
|
|
42
|
+
apply_file(src_path, dest, results)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
record_template_origin
|
|
46
|
+
results
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# @return [Boolean] true if the template exists
|
|
50
|
+
def template_exists?
|
|
51
|
+
template_dir.directory?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# @return [Array<String>] list of available template names
|
|
55
|
+
def self.available_templates
|
|
56
|
+
dir = RosettAi.root.join(TEMPLATES_DIR)
|
|
57
|
+
return [] unless dir.directory?
|
|
58
|
+
|
|
59
|
+
Dir.children(dir.to_s)
|
|
60
|
+
.select { |name| dir.join(name).directory? }
|
|
61
|
+
.sort
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def validate!
|
|
67
|
+
raise RosettAi::ProjectError, "Template '#{@template_name}' not found" unless template_exists?
|
|
68
|
+
raise RosettAi::ProjectError, 'Project .rosett-ai/ directory not found' unless @project_dir.directory?
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def template_dir
|
|
72
|
+
RosettAi.root.join(TEMPLATES_DIR, @template_name)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def template_files
|
|
76
|
+
Dir.glob(template_dir.join('**', '*').to_s)
|
|
77
|
+
.map { |path| Pathname.new(path) }
|
|
78
|
+
.reject(&:directory?)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def apply_file(src, dest, results)
|
|
82
|
+
dest.dirname.mkpath
|
|
83
|
+
|
|
84
|
+
if dest.exist? && !@force
|
|
85
|
+
results[:skipped] << dest.relative_path_from(@project_root).to_s
|
|
86
|
+
else
|
|
87
|
+
FileUtils.cp(src.to_s, dest.to_s)
|
|
88
|
+
results[:applied] << dest.relative_path_from(@project_root).to_s
|
|
89
|
+
end
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
results[:errors] << "#{dest}: #{e.message}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def record_template_origin
|
|
95
|
+
origin_file = @project_dir.join('.template_origin.yml')
|
|
96
|
+
data = {
|
|
97
|
+
'template' => @template_name,
|
|
98
|
+
'applied_at' => Time.now.utc.iso8601,
|
|
99
|
+
'rai_version' => RosettAi::VERSION
|
|
100
|
+
}
|
|
101
|
+
File.write(origin_file, YAML.dump(data))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
# Detects whether Dir.pwd is inside an rosett-ai-managed project by walking
|
|
8
|
+
# up from the start directory looking for a `.rosett-ai/` marker directory.
|
|
9
|
+
#
|
|
10
|
+
# When found, content commands (design, behaviour, compile) resolve
|
|
11
|
+
# their source paths relative to the project's `.rosett-ai/` directory
|
|
12
|
+
# instead of the Rosett-AI installation root.
|
|
13
|
+
class ProjectContext
|
|
14
|
+
# Directory name that marks the root of an rai project.
|
|
15
|
+
PROJECT_MARKER = '.rosett-ai'
|
|
16
|
+
|
|
17
|
+
# @return [Pathname, nil] absolute path to the project root, or nil if no project detected
|
|
18
|
+
attr_reader :project_root
|
|
19
|
+
|
|
20
|
+
# @param start_dir [String] directory to begin searching from.
|
|
21
|
+
# Defaults to `RAI_ORIGINAL_PWD` env var (set by the `.deb` wrapper
|
|
22
|
+
# to preserve the user's directory), falling back to `Dir.pwd`.
|
|
23
|
+
def initialize(start_dir: ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd))
|
|
24
|
+
@project_root = detect_project_root(start_dir)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [Boolean] true if a `.rosett-ai/` marker was found
|
|
28
|
+
def project?
|
|
29
|
+
!@project_root.nil?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] true if the detected project is the Rosett-AI installation itself
|
|
33
|
+
# or the Rosett-AI source tree (when running the installed .deb from source)
|
|
34
|
+
def rai_internal?
|
|
35
|
+
@project_root == RosettAi.root || nncc_source_tree?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Pathname] the `.rosett-ai/` directory if inside a project, otherwise `RosettAi.root`
|
|
39
|
+
#
|
|
40
|
+
# When inside the rosett-ai source tree, returns the source tree root (not `.rosett-ai/`)
|
|
41
|
+
# so that `conf/` resolves to the real configuration directory containing
|
|
42
|
+
# behaviour and design YAML files, rather than the empty `.rosett-ai/conf/`
|
|
43
|
+
# placeholder created by `rai init --project`.
|
|
44
|
+
def conf_root
|
|
45
|
+
return @project_root if rai_internal?
|
|
46
|
+
|
|
47
|
+
project? ? @project_root.join(PROJECT_MARKER) : RosettAi.root
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @return [Pathname] path to the `conf/` subdirectory for behaviour/design files
|
|
51
|
+
def project_conf_dir
|
|
52
|
+
conf_root.join('conf')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Detects the Rosett-AI source tree by checking for the gem's main entry
|
|
58
|
+
# point and gemspec. This combination is unique to rosett-ai — no other
|
|
59
|
+
# rosett-ai-managed project will have both files at the project root.
|
|
60
|
+
#
|
|
61
|
+
# Handles the case where the installed .deb package (RosettAi.root = /opt/rosett-ai/app)
|
|
62
|
+
# is run from the Rosett-AI source tree (project_root = ~/git/.../rosett-ai).
|
|
63
|
+
# Without this check, rai_internal? returns false and conf_root resolves
|
|
64
|
+
# to the empty .rosett-ai/conf/ placeholder instead of the real conf/ directory.
|
|
65
|
+
#
|
|
66
|
+
# @return [Boolean]
|
|
67
|
+
def nncc_source_tree?
|
|
68
|
+
return false unless @project_root
|
|
69
|
+
|
|
70
|
+
@project_root.join('lib', 'rosett_ai.rb').exist? &&
|
|
71
|
+
@project_root.join('rosett_ai.gemspec').exist?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def detect_project_root(dir)
|
|
75
|
+
path = Pathname.new(dir).expand_path
|
|
76
|
+
path.ascend do |ancestor|
|
|
77
|
+
marker = ancestor.join(PROJECT_MARKER)
|
|
78
|
+
return ancestor if marker.directory?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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 Provenance
|
|
8
|
+
# A single provenance entry recording AI involvement in a commit.
|
|
9
|
+
#
|
|
10
|
+
# Each entry captures the commit SHA, contributor identity, AI tool used,
|
|
11
|
+
# the AI's role, and which files were involved.
|
|
12
|
+
class Entry
|
|
13
|
+
ALLOWED_ROLES = [
|
|
14
|
+
'AI-Generated-By',
|
|
15
|
+
'AI-Co-Author',
|
|
16
|
+
'AI-Assisted-By',
|
|
17
|
+
'AI-Reviewed-By'
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :commit, :contributor, :ai_tool, :ai_role, :files, :timestamp
|
|
21
|
+
|
|
22
|
+
# @param commit [String] git commit SHA
|
|
23
|
+
# @param contributor [String] contributor name and email
|
|
24
|
+
# @param ai_tool [String] AI tool identifier (e.g. "Claude Opus 4.6 (Anthropic)")
|
|
25
|
+
# @param ai_role [String] one of {ALLOWED_ROLES}
|
|
26
|
+
# @param files [Array<Hash>] file-level source references
|
|
27
|
+
# @param timestamp [String, nil] ISO 8601 timestamp (defaults to now)
|
|
28
|
+
# rubocop:disable Metrics/ParameterLists -- provenance entry requires all fields
|
|
29
|
+
def initialize(commit:, contributor:, ai_tool:, ai_role:, files: [], timestamp: nil)
|
|
30
|
+
validate_role!(ai_role)
|
|
31
|
+
|
|
32
|
+
@commit = commit.freeze
|
|
33
|
+
@contributor = contributor.freeze
|
|
34
|
+
@ai_tool = ai_tool.freeze
|
|
35
|
+
@ai_role = ai_role.freeze
|
|
36
|
+
@files = files.freeze
|
|
37
|
+
@timestamp = (timestamp || Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')).freeze
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Metrics/ParameterLists
|
|
40
|
+
|
|
41
|
+
# @return [Hash] serializable representation for YAML output
|
|
42
|
+
def to_h
|
|
43
|
+
{
|
|
44
|
+
'commit' => @commit,
|
|
45
|
+
'timestamp' => @timestamp,
|
|
46
|
+
'contributor' => @contributor,
|
|
47
|
+
'ai_tool' => @ai_tool,
|
|
48
|
+
'ai_role' => @ai_role,
|
|
49
|
+
'files' => @files.map { |file| file.is_a?(Hash) ? file : file.to_h }
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def validate_role!(role)
|
|
56
|
+
return if ALLOWED_ROLES.include?(role)
|
|
57
|
+
|
|
58
|
+
raise ArgumentError,
|
|
59
|
+
"Invalid AI role '#{role}'. Allowed: #{ALLOWED_ROLES.join(', ')}"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
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 Provenance
|
|
8
|
+
# Represents a file-level source reference within a provenance entry.
|
|
9
|
+
#
|
|
10
|
+
# Tracks which files were involved in an AI contribution and
|
|
11
|
+
# what lines or sections were affected.
|
|
12
|
+
class FileSource
|
|
13
|
+
attr_reader :path, :lines, :source_type
|
|
14
|
+
|
|
15
|
+
# @param path [String] relative path from project root
|
|
16
|
+
# @param lines [String, nil] line range (e.g. "1-50") or nil for whole file
|
|
17
|
+
# @param source_type [String] one of Source::ALLOWED_TYPES
|
|
18
|
+
def initialize(path:, source_type:, lines: nil)
|
|
19
|
+
@path = path.freeze
|
|
20
|
+
@lines = lines&.freeze
|
|
21
|
+
@source_type = source_type.freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Hash] serializable representation
|
|
25
|
+
def to_h
|
|
26
|
+
hash = { 'path' => @path, 'source_type' => @source_type }
|
|
27
|
+
hash['lines'] = @lines if @lines
|
|
28
|
+
hash
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
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 Provenance
|
|
8
|
+
# Classifies the source type of an AI contribution.
|
|
9
|
+
#
|
|
10
|
+
# Source types track where AI-generated code drew its knowledge from,
|
|
11
|
+
# enabling license traceability and compliance auditing.
|
|
12
|
+
class Source
|
|
13
|
+
ALLOWED_TYPES = [
|
|
14
|
+
'library_api',
|
|
15
|
+
'project_code',
|
|
16
|
+
'documentation',
|
|
17
|
+
'pattern',
|
|
18
|
+
'external_source'
|
|
19
|
+
].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :type, :reference, :url
|
|
22
|
+
|
|
23
|
+
# @param type [String] one of {ALLOWED_TYPES}
|
|
24
|
+
# @param reference [String] human-readable description of the source
|
|
25
|
+
# @param url [String, nil] URL for external_source type
|
|
26
|
+
# @raise [ArgumentError] if type is not in {ALLOWED_TYPES}
|
|
27
|
+
# @raise [ArgumentError] if external_source has bare domain URL
|
|
28
|
+
def initialize(type:, reference:, url: nil)
|
|
29
|
+
validate_type!(type)
|
|
30
|
+
validate_url!(type, url)
|
|
31
|
+
|
|
32
|
+
@type = type.freeze
|
|
33
|
+
@reference = reference.freeze
|
|
34
|
+
@url = url&.freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# @return [Hash] serializable representation
|
|
38
|
+
def to_h
|
|
39
|
+
hash = { 'type' => @type, 'reference' => @reference }
|
|
40
|
+
hash['url'] = @url if @url
|
|
41
|
+
hash
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def validate_type!(type)
|
|
47
|
+
return if ALLOWED_TYPES.include?(type)
|
|
48
|
+
|
|
49
|
+
raise ArgumentError,
|
|
50
|
+
"Invalid source type '#{type}'. Allowed: #{ALLOWED_TYPES.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_url!(type, url)
|
|
54
|
+
return unless type == 'external_source'
|
|
55
|
+
return if url&.include?('/') && url.count('/') > 2
|
|
56
|
+
|
|
57
|
+
raise ArgumentError,
|
|
58
|
+
'external_source requires a specific URL, not a bare domain'
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
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 Provenance
|
|
11
|
+
# Append-only YAML store for `.ai-provenance.yml`.
|
|
12
|
+
#
|
|
13
|
+
# Manages the provenance file lifecycle: creation, appending entries,
|
|
14
|
+
# reading, querying, and hash chain integrity. Entries are never
|
|
15
|
+
# modified or deleted. Each entry includes a SHA-256 hash of the
|
|
16
|
+
# previous entry to create a tamper-evident chain.
|
|
17
|
+
#
|
|
18
|
+
# @author hugo
|
|
19
|
+
# @author claude
|
|
20
|
+
class Store
|
|
21
|
+
FILENAME = '.ai-provenance.yml'
|
|
22
|
+
MAX_FILE_SIZE = 1_048_576 # 1 MB
|
|
23
|
+
SCHEMA_VERSION = 1
|
|
24
|
+
ZERO_HASH = ('0' * 64).freeze
|
|
25
|
+
|
|
26
|
+
attr_reader :root
|
|
27
|
+
|
|
28
|
+
# @param root [Pathname, String] project root directory
|
|
29
|
+
def initialize(root:)
|
|
30
|
+
@root = Pathname.new(root)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Pathname] full path to the provenance file
|
|
34
|
+
def path
|
|
35
|
+
@root.join(FILENAME)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Boolean] true if the provenance file exists
|
|
39
|
+
def exist?
|
|
40
|
+
path.exist?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Creates a new provenance file with version header.
|
|
44
|
+
#
|
|
45
|
+
# @return [Pathname] path to the created file
|
|
46
|
+
# @raise [RosettAi::ProvenanceError] if file already exists
|
|
47
|
+
def init
|
|
48
|
+
raise RosettAi::ProvenanceError, "Provenance file already exists: #{path}" if exist?
|
|
49
|
+
|
|
50
|
+
data = { 'provenance_version' => SCHEMA_VERSION, 'entries' => [] }
|
|
51
|
+
path.write(YAML.dump(data))
|
|
52
|
+
path
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Appends an entry to the provenance file with hash chain linkage.
|
|
56
|
+
#
|
|
57
|
+
# @param entry [Entry] provenance entry to append
|
|
58
|
+
# @return [Pathname] path to the updated file
|
|
59
|
+
# @raise [RosettAi::ProvenanceError] if provenance file does not exist
|
|
60
|
+
def append(entry)
|
|
61
|
+
raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?
|
|
62
|
+
|
|
63
|
+
warn_file_size if file_over_limit?
|
|
64
|
+
data = read_data
|
|
65
|
+
entry_hash = chain_hash(data['entries'])
|
|
66
|
+
serialized = entry.to_h.merge('chain_hash' => entry_hash)
|
|
67
|
+
data['entries'] << serialized
|
|
68
|
+
path.write(YAML.dump(data))
|
|
69
|
+
path
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reads and parses the provenance file.
|
|
73
|
+
#
|
|
74
|
+
# @return [Hash] parsed provenance data
|
|
75
|
+
# @raise [RosettAi::ProvenanceError] if provenance file is missing
|
|
76
|
+
def read
|
|
77
|
+
raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?
|
|
78
|
+
|
|
79
|
+
read_data
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Returns all entries for a specific commit.
|
|
83
|
+
#
|
|
84
|
+
# @param commit_sha [String] full or partial commit SHA
|
|
85
|
+
# @return [Array<Hash>] matching entries
|
|
86
|
+
def entries_for_commit(commit_sha)
|
|
87
|
+
return [] unless exist?
|
|
88
|
+
|
|
89
|
+
read['entries'].select { |entry| entry['commit']&.start_with?(commit_sha) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns all entries that reference a specific file.
|
|
93
|
+
#
|
|
94
|
+
# @param file_path [String] relative file path
|
|
95
|
+
# @return [Array<Hash>] matching entries
|
|
96
|
+
def entries_for_file(file_path)
|
|
97
|
+
return [] unless exist?
|
|
98
|
+
|
|
99
|
+
read['entries'].select do |entry|
|
|
100
|
+
Array(entry['files']).any? { |file| file['path'] == file_path }
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# @return [Integer] number of entries in the provenance file
|
|
105
|
+
def entry_count
|
|
106
|
+
return 0 unless exist?
|
|
107
|
+
|
|
108
|
+
read['entries'].size
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Verifies the integrity of the hash chain.
|
|
112
|
+
#
|
|
113
|
+
# @return [Array<String>] list of integrity violations (empty if valid)
|
|
114
|
+
def verify_chain
|
|
115
|
+
return [] unless exist?
|
|
116
|
+
|
|
117
|
+
entries = read['entries']
|
|
118
|
+
violations = []
|
|
119
|
+
entries.each_with_index do |entry, index|
|
|
120
|
+
expected = chain_hash(entries[0...index])
|
|
121
|
+
actual = entry['chain_hash']
|
|
122
|
+
next if actual == expected
|
|
123
|
+
|
|
124
|
+
violations << "Entry ##{index + 1} (#{entry['commit']}): " \
|
|
125
|
+
"expected #{expected[0, 16]}..., got #{(actual || 'nil')[0, 16]}..."
|
|
126
|
+
end
|
|
127
|
+
violations
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
private
|
|
131
|
+
|
|
132
|
+
def read_data
|
|
133
|
+
YAML.safe_load(path.read, permitted_classes: [Date, Time])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def chain_hash(entries)
|
|
137
|
+
return ZERO_HASH if entries.empty?
|
|
138
|
+
|
|
139
|
+
previous = entries.last.reject { |k| k == 'chain_hash' }
|
|
140
|
+
Digest::SHA256.hexdigest(YAML.dump(previous))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def file_over_limit?
|
|
144
|
+
exist? && path.size > MAX_FILE_SIZE
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def warn_file_size
|
|
148
|
+
warn 'Provenance file exceeds 1 MB — run `rai provenance archive` ' \
|
|
149
|
+
'to move older entries to a dated archive'
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
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 Provenance
|
|
8
|
+
# MCP-facing facade for provenance operations.
|
|
9
|
+
#
|
|
10
|
+
# Delegates to {Store} for the actual provenance logic, providing
|
|
11
|
+
# the simplified interface expected by {Mcp::Tools::ProvenanceTool}.
|
|
12
|
+
#
|
|
13
|
+
# @author hugo
|
|
14
|
+
# @author claude
|
|
15
|
+
class Tracker
|
|
16
|
+
# @param root [Pathname, String] project root (defaults to pwd)
|
|
17
|
+
def initialize(root: Dir.pwd)
|
|
18
|
+
@store = Store.new(root: root)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Validates all provenance entries and hash chain integrity.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] with :errors key
|
|
24
|
+
def validate
|
|
25
|
+
errors = []
|
|
26
|
+
errors << 'Provenance file not found' unless @store.exist?
|
|
27
|
+
errors.concat(@store.verify_chain) if @store.exist?
|
|
28
|
+
{ errors: errors }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Finds provenance entries for a specific commit.
|
|
32
|
+
#
|
|
33
|
+
# @param commit [String] commit SHA
|
|
34
|
+
# @return [Hash, nil] matching entry
|
|
35
|
+
def find_by_commit(commit)
|
|
36
|
+
entries = @store.entries_for_commit(commit)
|
|
37
|
+
entries.first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Finds provenance entries referencing a file.
|
|
41
|
+
#
|
|
42
|
+
# @param file [String] file path
|
|
43
|
+
# @return [Array<Hash>] matching entries
|
|
44
|
+
def find_by_file(file)
|
|
45
|
+
@store.entries_for_file(file)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns provenance log, optionally filtered by role.
|
|
49
|
+
#
|
|
50
|
+
# @param role [String, nil] AI role filter
|
|
51
|
+
# @return [Array<Hash>] provenance entries
|
|
52
|
+
def log(role: nil)
|
|
53
|
+
return [] unless @store.exist?
|
|
54
|
+
|
|
55
|
+
entries = @store.read['entries'] || []
|
|
56
|
+
return entries unless role
|
|
57
|
+
|
|
58
|
+
entries.select { |e| e['role'] == role }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|