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.
Files changed (527) hide show
  1. checksums.yaml +7 -0
  2. data/.ai-provenance.yml +119 -0
  3. data/.debride_whitelist +186 -0
  4. data/.fasterer.yml +29 -0
  5. data/.mdl_style.rb +10 -0
  6. data/.mdlrc +3 -0
  7. data/.mutant.yml +49 -0
  8. data/.namespace-allowlist +42 -0
  9. data/.reek.yml +1040 -0
  10. data/.rosett-ai/config.yml +3 -0
  11. data/.rspec +5 -0
  12. data/.rubocop.yml +380 -0
  13. data/.ruby-version +1 -0
  14. data/.yamllint +51 -0
  15. data/.yardopts +12 -0
  16. data/AI-DISCLOSURE.md +48 -0
  17. data/CHANGELOG.md +519 -0
  18. data/CLAUDE.md +141 -0
  19. data/CONTRIBUTING.md +734 -0
  20. data/INSTALL.md +154 -0
  21. data/LICENSE +674 -0
  22. data/LICENSE.md +675 -0
  23. data/QUICKSTART.md +73 -0
  24. data/README.md +366 -0
  25. data/Rakefile +200 -0
  26. data/SECURITY.md +114 -0
  27. data/bin/rai +1 -0
  28. data/cliff.toml +52 -0
  29. data/conf/adopt_redactions.yml +8 -0
  30. data/conf/behaviour/.gitkeep +0 -0
  31. data/conf/compliance/cra_rules.yml +25 -0
  32. data/conf/compliance/license_rules.yml +20 -0
  33. data/conf/design/aaif_alignment.yml +181 -0
  34. data/conf/design/ab_testing.yml +172 -0
  35. data/conf/design/accessibility.yml +84 -0
  36. data/conf/design/ai_authorship.yml +210 -0
  37. data/conf/design/ai_provenance.yml +224 -0
  38. data/conf/design/ai_tool_configuration.yml +207 -0
  39. data/conf/design/architecture.yml +139 -0
  40. data/conf/design/autocompletion.yml +115 -0
  41. data/conf/design/backward_compatibility.yml +112 -0
  42. data/conf/design/behaviour_composition.yml +246 -0
  43. data/conf/design/build_rake_extraction.yml +57 -0
  44. data/conf/design/ci_pipeline.yml +100 -0
  45. data/conf/design/claude_code_configuration.yml +157 -0
  46. data/conf/design/compiler.yml +128 -0
  47. data/conf/design/comply.yml +153 -0
  48. data/conf/design/content_packs.yml +84 -0
  49. data/conf/design/desktop_integration.yml +289 -0
  50. data/conf/design/distribution.yml +216 -0
  51. data/conf/design/doctor.yml +184 -0
  52. data/conf/design/documentation.yml +152 -0
  53. data/conf/design/engine_architecture.yml +257 -0
  54. data/conf/design/error_handling.yml +103 -0
  55. data/conf/design/feature_flags.yml +142 -0
  56. data/conf/design/git_hooks.yml +165 -0
  57. data/conf/design/gui_plugins.yml +475 -0
  58. data/conf/design/i18n.yml +84 -0
  59. data/conf/design/integration_testing.yml +56 -0
  60. data/conf/design/licensing_system.yml +88 -0
  61. data/conf/design/lifecycle_management.yml +208 -0
  62. data/conf/design/mcp_integration.yml +207 -0
  63. data/conf/design/mcp_settings.yml +126 -0
  64. data/conf/design/migration.yml +56 -0
  65. data/conf/design/monitoring_observability.yml +194 -0
  66. data/conf/design/namespace_cleanup.yml +145 -0
  67. data/conf/design/plugin_test_segregation.yml +145 -0
  68. data/conf/design/policy_management.yml +229 -0
  69. data/conf/design/project_management.yml +183 -0
  70. data/conf/design/rai_mcp_asset_discovery.yml +164 -0
  71. data/conf/design/rai_mcp_server.yml +605 -0
  72. data/conf/design/release_management.yml +117 -0
  73. data/conf/design/retrofit.yml +199 -0
  74. data/conf/design/retrospective_analyzer.yml +79 -0
  75. data/conf/design/scope_hierarchy.yml +352 -0
  76. data/conf/design/security.yml +115 -0
  77. data/conf/design/session_retrospective.yml +85 -0
  78. data/conf/design/smart_ui_feedback.yml +89 -0
  79. data/conf/design/structured_logging.yml +148 -0
  80. data/conf/design/styles.yml +123 -0
  81. data/conf/design/test_peer_review.yml +89 -0
  82. data/conf/design/testing.yml +136 -0
  83. data/conf/design/threat_model.yml +108 -0
  84. data/conf/design/ui_framework.yml +111 -0
  85. data/conf/design/usage_optimization.yml +122 -0
  86. data/conf/design/version_management.yml +60 -0
  87. data/conf/design/workflow.yml +227 -0
  88. data/conf/mcp/server_defaults.yml +42 -0
  89. data/conf/mcp/trust.yml +21 -0
  90. data/conf/packaging/core.yml +12 -0
  91. data/conf/packaging/gtk4.yml +11 -0
  92. data/conf/packaging/qt6.yml +11 -0
  93. data/conf/policy/default_deny_list.yml +197 -0
  94. data/conf/review/cli-command-audit.yml +857 -0
  95. data/conf/review/design-docs.yml +1064 -0
  96. data/conf/review/design-questionnaire.yml +153 -0
  97. data/conf/review/questionnaire.yml +146 -0
  98. data/conf/review/rosett-ai-core.yml +2919 -0
  99. data/conf/schemas/ai_config_schema.json +73 -0
  100. data/conf/schemas/behaviour_schema.json +132 -0
  101. data/conf/schemas/compliance_rule_schema.json +63 -0
  102. data/conf/schemas/content_pack_manifest_schema.json +51 -0
  103. data/conf/schemas/design_schema.json +210 -0
  104. data/conf/schemas/engine_manifest_schema.json +144 -0
  105. data/conf/schemas/lockfile_schema.json +74 -0
  106. data/conf/schemas/mcp_server_schema.json +48 -0
  107. data/conf/schemas/packaging_schema.json +70 -0
  108. data/conf/schemas/policy_schema.json +85 -0
  109. data/conf/schemas/provenance_schema.json +84 -0
  110. data/conf/schemas/rai_config_schema.json +56 -0
  111. data/conf/schemas/rai_project_schema.json +20 -0
  112. data/conf/schemas/scope_hierarchy_schema.json +49 -0
  113. data/conf/schemas/target_schema.json +67 -0
  114. data/conf/schemas/tooling_schema.json +65 -0
  115. data/conf/schemas/workflow_schema.json +112 -0
  116. data/conf/targets/agents_md.yml +17 -0
  117. data/conf/targets/claude.yml +12 -0
  118. data/conf/tooling/tools.yml +58 -0
  119. data/dist/rosett-ai-mcp.service +48 -0
  120. data/dist/rosett-ai-mcp.yml.default +45 -0
  121. data/doc/AAIF_POSITIONING.md +58 -0
  122. data/doc/ADOPT.md +224 -0
  123. data/doc/AI_PROVENANCE.md +139 -0
  124. data/doc/ARCHITECTURE.md +920 -0
  125. data/doc/BEHAVIOUR.md +409 -0
  126. data/doc/BUILD.md +138 -0
  127. data/doc/CI_CD_RECIPES.md +171 -0
  128. data/doc/CLAUDE_SESSIONS_MOVED.md +16 -0
  129. data/doc/COMMAND_ANALYSIS.md +229 -0
  130. data/doc/CONFIGURATION.md +281 -0
  131. data/doc/DESIGN_AUDIT.md +235 -0
  132. data/doc/DESIGN_PEER_REVIEW.md +771 -0
  133. data/doc/DESKTOP.md +447 -0
  134. data/doc/ENGINES.md +567 -0
  135. data/doc/ENGINE_DEVELOPMENT_GUIDE.md +417 -0
  136. data/doc/FEATURE_AUDIT.md +218 -0
  137. data/doc/IMPLEMENTATION_PLAN.md +669 -0
  138. data/doc/INCIDENT_REPORT_2026-02-02.md +251 -0
  139. data/doc/MIGRATION_GUIDE.md +88 -0
  140. data/doc/PACKAGING.md +232 -0
  141. data/doc/PROJECT_DASHBOARD.md +153 -0
  142. data/doc/PULP_DEPLOYMENT.md +164 -0
  143. data/doc/QUALITY_FIX_SUMMARY.md +110 -0
  144. data/doc/QUICK_START.md +162 -0
  145. data/doc/REEK_CONFIGURATION.md +166 -0
  146. data/doc/REFERENCE.md +253 -0
  147. data/doc/REFERENCES.md +324 -0
  148. data/doc/SECURITY_REVIEW_CHECKLIST.md +72 -0
  149. data/doc/SESSION_2026-02-28_GTK4_HARDENING.md +359 -0
  150. data/doc/SETUP.md +202 -0
  151. data/doc/TEST_PEER_REVIEW.md +152 -0
  152. data/doc/THREAT_MODEL.md +230 -0
  153. data/doc/USAGE.md +545 -0
  154. data/doc/USER_MANUAL.md +585 -0
  155. data/doc/ai_test_review_checklist.md +110 -0
  156. data/doc/changes/2026-02-18-packaging-fpm.md +155 -0
  157. data/doc/changes/2026-02-19-testing-infrastructure.md +221 -0
  158. data/doc/changes/2026-02-20-security-implementation.md +281 -0
  159. data/doc/changes/2026-02-20-styles-implementation.md +220 -0
  160. data/doc/changes/2026-02-21-architecture-completion.md +95 -0
  161. data/doc/changes/2026-02-21-architecture-ui-layer.md +253 -0
  162. data/doc/changes/2026-02-21-cc-config-implementation.md +108 -0
  163. data/doc/changes/2026-02-21-ci-pipeline-implementation.md +214 -0
  164. data/doc/changes/2026-02-21-compiler-multi-target-pipeline.md +241 -0
  165. data/doc/changes/2026-02-21-config-design-show-commands.md +61 -0
  166. data/doc/changes/2026-02-21-design-implementation-overview.md +455 -0
  167. data/doc/changes/2026-02-21-lifecycle-management.md +196 -0
  168. data/doc/changes/2026-02-21-path-resolver.md +128 -0
  169. data/doc/changes/2026-02-24-ci-tmpdir-mutant-fetch.md +45 -0
  170. data/doc/changes/2026-03-01-ci-bundler-strategy.md +120 -0
  171. data/doc/changes/2026-03-20-security-hardening-phase2.md +163 -0
  172. data/doc/context/SESSION-HANDOFF.md +69 -0
  173. data/doc/context/ai-engine-usage-trends-2026.md +80 -0
  174. data/doc/context/plan-pluggable-engines.md +590 -0
  175. data/doc/decisions/001-flog-deferred.md +32 -0
  176. data/doc/decisions/002-path-resolution-strategy.md +158 -0
  177. data/doc/decisions/003-ui-adapter-selection.md +193 -0
  178. data/doc/decisions/004-design-document-validation.md +179 -0
  179. data/doc/decisions/005-package-splitting-strategy.md +200 -0
  180. data/doc/decisions/006-multi-engine-architecture.md +147 -0
  181. data/doc/decisions/007-engine-agnostic-pivot.md +219 -0
  182. data/doc/decisions/008-ci-bundler-strategy.md +129 -0
  183. data/doc/decisions/009-core-only-v1-release.md +60 -0
  184. data/doc/decisions/010-engine-debian-packaging.md +66 -0
  185. data/doc/decisions/011-context-aware-cli.md +71 -0
  186. data/doc/dependency_decisions.yml +247 -0
  187. data/doc/issues/001-wrapper-missing-environment-variables.md +197 -0
  188. data/doc/issues/002-embedded-ruby-wrong-prefix.md +217 -0
  189. data/doc/issues/003-smoke-test-false-positive.md +127 -0
  190. data/doc/issues/004-market-research-design-updates.md +109 -0
  191. data/doc/issues/005-compile-scope-coexistence.md +161 -0
  192. data/doc/locales/.gitkeep +0 -0
  193. data/doc/man/rai.1.ronn +505 -0
  194. data/doc/operations/packaging.md +133 -0
  195. data/doc/operations/rosett-ai-release.md +65 -0
  196. data/doc/reference/error-catalog.md +107 -0
  197. data/doc/reference/rosett-ai-technical-reference.pdf +0 -0
  198. data/doc/reference/src/Pictures/cover.jpg +0 -0
  199. data/doc/reference/src/Pictures/head1.jpg +0 -0
  200. data/doc/reference/src/Pictures/head2.jpg +0 -0
  201. data/doc/reference/src/Pictures/head3.jpg +0 -0
  202. data/doc/reference/src/Pictures/head4.jpg +0 -0
  203. data/doc/reference/src/Pictures/head5.jpg +0 -0
  204. data/doc/reference/src/Pictures/head6.jpg +0 -0
  205. data/doc/reference/src/Pictures/head7.jpg +0 -0
  206. data/doc/reference/src/Pictures/head8.jpg +0 -0
  207. data/doc/reference/src/StyleInd.ist +4 -0
  208. data/doc/reference/src/bibliography.bib +79 -0
  209. data/doc/reference/src/main.tex +1288 -0
  210. data/doc/reference/src/structure.tex +303 -0
  211. data/doc/rosett-ai-bookmarks.html +301 -0
  212. data/kitchen.yml +46 -0
  213. data/lib/rosett_ai/adopter/executor_resolver.rb +77 -0
  214. data/lib/rosett_ai/adopter/local_analysis_collector.rb +154 -0
  215. data/lib/rosett_ai/adopter/rule_adopter.rb +254 -0
  216. data/lib/rosett_ai/ai_config/config_compiler.rb +111 -0
  217. data/lib/rosett_ai/ai_config/context_window.rb +55 -0
  218. data/lib/rosett_ai/ai_config/cost_controls.rb +44 -0
  219. data/lib/rosett_ai/ai_config/fallback_chain.rb +64 -0
  220. data/lib/rosett_ai/ai_config/model_router.rb +121 -0
  221. data/lib/rosett_ai/ai_config/validator.rb +45 -0
  222. data/lib/rosett_ai/authorship/attribution_compiler.rb +99 -0
  223. data/lib/rosett_ai/authorship/disclosure_policy.rb +81 -0
  224. data/lib/rosett_ai/authorship/review_validator.rb +39 -0
  225. data/lib/rosett_ai/authorship/trailer_generator.rb +88 -0
  226. data/lib/rosett_ai/backup/compressor.rb +180 -0
  227. data/lib/rosett_ai/backup/destination.rb +91 -0
  228. data/lib/rosett_ai/behaviour/manager.rb +156 -0
  229. data/lib/rosett_ai/compiler/backend.rb +86 -0
  230. data/lib/rosett_ai/compiler/backends/agents_md_backend.rb +80 -0
  231. data/lib/rosett_ai/compiler/backends/claude_backend.rb +88 -0
  232. data/lib/rosett_ai/compiler/backends/generic_backend.rb +15 -0
  233. data/lib/rosett_ai/compiler/behaviour_compiler.rb +40 -0
  234. data/lib/rosett_ai/compiler/capability_checker.rb +104 -0
  235. data/lib/rosett_ai/compiler/compilation_pipeline.rb +361 -0
  236. data/lib/rosett_ai/compiler/compiled_output.rb +39 -0
  237. data/lib/rosett_ai/compiler/locale_compiler.rb +250 -0
  238. data/lib/rosett_ai/compiler/target_profile.rb +112 -0
  239. data/lib/rosett_ai/completion/generator.rb +101 -0
  240. data/lib/rosett_ai/completion/shells/bash_generator.rb +126 -0
  241. data/lib/rosett_ai/completion/shells/fish_generator.rb +78 -0
  242. data/lib/rosett_ai/completion/shells/zsh_generator.rb +126 -0
  243. data/lib/rosett_ai/comply/checkers/cra_checker.rb +102 -0
  244. data/lib/rosett_ai/comply/checkers/license_checker.rb +85 -0
  245. data/lib/rosett_ai/comply/checkers/spdx_header_checker.rb +98 -0
  246. data/lib/rosett_ai/comply/reporter.rb +113 -0
  247. data/lib/rosett_ai/comply/runner.rb +50 -0
  248. data/lib/rosett_ai/composition/circular_dependency_detector.rb +56 -0
  249. data/lib/rosett_ai/composition/composer.rb +158 -0
  250. data/lib/rosett_ai/composition/composition_result.rb +64 -0
  251. data/lib/rosett_ai/composition/conflict_detector.rb +53 -0
  252. data/lib/rosett_ai/composition/lockfile.rb +103 -0
  253. data/lib/rosett_ai/composition/merge_strategy.rb +131 -0
  254. data/lib/rosett_ai/composition/priority_sorter.rb +29 -0
  255. data/lib/rosett_ai/composition/scope_resolver.rb +55 -0
  256. data/lib/rosett_ai/config/compile_result.rb +37 -0
  257. data/lib/rosett_ai/config/compiler.rb +13 -0
  258. data/lib/rosett_ai/config/domain_transformer.rb +13 -0
  259. data/lib/rosett_ai/config/key_map.rb +13 -0
  260. data/lib/rosett_ai/config/masking_secret_resolver.rb +40 -0
  261. data/lib/rosett_ai/config/scope_router.rb +13 -0
  262. data/lib/rosett_ai/config/secret_resolver.rb +125 -0
  263. data/lib/rosett_ai/configuration.rb +119 -0
  264. data/lib/rosett_ai/content/content_client.rb +60 -0
  265. data/lib/rosett_ai/content/pack_installer.rb +117 -0
  266. data/lib/rosett_ai/content/pack_manifest.rb +50 -0
  267. data/lib/rosett_ai/content/pack_registry.rb +68 -0
  268. data/lib/rosett_ai/content_packs/manager.rb +50 -0
  269. data/lib/rosett_ai/dbus/compositor_detector.rb +77 -0
  270. data/lib/rosett_ai/dbus/focus_adapters/base.rb +59 -0
  271. data/lib/rosett_ai/dbus/focus_adapters/gnome_adapter.rb +172 -0
  272. data/lib/rosett_ai/dbus/focus_adapters/hyprland_adapter.rb +77 -0
  273. data/lib/rosett_ai/dbus/focus_adapters/i3_adapter.rb +65 -0
  274. data/lib/rosett_ai/dbus/focus_adapters/kwin_adapter.rb +103 -0
  275. data/lib/rosett_ai/dbus/focus_adapters/x11_adapter.rb +105 -0
  276. data/lib/rosett_ai/dbus/focus_monitor_interface.rb +103 -0
  277. data/lib/rosett_ai/dbus/manager_interface.rb +213 -0
  278. data/lib/rosett_ai/dbus/plugin_manager_interface.rb +169 -0
  279. data/lib/rosett_ai/dbus/rate_limiter.rb +89 -0
  280. data/lib/rosett_ai/dbus/service.rb +121 -0
  281. data/lib/rosett_ai/dbus/status_notifier_interface.rb +79 -0
  282. data/lib/rosett_ai/deprecation.rb +79 -0
  283. data/lib/rosett_ai/desktop/dbus_client.rb +259 -0
  284. data/lib/rosett_ai/desktop/gtk4_app.rb +371 -0
  285. data/lib/rosett_ai/desktop/gtk4_preferences.rb +331 -0
  286. data/lib/rosett_ai/desktop/gui_logger.rb +236 -0
  287. data/lib/rosett_ai/doctor/check.rb +92 -0
  288. data/lib/rosett_ai/doctor/checks/cache_health_check.rb +50 -0
  289. data/lib/rosett_ai/doctor/checks/dbus_availability_check.rb +39 -0
  290. data/lib/rosett_ai/doctor/checks/engine_detection_check.rb +46 -0
  291. data/lib/rosett_ai/doctor/checks/file_permission_check.rb +44 -0
  292. data/lib/rosett_ai/doctor/checks/gem_dependency_check.rb +55 -0
  293. data/lib/rosett_ai/doctor/checks/ruby_version_check.rb +50 -0
  294. data/lib/rosett_ai/doctor/checks/stale_config_nncc_check.rb +57 -0
  295. data/lib/rosett_ai/doctor/checks/stale_home_nncc_check.rb +59 -0
  296. data/lib/rosett_ai/doctor.rb +81 -0
  297. data/lib/rosett_ai/documentation/reference_compiler.rb +122 -0
  298. data/lib/rosett_ai/documentation/translator.rb +62 -0
  299. data/lib/rosett_ai/engines/base_config_compiler.rb +203 -0
  300. data/lib/rosett_ai/engines/detector.rb +63 -0
  301. data/lib/rosett_ai/engines/registry.rb +50 -0
  302. data/lib/rosett_ai/error_handler.rb +139 -0
  303. data/lib/rosett_ai/exit_codes.rb +76 -0
  304. data/lib/rosett_ai/feature_flags.rb +102 -0
  305. data/lib/rosett_ai/formatting.rb +33 -0
  306. data/lib/rosett_ai/gem_consistency_checker.rb +199 -0
  307. data/lib/rosett_ai/git_hooks/chain_detector.rb +86 -0
  308. data/lib/rosett_ai/git_hooks/installer.rb +175 -0
  309. data/lib/rosett_ai/git_hooks/script_generator.rb +125 -0
  310. data/lib/rosett_ai/gitlab/validators/supplementary_gitlab_ci_yaml_validator.rb +79 -0
  311. data/lib/rosett_ai/i18n/locale_resolver.rb +46 -0
  312. data/lib/rosett_ai/i18n/utf8_checker.rb +32 -0
  313. data/lib/rosett_ai/init/config_file_writer.rb +24 -0
  314. data/lib/rosett_ai/init/directory_builder.rb +38 -0
  315. data/lib/rosett_ai/init/file_copier.rb +95 -0
  316. data/lib/rosett_ai/init/global_initializer.rb +28 -0
  317. data/lib/rosett_ai/init/local_initializer.rb +27 -0
  318. data/lib/rosett_ai/init/mcp_registrar.rb +109 -0
  319. data/lib/rosett_ai/init/project_initializer.rb +38 -0
  320. data/lib/rosett_ai/licensing/license_key.rb +139 -0
  321. data/lib/rosett_ai/licensing/license_store.rb +64 -0
  322. data/lib/rosett_ai/licensing/license_validator.rb +60 -0
  323. data/lib/rosett_ai/licensing/tier.rb +42 -0
  324. data/lib/rosett_ai/mcp/admin/auditor.rb +88 -0
  325. data/lib/rosett_ai/mcp/admin/health_checker.rb +81 -0
  326. data/lib/rosett_ai/mcp/admin/registry.rb +100 -0
  327. data/lib/rosett_ai/mcp/admin/schema_validator.rb +63 -0
  328. data/lib/rosett_ai/mcp/enforcement/.gitkeep +0 -0
  329. data/lib/rosett_ai/mcp/enforcement/hook_generator.rb +197 -0
  330. data/lib/rosett_ai/mcp/enforcement/validator.rb +215 -0
  331. data/lib/rosett_ai/mcp/governance.rb +160 -0
  332. data/lib/rosett_ai/mcp/http_security_config.rb +158 -0
  333. data/lib/rosett_ai/mcp/instructions.rb +266 -0
  334. data/lib/rosett_ai/mcp/key_hasher.rb +66 -0
  335. data/lib/rosett_ai/mcp/keyfile.rb +221 -0
  336. data/lib/rosett_ai/mcp/middleware/authentication.rb +146 -0
  337. data/lib/rosett_ai/mcp/middleware/content_type.rb +56 -0
  338. data/lib/rosett_ai/mcp/middleware/cors.rb +83 -0
  339. data/lib/rosett_ai/mcp/middleware/origin_validation.rb +73 -0
  340. data/lib/rosett_ai/mcp/middleware/rate_limit.rb +106 -0
  341. data/lib/rosett_ai/mcp/middleware/request_size.rb +51 -0
  342. data/lib/rosett_ai/mcp/plugins.rb +143 -0
  343. data/lib/rosett_ai/mcp/prompts/compilation_prompt.rb +40 -0
  344. data/lib/rosett_ai/mcp/prompts/compliance_prompt.rb +41 -0
  345. data/lib/rosett_ai/mcp/prompts/diagnostics_prompt.rb +41 -0
  346. data/lib/rosett_ai/mcp/prompts/validation_prompt.rb +41 -0
  347. data/lib/rosett_ai/mcp/resources/behaviour_resource.rb +127 -0
  348. data/lib/rosett_ai/mcp/resources/config_resource.rb +72 -0
  349. data/lib/rosett_ai/mcp/resources/design_resource.rb +58 -0
  350. data/lib/rosett_ai/mcp/resources/hooks_resource.rb +74 -0
  351. data/lib/rosett_ai/mcp/resources/provenance_resource.rb +51 -0
  352. data/lib/rosett_ai/mcp/resources/rules_resource.rb +60 -0
  353. data/lib/rosett_ai/mcp/resources/schema_resource.rb +72 -0
  354. data/lib/rosett_ai/mcp/response_helper.rb +46 -0
  355. data/lib/rosett_ai/mcp/security_logger.rb +60 -0
  356. data/lib/rosett_ai/mcp/server.rb +212 -0
  357. data/lib/rosett_ai/mcp/settings/server_installer.rb +112 -0
  358. data/lib/rosett_ai/mcp/settings/trust_manager.rb +142 -0
  359. data/lib/rosett_ai/mcp/tools/adopt_tool.rb +70 -0
  360. data/lib/rosett_ai/mcp/tools/backup_tool.rb +64 -0
  361. data/lib/rosett_ai/mcp/tools/behaviour_display_tool.rb +72 -0
  362. data/lib/rosett_ai/mcp/tools/behaviour_list_tool.rb +56 -0
  363. data/lib/rosett_ai/mcp/tools/behaviour_manage_tool.rb +114 -0
  364. data/lib/rosett_ai/mcp/tools/behaviour_show_tool.rb +62 -0
  365. data/lib/rosett_ai/mcp/tools/compile_status_tool.rb +122 -0
  366. data/lib/rosett_ai/mcp/tools/compile_tool.rb +191 -0
  367. data/lib/rosett_ai/mcp/tools/comply_tool.rb +79 -0
  368. data/lib/rosett_ai/mcp/tools/config_compile_tool.rb +71 -0
  369. data/lib/rosett_ai/mcp/tools/config_status_tool.rb +79 -0
  370. data/lib/rosett_ai/mcp/tools/content_tool.rb +78 -0
  371. data/lib/rosett_ai/mcp/tools/context_query_tool.rb +156 -0
  372. data/lib/rosett_ai/mcp/tools/design_list_tool.rb +57 -0
  373. data/lib/rosett_ai/mcp/tools/design_show_tool.rb +69 -0
  374. data/lib/rosett_ai/mcp/tools/doctor_tool.rb +62 -0
  375. data/lib/rosett_ai/mcp/tools/documentation_status_tool.rb +45 -0
  376. data/lib/rosett_ai/mcp/tools/engines_tool.rb +84 -0
  377. data/lib/rosett_ai/mcp/tools/hook_install_tool.rb +190 -0
  378. data/lib/rosett_ai/mcp/tools/hook_preview_tool.rb +173 -0
  379. data/lib/rosett_ai/mcp/tools/hooks_status_tool.rb +84 -0
  380. data/lib/rosett_ai/mcp/tools/init_tool.rb +87 -0
  381. data/lib/rosett_ai/mcp/tools/license_status_tool.rb +44 -0
  382. data/lib/rosett_ai/mcp/tools/project_tool.rb +117 -0
  383. data/lib/rosett_ai/mcp/tools/provenance_tool.rb +97 -0
  384. data/lib/rosett_ai/mcp/tools/provenance_write_tool.rb +40 -0
  385. data/lib/rosett_ai/mcp/tools/retrofit_tool.rb +81 -0
  386. data/lib/rosett_ai/mcp/tools/rule_search_tool.rb +163 -0
  387. data/lib/rosett_ai/mcp/tools/schema_get_tool.rb +94 -0
  388. data/lib/rosett_ai/mcp/tools/tooling_tool.rb +86 -0
  389. data/lib/rosett_ai/mcp/tools/validate_tool.rb +105 -0
  390. data/lib/rosett_ai/mcp/tools/workflow_execute_tool.rb +74 -0
  391. data/lib/rosett_ai/mcp/tools/workflow_tool.rb +78 -0
  392. data/lib/rosett_ai/migration/detector.rb +117 -0
  393. data/lib/rosett_ai/migration/nncc_config_migrator.rb +94 -0
  394. data/lib/rosett_ai/migration/nncc_project_migrator.rb +90 -0
  395. data/lib/rosett_ai/migration/xdg_migrator.rb +123 -0
  396. data/lib/rosett_ai/package_manager/apt.rb +108 -0
  397. data/lib/rosett_ai/package_manager/base.rb +68 -0
  398. data/lib/rosett_ai/package_manager/gem_backend.rb +90 -0
  399. data/lib/rosett_ai/packaging/variant_config.rb +92 -0
  400. data/lib/rosett_ai/path_resolver.rb +115 -0
  401. data/lib/rosett_ai/plugins/contract.rb +43 -0
  402. data/lib/rosett_ai/plugins/engine_contract.rb +60 -0
  403. data/lib/rosett_ai/plugins/gui_contract.rb +74 -0
  404. data/lib/rosett_ai/plugins/mcp_contract.rb +48 -0
  405. data/lib/rosett_ai/plugins/registry.rb +150 -0
  406. data/lib/rosett_ai/policy/auditor.rb +41 -0
  407. data/lib/rosett_ai/policy/deny_list.rb +71 -0
  408. data/lib/rosett_ai/policy/opt_out_scanner.rb +37 -0
  409. data/lib/rosett_ai/policy/policy_compiler.rb +84 -0
  410. data/lib/rosett_ai/policy/protected_files.rb +47 -0
  411. data/lib/rosett_ai/policy/tier_hierarchy.rb +48 -0
  412. data/lib/rosett_ai/policy/validator.rb +35 -0
  413. data/lib/rosett_ai/profiler.rb +79 -0
  414. data/lib/rosett_ai/project/drift_detector.rb +126 -0
  415. data/lib/rosett_ai/project/manager.rb +115 -0
  416. data/lib/rosett_ai/project/sync_manager.rb +138 -0
  417. data/lib/rosett_ai/project/template_applier.rb +105 -0
  418. data/lib/rosett_ai/project_context.rb +82 -0
  419. data/lib/rosett_ai/provenance/entry.rb +63 -0
  420. data/lib/rosett_ai/provenance/file_source.rb +32 -0
  421. data/lib/rosett_ai/provenance/source.rb +62 -0
  422. data/lib/rosett_ai/provenance/store.rb +153 -0
  423. data/lib/rosett_ai/provenance/tracker.rb +62 -0
  424. data/lib/rosett_ai/provenance/trailer_generator.rb +43 -0
  425. data/lib/rosett_ai/provenance/validator.rb +45 -0
  426. data/lib/rosett_ai/quorum/collector.rb +59 -0
  427. data/lib/rosett_ai/quorum/comparator.rb +81 -0
  428. data/lib/rosett_ai/quorum/dispatcher.rb +57 -0
  429. data/lib/rosett_ai/quorum/strategies/adopt.rb +56 -0
  430. data/lib/rosett_ai/rai_config.rb +107 -0
  431. data/lib/rosett_ai/retrofit/base_parser.rb +66 -0
  432. data/lib/rosett_ai/retrofit/engine.rb +171 -0
  433. data/lib/rosett_ai/retrofit/parsers/agents_md_parser.rb +50 -0
  434. data/lib/rosett_ai/retrofit/parsers/claude_parser.rb +69 -0
  435. data/lib/rosett_ai/retrofit/parsers/cursor_parser.rb +82 -0
  436. data/lib/rosett_ai/retrofit/round_trip_validator.rb +65 -0
  437. data/lib/rosett_ai/retrofit/scanner.rb +47 -0
  438. data/lib/rosett_ai/retrofit/secret_detector.rb +87 -0
  439. data/lib/rosett_ai/secrets_resolver.rb +71 -0
  440. data/lib/rosett_ai/smart_feedback/suggester.rb +83 -0
  441. data/lib/rosett_ai/smart_feedback/thor_middleware.rb +84 -0
  442. data/lib/rosett_ai/structured_logger.rb +110 -0
  443. data/lib/rosett_ai/telemetry/json_lines_writer.rb +50 -0
  444. data/lib/rosett_ai/telemetry/log_rotator.rb +67 -0
  445. data/lib/rosett_ai/telemetry/provider.rb +26 -0
  446. data/lib/rosett_ai/telemetry/reporter.rb +144 -0
  447. data/lib/rosett_ai/telemetry.rb +47 -0
  448. data/lib/rosett_ai/text_sanitizer.rb +62 -0
  449. data/lib/rosett_ai/thor/cli.rb +269 -0
  450. data/lib/rosett_ai/thor/tasks/adopt.rb +250 -0
  451. data/lib/rosett_ai/thor/tasks/backup.rb +420 -0
  452. data/lib/rosett_ai/thor/tasks/behaviour.rb +474 -0
  453. data/lib/rosett_ai/thor/tasks/build.rb +1162 -0
  454. data/lib/rosett_ai/thor/tasks/compile.rb +415 -0
  455. data/lib/rosett_ai/thor/tasks/completion.rb +123 -0
  456. data/lib/rosett_ai/thor/tasks/comply.rb +82 -0
  457. data/lib/rosett_ai/thor/tasks/config.rb +265 -0
  458. data/lib/rosett_ai/thor/tasks/content.rb +193 -0
  459. data/lib/rosett_ai/thor/tasks/dbus.rb +321 -0
  460. data/lib/rosett_ai/thor/tasks/design.rb +258 -0
  461. data/lib/rosett_ai/thor/tasks/desktop.rb +129 -0
  462. data/lib/rosett_ai/thor/tasks/doctor.rb +127 -0
  463. data/lib/rosett_ai/thor/tasks/documentation.rb +321 -0
  464. data/lib/rosett_ai/thor/tasks/engines.rb +167 -0
  465. data/lib/rosett_ai/thor/tasks/hooks.rb +219 -0
  466. data/lib/rosett_ai/thor/tasks/init.rb +259 -0
  467. data/lib/rosett_ai/thor/tasks/license.rb +120 -0
  468. data/lib/rosett_ai/thor/tasks/mcp.rb +535 -0
  469. data/lib/rosett_ai/thor/tasks/migrate.rb +121 -0
  470. data/lib/rosett_ai/thor/tasks/plugins.rb +157 -0
  471. data/lib/rosett_ai/thor/tasks/project.rb +260 -0
  472. data/lib/rosett_ai/thor/tasks/provenance.rb +195 -0
  473. data/lib/rosett_ai/thor/tasks/release.rb +314 -0
  474. data/lib/rosett_ai/thor/tasks/retrofit.rb +90 -0
  475. data/lib/rosett_ai/thor/tasks/tooling.rb +308 -0
  476. data/lib/rosett_ai/thor/tasks/validate.rb +108 -0
  477. data/lib/rosett_ai/thor/tasks/workflow.rb +196 -0
  478. data/lib/rosett_ai/tooling/ci_yaml_validator.rb +37 -0
  479. data/lib/rosett_ai/tooling/version_checker.rb +35 -0
  480. data/lib/rosett_ai/ui/accessible_tui.rb +61 -0
  481. data/lib/rosett_ai/ui/base.rb +46 -0
  482. data/lib/rosett_ai/ui/gtk4.rb +98 -0
  483. data/lib/rosett_ai/ui/kde.rb +40 -0
  484. data/lib/rosett_ai/ui/qt6.rb +40 -0
  485. data/lib/rosett_ai/ui/registry.rb +60 -0
  486. data/lib/rosett_ai/ui/tty_helper.rb +74 -0
  487. data/lib/rosett_ai/ui/tui.rb +59 -0
  488. data/lib/rosett_ai/validators/behaviour_validator.rb +20 -0
  489. data/lib/rosett_ai/validators/design_validator.rb +17 -0
  490. data/lib/rosett_ai/validators/schema_validator.rb +84 -0
  491. data/lib/rosett_ai/validators/tooling_validator.rb +17 -0
  492. data/lib/rosett_ai/version.rb +8 -0
  493. data/lib/rosett_ai/version_consistency_checker.rb +129 -0
  494. data/lib/rosett_ai/workflow/audit_log.rb +86 -0
  495. data/lib/rosett_ai/workflow/engine.rb +142 -0
  496. data/lib/rosett_ai/workflow/manager.rb +82 -0
  497. data/lib/rosett_ai/workflow/schema_validator.rb +71 -0
  498. data/lib/rosett_ai/workflow/step_runner.rb +61 -0
  499. data/lib/rosett_ai/workflow/steps/prompt_step.rb +62 -0
  500. data/lib/rosett_ai/workflow/steps/rai_step.rb +74 -0
  501. data/lib/rosett_ai/workflow/steps/shell_step.rb +53 -0
  502. data/lib/rosett_ai/yaml_loader.rb +78 -0
  503. data/lib/rosett_ai.rb +221 -0
  504. data/lib/rubocop/cop/rosett_ai/shell_interpolation.rb +54 -0
  505. data/lib/rubocop/cop/rosett_ai/unsafe_const_get.rb +60 -0
  506. data/lib/rubocop/cop/rosett_ai/unsafe_send.rb +50 -0
  507. data/lib/rubocop/cop/rosett_ai/unsafe_yaml_load.rb +40 -0
  508. data/lib/rubocop/rosett_ai.rb +9 -0
  509. data/lib/scripts/generated/docker_hub_tags.rb +126 -0
  510. data/locales/.gitkeep +0 -0
  511. data/locales/ar.yml +579 -0
  512. data/locales/en.yml +571 -0
  513. data/locales/fr.yml +567 -0
  514. data/packaging/build-engine-deb.sh +81 -0
  515. data/packaging/scripts/postinst +17 -0
  516. data/packaging/scripts/postrm +19 -0
  517. data/packaging/scripts/prerm +10 -0
  518. data/packaging/wrapper.sh.template +38 -0
  519. data/rosett-ai.gemspec +63 -0
  520. data/rules/.gitkeep +0 -0
  521. data/scripts/publish/pulp_upload.sh +123 -0
  522. data/settings.json +29 -0
  523. data/share/applications/be.neatnerds.rosettai.desktop +29 -0
  524. data/share/dbus-1/interfaces/be.neatnerds.rosettai.xml +103 -0
  525. data/share/dbus-1/services/be.neatnerds.rosettai.service +3 -0
  526. data/share/templates/behaviour/criticalthinking.yml +69 -0
  527. metadata +810 -0
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Orchestrates behaviour composition: discovers behaviour files,
9
+ # resolves scopes, sorts by priority, detects conflicts and
10
+ # circular dependencies, and produces a deterministic result.
11
+ #
12
+ # Supports configurable merge strategies for rule ID collisions:
13
+ # - first_wins: highest-priority rule wins (default)
14
+ # - deep_merge: recursively merge hash fields from all scopes
15
+ # - array_union: combine array fields from all scopes
16
+ #
17
+ # @author hugo
18
+ # @author claude
19
+ class Composer
20
+ # @param scope_resolver [ScopeResolver] scope resolution strategy
21
+ # @param priority_sorter [PrioritySorter] priority sorting strategy
22
+ # @param conflict_detector [ConflictDetector] conflict detection
23
+ # @param cycle_detector [CircularDependencyDetector] cycle detection
24
+ def initialize(
25
+ scope_resolver: ScopeResolver.new,
26
+ priority_sorter: PrioritySorter.new,
27
+ conflict_detector: ConflictDetector.new,
28
+ cycle_detector: CircularDependencyDetector.new
29
+ )
30
+ @scope_resolver = scope_resolver
31
+ @priority_sorter = priority_sorter
32
+ @conflict_detector = conflict_detector
33
+ @cycle_detector = cycle_detector
34
+ end
35
+
36
+ # Composes all behaviour files into a single result.
37
+ #
38
+ # @param behaviour_files [Array<String>] paths to behaviour YAML files
39
+ # @param project_root [Pathname, nil] project root for scope resolution
40
+ # @param strict [Boolean] treat warnings as errors
41
+ # @param target [String, nil] compilation target (for sensitive filtering)
42
+ # @param merge_strategy [String] merge strategy name (first_wins, deep_merge, array_union)
43
+ # @return [CompositionResult]
44
+ # @raise [RosettAi::CompositionError] on circular dependencies or strict conflicts
45
+ def compose(behaviour_files, project_root: nil, strict: false, target: nil,
46
+ merge_strategy: 'first_wins')
47
+ behaviours = load_behaviours(behaviour_files, project_root: project_root)
48
+ check_circular_dependencies!(behaviours)
49
+
50
+ annotated_rules = annotate_rules(behaviours)
51
+ annotated_rules = filter_sensitive(annotated_rules, target) if target
52
+
53
+ sorted_rules = @priority_sorter.sort_rules(annotated_rules)
54
+ conflicts = @conflict_detector.detect(sorted_rules)
55
+
56
+ if strict && !conflicts.empty?
57
+ raise RosettAi::CompositionError,
58
+ "Composition conflicts in strict mode:\n#{conflicts.join("\n")}"
59
+ end
60
+
61
+ strategy = MergeStrategy.resolve(merge_strategy)
62
+ merged_rules = strategy.new.apply(sorted_rules)
63
+
64
+ trace = build_trace(merged_rules)
65
+ warnings = build_warnings(conflicts, behaviours)
66
+ sources = behaviours.to_h { |behaviour| [behaviour[:name], behaviour[:scope]] }
67
+
68
+ CompositionResult.new(
69
+ rules: merged_rules,
70
+ trace: trace,
71
+ warnings: warnings,
72
+ conflicts: conflicts,
73
+ sources: sources,
74
+ merge_strategy: merge_strategy
75
+ )
76
+ end
77
+
78
+ private
79
+
80
+ def load_behaviours(files, project_root:)
81
+ files.map do |file|
82
+ data = RosettAi::YamlLoader.load_file(file)
83
+ scope = @scope_resolver.resolve(file, project_root: project_root)
84
+ {
85
+ name: data['name'],
86
+ path: file,
87
+ scope: scope,
88
+ scope_weight: @scope_resolver.weight(scope),
89
+ domain: data.fetch('domain', nil),
90
+ sensitive: data.fetch('sensitive', false),
91
+ depends_on: Array(data['depends_on']),
92
+ rules: Array(data['rules'])
93
+ }
94
+ end
95
+ end
96
+
97
+ def check_circular_dependencies!(behaviours)
98
+ graph = behaviours.to_h { |behaviour| [behaviour[:name], behaviour[:depends_on]] }
99
+ cycle = @cycle_detector.detect(graph)
100
+
101
+ return unless cycle
102
+
103
+ raise RosettAi::CompositionError,
104
+ "Circular dependency detected: #{cycle.join(' -> ')}"
105
+ end
106
+
107
+ def annotate_rules(behaviours)
108
+ behaviours.flat_map do |behaviour|
109
+ behaviour[:rules].map do |rule|
110
+ {
111
+ id: rule['id'],
112
+ description: rule['description'],
113
+ priority: rule.fetch('priority', 50),
114
+ enabled: rule.fetch('enabled', true),
115
+ behaviour_name: behaviour[:name],
116
+ scope: behaviour[:scope],
117
+ scope_weight: behaviour[:scope_weight],
118
+ domain: behaviour[:domain],
119
+ sensitive: behaviour[:sensitive]
120
+ }
121
+ end
122
+ end
123
+ end
124
+
125
+ def filter_sensitive(rules, target)
126
+ return rules if target == 'claude' || target.nil?
127
+
128
+ rules.reject { |r| r[:sensitive] }
129
+ end
130
+
131
+ def build_trace(sorted_rules)
132
+ sorted_rules.each_with_index.map do |rule, index|
133
+ {
134
+ position: index + 1,
135
+ rule_id: rule[:id],
136
+ behaviour: rule[:behaviour_name],
137
+ scope: rule[:scope],
138
+ priority: rule[:priority]
139
+ }
140
+ end
141
+ end
142
+
143
+ def build_warnings(conflicts, behaviours)
144
+ warnings = conflicts.dup
145
+
146
+ behaviours.each do |behaviour|
147
+ ids = behaviour[:rules].map { |r| r['id'] }
148
+ duplicates = ids.select { |id| ids.count(id) > 1 }.uniq
149
+ duplicates.each do |dup_id|
150
+ warnings << "Duplicate rule ID #{dup_id} within #{behaviour[:name]}.yml"
151
+ end
152
+ end
153
+
154
+ warnings
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Value object representing the final result of behaviour composition.
9
+ # Contains the merged rules, metadata about how they were composed,
10
+ # and any warnings or conflicts detected during composition.
11
+ class CompositionResult
12
+ # @return [Array<Hash>] final merged rules in priority order
13
+ attr_reader :rules
14
+
15
+ # @return [Array<Hash>] trace entries showing composition decisions
16
+ attr_reader :trace
17
+
18
+ # @return [Array<String>] warnings emitted during composition
19
+ attr_reader :warnings
20
+
21
+ # @return [Array<String>] conflicts detected (rule ID collisions)
22
+ attr_reader :conflicts
23
+
24
+ # @return [Hash{String => String}] behaviour name => scope mapping
25
+ attr_reader :sources
26
+
27
+ # @return [String] merge strategy used during composition
28
+ attr_reader :merge_strategy
29
+
30
+ # @param rules [Array<Hash>] composed rules
31
+ # @param trace [Array<Hash>] composition trace entries
32
+ # @param warnings [Array<String>] composition warnings
33
+ # @param conflicts [Array<String>] detected conflicts
34
+ # @param sources [Hash{String => String}] behaviour source mapping
35
+ # @param merge_strategy [String] merge strategy name
36
+ # rubocop:disable Metrics/ParameterLists -- value object with all-keyword defaults
37
+ def initialize(rules:, trace: [], warnings: [], conflicts: [], sources: {},
38
+ merge_strategy: 'first_wins')
39
+ # rubocop:enable Metrics/ParameterLists
40
+ @rules = rules.freeze
41
+ @trace = trace.freeze
42
+ @warnings = warnings.freeze
43
+ @conflicts = conflicts.freeze
44
+ @sources = sources.freeze
45
+ @merge_strategy = merge_strategy.freeze
46
+ end
47
+
48
+ # @return [Integer] total number of composed rules
49
+ def rule_count
50
+ rules.size
51
+ end
52
+
53
+ # @return [Boolean] true if any conflicts were detected
54
+ def conflicts?
55
+ !conflicts.empty?
56
+ end
57
+
58
+ # @return [Boolean] true if any warnings were emitted
59
+ def warnings?
60
+ !warnings.empty?
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Detects rule ID collisions across behaviour files.
9
+ # When two behaviours define the same rule ID at the same scope
10
+ # and priority, this is a conflict that must be reported.
11
+ class ConflictDetector
12
+ # Detects rule ID collisions in a list of annotated rules.
13
+ #
14
+ # @param rules [Array<Hash>] rules with :id, :behaviour_name, :scope, :priority
15
+ # @return [Array<String>] conflict descriptions
16
+ def detect(rules)
17
+ conflicts = []
18
+ seen = {}
19
+
20
+ rules.each do |rule|
21
+ rule_id = rule[:id]
22
+ key = rule_id.to_s
23
+
24
+ if seen.key?(key)
25
+ existing = seen[key]
26
+ conflicts << format_conflict(rule_id, existing, rule)
27
+ else
28
+ seen[key] = rule
29
+ end
30
+ end
31
+
32
+ conflicts
33
+ end
34
+
35
+ # Checks if a security-domain rule would be overridden by a
36
+ # non-security rule. This is never allowed.
37
+ #
38
+ # @param existing [Hash] the existing rule
39
+ # @param candidate [Hash] the candidate replacement
40
+ # @return [Boolean] true if this is an invalid override
41
+ def security_override?(existing, candidate)
42
+ existing[:domain] == 'security' && candidate[:domain] != 'security'
43
+ end
44
+
45
+ private
46
+
47
+ def format_conflict(rule_id, existing, candidate)
48
+ "Rule ID collision: #{rule_id} defined in both " \
49
+ "#{existing[:behaviour_name]}.yml and #{candidate[:behaviour_name]}.yml"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'digest'
7
+ require 'yaml'
8
+
9
+ module RosettAi
10
+ module Composition
11
+ # Manages the `.rosett-ai-lock.yml` lockfile for reproducible behaviour
12
+ # composition. Writes the exact composition result so that
13
+ # `--frozen` compilations can reproduce the same output.
14
+ class Lockfile
15
+ FILENAME = '.rosett-ai-lock.yml'
16
+
17
+ # @param root [Pathname] project root directory
18
+ def initialize(root:)
19
+ @root = Pathname.new(root)
20
+ end
21
+
22
+ # @return [Pathname] full path to the lockfile
23
+ def path
24
+ @root.join(FILENAME)
25
+ end
26
+
27
+ # @return [Boolean] true if the lockfile exists
28
+ def exist?
29
+ path.exist?
30
+ end
31
+
32
+ # Writes the lockfile from a CompositionResult.
33
+ #
34
+ # @param result [CompositionResult] composition result
35
+ # @param behaviours [Array<Hash>] source behaviour metadata
36
+ # @return [Pathname] path to the written lockfile
37
+ def write(result, behaviours:)
38
+ data = build_lockfile_data(result, behaviours)
39
+ path.write(YAML.dump(data))
40
+ path
41
+ end
42
+
43
+ # Reads and parses the lockfile.
44
+ #
45
+ # @return [Hash] lockfile data
46
+ # @raise [RosettAi::CompositionError] if lockfile is missing or invalid
47
+ def read
48
+ raise RosettAi::CompositionError, "Lockfile not found: #{path}" unless exist?
49
+
50
+ RosettAi::YamlLoader.load_file(path.to_s)
51
+ end
52
+
53
+ # Checks if behaviour files have changed since the lockfile was written.
54
+ #
55
+ # @param behaviour_files [Array<String>] current behaviour file paths
56
+ # @return [Boolean] true if files match the lockfile
57
+ def fresh?(behaviour_files)
58
+ return false unless exist?
59
+
60
+ data = read
61
+ locked_checksums = data.fetch('behaviours', {})
62
+ current_checksums = compute_checksums(behaviour_files)
63
+
64
+ locked_checksums == current_checksums
65
+ end
66
+
67
+ private
68
+
69
+ def build_lockfile_data(result, behaviours)
70
+ {
71
+ 'lockfile_version' => 1,
72
+ 'generated_at' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
73
+ 'generator' => 'rosett-ai',
74
+ 'generator_version' => RosettAi::VERSION,
75
+ 'merge_strategy' => result.merge_strategy,
76
+ 'rule_count' => result.rule_count,
77
+ 'behaviours' => compute_checksums(behaviours.filter_map { |entry| entry[:path] }),
78
+ 'composition' => result.rules.map do |rule|
79
+ {
80
+ 'id' => rule[:id],
81
+ 'behaviour' => rule[:behaviour_name],
82
+ 'scope' => rule[:scope],
83
+ 'priority' => rule[:priority]
84
+ }
85
+ end
86
+ }
87
+ end
88
+
89
+ def compute_checksums(file_paths)
90
+ file_paths.sort.to_h do |fp|
91
+ content = File.read(fp, encoding: 'utf-8')
92
+ [relative_path(fp), "sha256:#{Digest::SHA256.hexdigest(content)}"]
93
+ end
94
+ end
95
+
96
+ def relative_path(file_path)
97
+ Pathname.new(file_path).relative_path_from(@root).to_s
98
+ rescue ArgumentError
99
+ file_path.to_s
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Configurable merge strategies for resolving rule ID collisions across
9
+ # scopes. When the same rule ID exists in multiple behaviours, the merge
10
+ # strategy determines which value(s) appear in the final composition.
11
+ #
12
+ # @author hugo
13
+ # @author claude
14
+ module MergeStrategy
15
+ STRATEGIES = ['first_wins', 'deep_merge', 'array_union'].freeze
16
+
17
+ # Resolves the strategy class from a name string.
18
+ #
19
+ # @param name [String] strategy name (first_wins, deep_merge, array_union)
20
+ # @return [Class] strategy class
21
+ # @raise [RosettAi::CompositionError] if strategy name is unknown
22
+ def self.resolve(name)
23
+ case name.to_s
24
+ when 'first_wins' then FirstWins
25
+ when 'deep_merge' then DeepMerge
26
+ when 'array_union' then ArrayUnion
27
+ else
28
+ raise RosettAi::CompositionError,
29
+ "Unknown merge strategy: #{name}. Valid: #{STRATEGIES.join(', ')}"
30
+ end
31
+ end
32
+
33
+ # Groups rules by ID while preserving insertion order.
34
+ #
35
+ # @param rules [Array<Hash>] rules with :id key
36
+ # @return [Array<Array(String, Array<Hash>)>] ordered [id, group] pairs
37
+ def self.group_by_id(rules)
38
+ groups = rules.group_by { |r| r[:id] }
39
+ seen = {}
40
+ rules.filter_map do |rule|
41
+ id = rule[:id]
42
+ next if seen.key?(id)
43
+
44
+ seen[id] = true
45
+ [id, groups[id]]
46
+ end
47
+ end
48
+
49
+ # First-wins strategy: the highest-priority (first in sorted order)
50
+ # rule wins. Duplicate IDs from lower-priority scopes are dropped.
51
+ # This is the default behaviour and matches pre-merge-strategy semantics.
52
+ class FirstWins
53
+ # @param rules [Array<Hash>] sorted rules (highest priority first)
54
+ # @return [Array<Hash>] deduplicated rules (first occurrence wins)
55
+ def apply(rules)
56
+ MergeStrategy.group_by_id(rules).map { |_id, group| group.first }
57
+ end
58
+ end
59
+
60
+ # Deep-merge strategy: when the same rule ID exists in multiple scopes,
61
+ # hash fields from all occurrences are recursively merged. Higher-priority
62
+ # values take precedence for scalar fields.
63
+ class DeepMerge
64
+ # @param rules [Array<Hash>] sorted rules (highest priority first)
65
+ # @return [Array<Hash>] merged rules (hashes deep-merged by ID)
66
+ def apply(rules)
67
+ MergeStrategy.group_by_id(rules).map do |_id, group|
68
+ next group.first if group.size == 1
69
+
70
+ merge_group(group)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def merge_group(group)
77
+ # Reverse so lowest-priority is merged first, highest-priority wins
78
+ group.reverse.reduce({}) { |merged, rule| deep_merge_hashes(merged, rule) }
79
+ end
80
+
81
+ def deep_merge_hashes(base, override)
82
+ base.merge(override) do |_key, old_val, new_val|
83
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
84
+ deep_merge_hashes(old_val, new_val)
85
+ else
86
+ new_val
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # Array-union strategy: when the same rule ID exists in multiple scopes,
93
+ # array fields are combined (union) and deduplicated. Scalar fields use
94
+ # highest-priority value.
95
+ class ArrayUnion
96
+ # @param rules [Array<Hash>] sorted rules (highest priority first)
97
+ # @return [Array<Hash>] merged rules (arrays unioned by ID)
98
+ def apply(rules)
99
+ MergeStrategy.group_by_id(rules).map do |_id, group|
100
+ next group.first if group.size == 1
101
+
102
+ merge_group(group)
103
+ end
104
+ end
105
+
106
+ private
107
+
108
+ def merge_group(group)
109
+ # Start with highest-priority, union arrays from lower-priority
110
+ group.reduce({}) do |merged, rule|
111
+ if merged.empty?
112
+ rule
113
+ else
114
+ union_merge(merged, rule)
115
+ end
116
+ end
117
+ end
118
+
119
+ def union_merge(base, other)
120
+ base.merge(other) do |_key, old_val, new_val|
121
+ if old_val.is_a?(Array) && new_val.is_a?(Array)
122
+ (old_val | new_val)
123
+ else
124
+ old_val # higher-priority wins for non-arrays
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Sorts rules by priority within their scope.
9
+ # Lower priority numbers are higher importance (1 = most important).
10
+ # Equal-priority rules within the same scope are ordered by behaviour
11
+ # name alphabetically for deterministic output.
12
+ class PrioritySorter
13
+ # Sorts an array of annotated rules by scope weight, then priority,
14
+ # then behaviour name.
15
+ #
16
+ # @param rules [Array<Hash>] rules with :scope_weight, :priority, :behaviour_name keys
17
+ # @return [Array<Hash>] sorted rules (stable, deterministic)
18
+ def sort_rules(rules)
19
+ rules.sort_by do |rule|
20
+ [
21
+ -(rule[:scope_weight] || 0), # higher scope weight first (negate for ascending sort)
22
+ rule[:priority] || 50, # lower priority number first
23
+ rule[:behaviour_name].to_s # alphabetical tie-break
24
+ ]
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Composition
8
+ # Resolves the scope hierarchy for behaviour files.
9
+ # Scopes are ordered: global > organisation > project > local.
10
+ # Later scopes override earlier ones at equal priority.
11
+ class ScopeResolver
12
+ # Ordered scope names from lowest to highest precedence.
13
+ SCOPE_ORDER = ['global', 'organisation', 'project', 'local'].freeze
14
+
15
+ # Numeric weight for each scope (higher = takes precedence).
16
+ SCOPE_WEIGHTS = SCOPE_ORDER.each_with_index.to_h { |scope, i| [scope, i] }.freeze
17
+
18
+ # Resolves the scope for a given behaviour file path.
19
+ #
20
+ # @param file_path [String, Pathname] path to the behaviour file
21
+ # @param project_root [Pathname, nil] detected project root
22
+ # @return [String] one of SCOPE_ORDER values
23
+ def resolve(file_path, project_root: nil)
24
+ path = file_path.to_s
25
+
26
+ if project_root && path.start_with?(project_root.join('.rosett-ai', 'conf', 'behaviour').to_s)
27
+ 'project'
28
+ elsif path.include?('/organisation/') || path.include?('/org/')
29
+ 'organisation'
30
+ elsif path.include?('/local/')
31
+ 'local'
32
+ else
33
+ 'global'
34
+ end
35
+ end
36
+
37
+ # Returns the numeric weight for a scope name.
38
+ #
39
+ # @param scope [String] scope name
40
+ # @return [Integer] weight (higher = overrides lower)
41
+ def weight(scope)
42
+ SCOPE_WEIGHTS.fetch(scope, 0)
43
+ end
44
+
45
+ # Compares two scopes. Returns true if scope_a overrides scope_b.
46
+ #
47
+ # @param scope_a [String] candidate overriding scope
48
+ # @param scope_b [String] candidate overridden scope
49
+ # @return [Boolean]
50
+ def overrides?(scope_a, scope_b)
51
+ weight(scope_a) > weight(scope_b)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ module Config
8
+ # Structured result from compiling a single scope file.
9
+ #
10
+ # @!attribute scope [String] The scope name (managed, user, project, local)
11
+ # @!attribute source_path [Pathname] Path to the source YAML file
12
+ # @!attribute target_path [Pathname] Path to the target JSON file
13
+ # @!attribute json_data [Hash] The compiled JSON data (before serialization)
14
+ # @!attribute warnings [Array<String>] Advisory messages (version mismatch, missing env vars, etc.)
15
+ # @!attribute action [Symbol] One of :created, :updated, :unchanged, :skipped
16
+ # @!attribute diff [String, nil] Unified diff string (for simulate mode)
17
+ CompileResult = Struct.new(
18
+ :scope, :source_path, :target_path, :json_data,
19
+ :warnings, :action, :diff
20
+ ) do
21
+ def initialize(**)
22
+ super
23
+ self.warnings ||= []
24
+ end
25
+
26
+ # @return [Boolean] true if action is :created, :updated, or :unchanged
27
+ def success?
28
+ [:created, :updated, :unchanged].include?(action)
29
+ end
30
+
31
+ # @return [Boolean] true if action is :created or :updated
32
+ def changed?
33
+ [:created, :updated].include?(action)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ # Configuration subsystem.
8
+ module Config
9
+ # Deprecated — use RosettAiEngine::Claude::ConfigCompiler. Will be removed in v1.1.0.
10
+ # Only defined when rosett-ai-engine-claude gem is installed.
11
+ Compiler = RosettAiEngine::Claude::ConfigCompiler if defined?(RosettAiEngine::Claude::ConfigCompiler)
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ # Configuration subsystem.
8
+ module Config
9
+ # Deprecated — use RosettAiEngine::Claude::DomainTransformer. Will be removed in v1.1.0.
10
+ # Only defined when rosett-ai-engine-claude gem is installed.
11
+ DomainTransformer = RosettAiEngine::Claude::DomainTransformer if defined?(RosettAiEngine::Claude::DomainTransformer)
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ # Configuration subsystem.
8
+ module Config
9
+ # Deprecated — use RosettAiEngine::Claude::KeyMap. Will be removed in v1.1.0.
10
+ # Only defined when rosett-ai-engine-claude gem is installed.
11
+ KeyMap = RosettAiEngine::Claude::KeyMap if defined?(RosettAiEngine::Claude::KeyMap)
12
+ end
13
+ end