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