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
data/kitchen.yml ADDED
@@ -0,0 +1,46 @@
1
+ # SPDX-License-Identifier: GPL-3.0-only
2
+ # SPDX-FileCopyrightText: 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
3
+ #
4
+ # Test Kitchen configuration for Rosett-AI package verification
5
+ #
6
+ # Prerequisites:
7
+ # - Vagrant 2.3.x (MPL-2.0) with vagrant-libvirt plugin
8
+ # - libvirt/QEMU/KVM installed and running
9
+ # - Pre-built .deb in pkg/ (rake build:package)
10
+ #
11
+ # Usage:
12
+ # bundle exec kitchen test debian-12
13
+ # bundle exec kitchen converge debian-12
14
+ # bundle exec kitchen verify debian-12
15
+ # bundle exec kitchen destroy debian-12
16
+
17
+ driver:
18
+ name: vagrant
19
+ provider: libvirt
20
+
21
+ provisioner:
22
+ name: shell
23
+ script: test/integration/provision.sh
24
+ data_path: pkg
25
+
26
+ transport:
27
+ name: ssh
28
+
29
+ verifier:
30
+ name: inspec
31
+ sudo: false
32
+
33
+ platforms:
34
+ - name: debian-12
35
+ driver:
36
+ box: debian/bookworm64
37
+
38
+ suites:
39
+ - name: rai-context
40
+ verifier:
41
+ inspec_tests:
42
+ - test/integration/rai_context
43
+ - name: rai-commands
44
+ verifier:
45
+ inspec_tests:
46
+ - test/integration/rai_commands
@@ -0,0 +1,77 @@
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 Adopter
8
+ # Resolves the correct executor for an engine by reading its manifest
9
+ # to determine the API type and constructor kwargs.
10
+ #
11
+ # SDK-based engines (api_gem) receive api_key: and base_url:.
12
+ # HTTP-based engines (api_type: http|openai_compatible) receive
13
+ # endpoint: and model: from the manifest defaults.
14
+ # CLI-based engines (api_type: cli) receive binary: from the manifest.
15
+ class ExecutorResolver
16
+ def initialize(engine_name)
17
+ @engine_name = engine_name.to_s
18
+ @manifest = RosettAi::Engines::Registry.manifest(@engine_name)
19
+ end
20
+
21
+ # Build an executor instance with the correct kwargs for the engine.
22
+ #
23
+ # @param api_key [String, nil] API key (SDK-based engines only)
24
+ # @return [Object] executor instance with #analyze method
25
+ def resolve(api_key: nil)
26
+ engine_mod = RosettAi::Engines::Registry.engine_module(@engine_name)
27
+ executor_klass = engine_mod.executor_class
28
+ raise RosettAi::AdoptError, "Engine '#{@engine_name}' does not provide an executor" unless executor_klass
29
+
30
+ executor_klass.new(**build_kwargs(api_key))
31
+ end
32
+
33
+ private
34
+
35
+ def build_kwargs(api_key)
36
+ execution = @manifest['execution'] || {}
37
+
38
+ if execution['api_gem']
39
+ guard_api_key!(api_key)
40
+ sdk_kwargs(api_key)
41
+ elsif execution['api_type'] == 'cli'
42
+ cli_kwargs(execution)
43
+ else
44
+ http_kwargs(execution)
45
+ end
46
+ end
47
+
48
+ def guard_api_key!(api_key)
49
+ return unless api_key.nil?
50
+
51
+ raise RosettAi::AdoptError,
52
+ "Engine '#{@engine_name}' requires an API key. " \
53
+ 'Set ANTHROPIC_API_KEY or use --local for local-only analysis.'
54
+ end
55
+
56
+ def sdk_kwargs(api_key)
57
+ {
58
+ api_key: api_key,
59
+ base_url: ENV.fetch('ANTHROPIC_API_BASE_URL', nil)
60
+ }
61
+ end
62
+
63
+ def cli_kwargs(execution)
64
+ kwargs = {}
65
+ kwargs[:binary] = execution['cli_binary'] if execution['cli_binary']
66
+ kwargs
67
+ end
68
+
69
+ def http_kwargs(execution)
70
+ kwargs = {}
71
+ kwargs[:endpoint] = execution['default_endpoint'] if execution['default_endpoint']
72
+ kwargs[:model] = execution['default_model'] if execution['default_model']
73
+ kwargs
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,154 @@
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 Adopter
8
+ # Performs local structural analysis of compiled rule files
9
+ # without making API calls. Checks for duplicates, missing
10
+ # fields, and other structural issues.
11
+ class LocalAnalysisCollector
12
+ FAIL_SEVERITIES = ['high', 'critical'].freeze
13
+
14
+ def analyze(files)
15
+ @findings = []
16
+ @all_rules = {}
17
+
18
+ collect_rules(files)
19
+ check_duplicate_ids
20
+ check_identical_descriptions
21
+ build_result
22
+ end
23
+
24
+ private
25
+
26
+ def collect_rules(files)
27
+ files.each do |file|
28
+ basename = File.basename(file)
29
+ content = File.read(file, encoding: 'utf-8')
30
+ rules = extract_rules_from_markdown(content)
31
+ process_rules(rules, basename)
32
+ end
33
+ end
34
+
35
+ def process_rules(rules, basename)
36
+ rules.each do |rule|
37
+ rule_id = rule[:id]
38
+ @all_rules[rule_id] ||= []
39
+ @all_rules[rule_id] << { file: basename, description: rule[:description] }
40
+
41
+ check_empty_description(rule, basename)
42
+ check_missing_priority(rule, basename)
43
+ end
44
+ end
45
+
46
+ def check_empty_description(rule, basename)
47
+ return unless rule[:description].to_s.strip.empty?
48
+
49
+ @findings << finding(category: 'other', severity: 'medium', file: basename, rule_id: rule[:id],
50
+ summary: 'Empty rule description',
51
+ explanation: "Rule #{rule[:id]} has an empty or blank description.")
52
+ end
53
+
54
+ def check_missing_priority(rule, basename)
55
+ return if rule[:has_priority]
56
+
57
+ @findings << finding(category: 'other', severity: 'low', file: basename, rule_id: rule[:id],
58
+ summary: 'No priority set',
59
+ explanation: "Rule #{rule[:id]} does not have a priority specified.")
60
+ end
61
+
62
+ def check_duplicate_ids
63
+ @all_rules.each do |rule_id, occurrences|
64
+ next unless occurrences.size > 1
65
+
66
+ file_list = occurrences.map { |occ| occ[:file] }.join(', ')
67
+ @findings << finding(category: 'duplicates', severity: 'medium', file: occurrences.first[:file],
68
+ rule_id: rule_id, summary: 'Duplicate rule ID across files',
69
+ explanation: "Rule ID '#{rule_id}' appears in multiple files: #{file_list}.")
70
+ end
71
+ end
72
+
73
+ def check_identical_descriptions
74
+ desc_map = build_description_map
75
+ desc_map.each_value do |entries|
76
+ next unless entries.size > 1
77
+ next if entries.map { |e| e[:rule_id] }.uniq.size == 1
78
+
79
+ label = entries.map { |e| "#{e[:rule_id]} (#{e[:file]})" }.join(', ')
80
+ @findings << finding(category: 'duplicates', severity: 'low', file: entries.first[:file],
81
+ rule_id: entries.first[:rule_id], summary: 'Identical rule descriptions',
82
+ explanation: "Multiple rules share the same description: #{label}.")
83
+ end
84
+ end
85
+
86
+ def build_description_map
87
+ desc_map = {}
88
+ @all_rules.each do |rule_id, occurrences|
89
+ occurrences.each do |occ|
90
+ desc = occ[:description]&.strip
91
+ next if desc.to_s.strip.empty?
92
+
93
+ desc_map[desc] ||= []
94
+ desc_map[desc] << { rule_id: rule_id, file: occ[:file] }
95
+ end
96
+ end
97
+ desc_map
98
+ end
99
+
100
+ def build_result
101
+ status = if @findings.any? { |finding| FAIL_SEVERITIES.include?(finding['severity']) }
102
+ 'fail'
103
+ elsif @findings.any?
104
+ 'warn'
105
+ else
106
+ 'pass'
107
+ end
108
+
109
+ {
110
+ 'findings' => @findings,
111
+ 'overall_status' => status,
112
+ 'summary' => @findings.empty? ? 'No structural issues detected' : "Found #{@findings.size} structural issue(s)"
113
+ }
114
+ end
115
+
116
+ def finding(attrs)
117
+ {
118
+ 'category' => attrs[:category], 'severity' => attrs[:severity],
119
+ 'file' => attrs[:file], 'rule_id' => attrs[:rule_id],
120
+ 'summary' => attrs[:summary], 'explanation' => attrs[:explanation]
121
+ }
122
+ end
123
+
124
+ def extract_rules_from_markdown(content)
125
+ rules = extract_heading_rules(content)
126
+ return rules unless rules.empty?
127
+
128
+ extract_bullet_rules(content)
129
+ end
130
+
131
+ def extract_heading_rules(content)
132
+ rules = []
133
+ content.scan(/^### (\S+) \(priority: (\d+)\)\s*\n\n?(.*?)(?=\n### |\n\z|\z)/m) do |id, _priority, desc|
134
+ rules << { id: id, description: desc.strip, has_priority: true }
135
+ end
136
+ content.scan(/^### (\S+)\s*\n\n?(.*?)(?=\n### |\n\z|\z)/m) do |id, desc|
137
+ next if rules.any? { |rule| rule[:id] == id }
138
+
139
+ rules << { id: id, description: desc.strip, has_priority: false }
140
+ end
141
+ rules
142
+ end
143
+
144
+ def extract_bullet_rules(content)
145
+ rules = []
146
+ content.scan(/^- (.+)$/) do |desc,|
147
+ index = rules.size + 1
148
+ rules << { id: format('bullet_%03d', index), description: desc.strip, has_priority: false }
149
+ end
150
+ rules
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,254 @@
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 'digest'
8
+ require 'json'
9
+ require 'time'
10
+
11
+ module RosettAi
12
+ module Adopter
13
+ # Analyzes compiled markdown rule files for inconsistencies, conflicts,
14
+ # harmful content, duplicates, and other issues.
15
+ #
16
+ # Supports four layers of data privacy protection:
17
+ # 1. Opt-in per file — sensitive: true in YAML excludes from API analysis
18
+ # 2. Redaction — regex patterns replace matches before sending to API
19
+ # 3. Configurable endpoint — ANTHROPIC_API_BASE_URL for proxy/Bedrock/Vertex
20
+ # 4. Local-only mode — structural checks without API calls
21
+ class RuleAdopter
22
+ GENERATED_MARKER = '<!-- rosett-ai-'
23
+
24
+ attr_reader :rules_dir, :cache_path, :redactions_path, :engine
25
+
26
+ def initialize(rules_dir:, cache_path:, redactions_path:, engine: 'claude')
27
+ @rules_dir = Pathname.new(rules_dir)
28
+ @cache_path = Pathname.new(cache_path)
29
+ @redactions_path = Pathname.new(redactions_path)
30
+ @engine = engine.to_s
31
+ end
32
+
33
+ def evaluate(local_only: false)
34
+ files = discover_managed_files
35
+ raise RosettAi::AdoptError, 'No managed rule files found in rules directory' if files.empty?
36
+
37
+ sensitive = sensitive_files
38
+ api_files = filter_sensitive(files, sensitive)
39
+
40
+ if local_only
41
+ evaluate_local(files, sensitive)
42
+ else
43
+ evaluate_remote(files, api_files, sensitive)
44
+ end
45
+ end
46
+
47
+ def discover_managed_files
48
+ return [] unless rules_dir.exist?
49
+
50
+ Dir.glob(rules_dir.join('*.md')).select do |file|
51
+ first_line = File.open(file, &:readline)
52
+ first_line.start_with?(GENERATED_MARKER)
53
+ rescue EOFError
54
+ false
55
+ end.sort
56
+ end
57
+
58
+ def content_checksum(files)
59
+ content = files.sort.map { |f| File.read(f) }.join
60
+ Digest::SHA256.hexdigest(content)
61
+ end
62
+
63
+ def cached_result(checksum)
64
+ return nil unless cache_path.exist?
65
+
66
+ data = RosettAi::YamlLoader.load_file(cache_path.to_s, permitted_classes: [Time, Date])
67
+ return nil unless data.is_a?(Hash) && data['checksum'] == checksum
68
+ return nil if cache_expired?(data)
69
+
70
+ data['result']
71
+ rescue Psych::SyntaxError, Psych::DisallowedClass => e
72
+ RosettAi.logger.warn("Corrupt adopt cache (#{e.message}), will re-analyze")
73
+ nil
74
+ end
75
+
76
+ def write_cache(checksum, result)
77
+ FileUtils.mkdir_p(cache_path.dirname)
78
+ data = {
79
+ 'checksum' => checksum,
80
+ 'analyzed_at' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
81
+ 'result' => result
82
+ }
83
+ File.open(cache_path, 'w', 0o644) { |f| f.write(data.to_yaml) }
84
+ end
85
+
86
+ def build_prompt(files)
87
+ parts = [prompt_header]
88
+ append_file_contents(parts, files)
89
+ parts.join("\n")
90
+ end
91
+
92
+ def redact(content)
93
+ patterns = load_redaction_patterns
94
+ result = content.dup
95
+ patterns.each do |entry|
96
+ regex = Regexp.new(entry['pattern'])
97
+ result.gsub!(regex, entry['replacement'])
98
+ rescue RegexpError => e
99
+ RosettAi.logger.warn("Invalid redaction pattern '#{entry['pattern']}': #{e.message}")
100
+ end
101
+ result
102
+ end
103
+
104
+ def analyze(files)
105
+ return empty_result if files.empty?
106
+
107
+ prompt = build_prompt(files)
108
+ executor = resolve_executor
109
+ executor.analyze(prompt)
110
+ end
111
+
112
+ def analyze_local(files)
113
+ collector = LocalAnalysisCollector.new
114
+ collector.analyze(files)
115
+ end
116
+
117
+ def sensitive_files
118
+ behaviour_dir = RosettAi.root.join('conf', 'behaviour')
119
+ return [] unless behaviour_dir.exist?
120
+
121
+ Dir.glob(behaviour_dir.join('*.yml')).select do |file|
122
+ data = RosettAi::YamlLoader.load_file(file)
123
+ data.is_a?(Hash) && data['sensitive'] == true
124
+ rescue StandardError
125
+ false
126
+ end
127
+ end
128
+
129
+ def filter_sensitive(files, sensitive_sources)
130
+ return files if sensitive_sources.empty?
131
+
132
+ sensitive_names = sensitive_sources.map { |f| RosettAi::TextSanitizer.normalize_nfc(File.basename(f, '.yml')) }
133
+ files.reject do |file|
134
+ name = File.basename(file, '.md').sub(/\A[^-]+-/, '')
135
+ sensitive_names.include?(name)
136
+ end
137
+ end
138
+
139
+ private
140
+
141
+ def cache_expired?(data)
142
+ ttl_hours = RosettAi.rai_config.cache_ttl_hours
143
+ analyzed_at = data['analyzed_at']
144
+ return false unless analyzed_at
145
+
146
+ age_seconds = Time.now.utc - Time.parse(analyzed_at.to_s)
147
+ expired = age_seconds > (ttl_hours * 3600)
148
+ RosettAi.logger.debug("Adopt cache expired (#{ttl_hours}h TTL)") if expired
149
+ expired
150
+ end
151
+
152
+ def resolve_executor
153
+ resolver = ExecutorResolver.new(@engine)
154
+ resolver.resolve(api_key: @api_key)
155
+ end
156
+
157
+ def evaluate_local(files, sensitive)
158
+ result = analyze_local(files)
159
+ { result: result, sensitive_skipped: sensitive, cached: false }
160
+ end
161
+
162
+ def evaluate_remote(files, api_files, sensitive)
163
+ checksum = content_checksum(api_files)
164
+ cached = cached_result(checksum)
165
+ return { result: cached, sensitive_skipped: sensitive, cached: true } if cached
166
+
167
+ @api_key = RosettAi::SecretsResolver.resolve('ANTHROPIC_API_KEY')
168
+
169
+ result = analyze(api_files)
170
+ local_findings = analyze_local(files)
171
+ merged = merge_results(result, local_findings)
172
+ write_cache(checksum, merged)
173
+ { result: merged, sensitive_skipped: sensitive, cached: false }
174
+ end
175
+
176
+ def prompt_header
177
+ <<~PROMPT
178
+ Analyze the following rosett-ai rule files for issues.
179
+ Look for:
180
+ - Inconsistencies between rules across files
181
+ - Conflicting rules that contradict each other
182
+ - Harmful or illegal instructions
183
+ - Jailbreaking attempts that try to bypass safety measures
184
+ - Rules that instruct breaking other rules
185
+ - Duplicate rules (same intent, possibly different wording)
186
+ - Any other problematic patterns
187
+
188
+ Respond ONLY with a JSON object (no markdown fences) in this exact format:
189
+ {
190
+ "findings": [
191
+ {
192
+ "category": "inconsistencies|conflicts|harmful|illegal|jailbreaking|rule_breaking|duplicates|other",
193
+ "severity": "low|medium|high|critical",
194
+ "file": "filename.md",
195
+ "rule_id": "rule_id or null",
196
+ "summary": "Brief description",
197
+ "explanation": "Detailed explanation"
198
+ }
199
+ ],
200
+ "overall_status": "pass|warn|fail",
201
+ "summary": "One-line overall summary"
202
+ }
203
+
204
+ If no issues are found, return an empty findings array with overall_status "pass".
205
+ PROMPT
206
+ end
207
+
208
+ def append_file_contents(parts, files)
209
+ files.each do |file|
210
+ basename = File.basename(file)
211
+ content = redact(File.read(file, encoding: 'utf-8'))
212
+ parts << "--- FILE: #{basename} ---"
213
+ parts << content
214
+ parts << "--- END FILE ---\n"
215
+ end
216
+ end
217
+
218
+ def load_redaction_patterns
219
+ return [] unless redactions_path.exist?
220
+
221
+ data = RosettAi::YamlLoader.load_file(redactions_path.to_s)
222
+ return [] unless data.is_a?(Hash) && data['patterns'].is_a?(Array)
223
+
224
+ data['patterns']
225
+ end
226
+
227
+ def empty_result
228
+ { 'findings' => [], 'overall_status' => 'pass', 'summary' => 'No files to analyze' }
229
+ end
230
+
231
+ def merge_results(api_result, local_result)
232
+ merged_findings = (api_result['findings'] || []) + (local_result['findings'] || [])
233
+ fail_severities = ['high', 'critical']
234
+ status = determine_status(merged_findings, fail_severities)
235
+
236
+ {
237
+ 'findings' => merged_findings,
238
+ 'overall_status' => status,
239
+ 'summary' => merged_findings.empty? ? 'No issues detected' : "Found #{merged_findings.size} issue(s)"
240
+ }
241
+ end
242
+
243
+ def determine_status(findings, fail_severities)
244
+ if findings.any? { |f| fail_severities.include?(f['severity']) }
245
+ 'fail'
246
+ elsif findings.any?
247
+ 'warn'
248
+ else
249
+ 'pass'
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,111 @@
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 AiConfig
8
+ # Compiles generic AI configuration to engine-native settings.
9
+ #
10
+ # Takes the canonical `.rosett-ai/conf/ai_config.yml` and transforms it
11
+ # into engine-specific configuration keys. Unsupported settings
12
+ # produce warnings (or errors in strict mode).
13
+ #
14
+ # When an engine manifest declares `capabilities.config: false`,
15
+ # a warning is emitted (error in strict mode). Fallback chains
16
+ # from config are compiled with local-to-remote transition warnings.
17
+ #
18
+ # @author hugo
19
+ # @author claude
20
+ class ConfigCompiler
21
+ # @param model_router [ModelRouter] model tier resolver
22
+ # @param strict [Boolean] treat unsupported settings as errors
23
+ def initialize(model_router: ModelRouter.new, strict: false)
24
+ @model_router = model_router
25
+ @strict = strict
26
+ end
27
+
28
+ # Compiles AI config for a specific engine.
29
+ #
30
+ # @param config [Hash] parsed ai_config.yml data
31
+ # @param engine [String] target engine identifier
32
+ # @return [Hash] compilation result with :settings and :warnings
33
+ def compile(config, engine:)
34
+ warnings = []
35
+ settings = {}
36
+
37
+ check_engine_capability(engine, warnings)
38
+ compile_model_routing(config, engine, settings, warnings)
39
+ compile_context_window(config, engine, settings, warnings)
40
+ compile_cost_controls(config, settings)
41
+ compile_fallback_chain(config, settings, warnings)
42
+
43
+ { settings: settings, warnings: warnings }
44
+ end
45
+
46
+ private
47
+
48
+ def check_engine_capability(engine, warnings)
49
+ manifest = RosettAi::Engines::Registry.manifest(engine)
50
+ return if manifest.dig('capabilities', 'config') != false
51
+
52
+ handle_warning(warnings, "Engine '#{engine}' declares config capability as false")
53
+ rescue RosettAi::Error
54
+ nil
55
+ end
56
+
57
+ def compile_fallback_chain(config, settings, warnings)
58
+ engines = config.dig('fallback_chain', 'engines')
59
+ return unless engines.is_a?(Array) && engines.size > 1
60
+
61
+ chain = FallbackChain.new(engines: engines)
62
+ settings['fallback_chain'] = chain.to_h
63
+ chain.warnings.each { |w| warnings << w }
64
+ end
65
+
66
+ def compile_model_routing(config, engine, settings, warnings)
67
+ routing = config.fetch('model_routing', {})
68
+ return if routing.empty?
69
+
70
+ resolved = {}
71
+ routing.each do |task_type, tier|
72
+ resolved[task_type] = @model_router.resolve(tier, engine: engine)
73
+ rescue RosettAi::AiConfigError => e
74
+ handle_warning(warnings, e.message)
75
+ end
76
+ settings['model_routing'] = resolved unless resolved.empty?
77
+ end
78
+
79
+ def compile_context_window(config, engine, settings, warnings)
80
+ context = config.fetch('context_window', {})
81
+ return if context.empty?
82
+
83
+ window = ContextWindow.new(
84
+ max_tokens: context.fetch('max_tokens', ContextWindow::DEFAULT_MAX_TOKENS),
85
+ output_reserve: context.fetch('output_reserve', ContextWindow::DEFAULT_RESERVE),
86
+ truncation_strategy: context.fetch('truncation_strategy', 'tail')
87
+ )
88
+ settings['context_window'] = window.to_h
89
+ rescue ArgumentError => e
90
+ handle_warning(warnings, "Context window: #{e.message} (engine: #{engine})")
91
+ end
92
+
93
+ def compile_cost_controls(config, settings)
94
+ cost = config.fetch('cost_controls', {})
95
+ return if cost.empty?
96
+
97
+ controls = CostControls.new(
98
+ preferred_tier: cost.fetch('preferred_tier', 'standard'),
99
+ monthly_budget_note: cost['monthly_budget_note']
100
+ )
101
+ settings['cost_controls'] = controls.to_h
102
+ end
103
+
104
+ def handle_warning(warnings, message)
105
+ raise RosettAi::AiConfigError, message if @strict
106
+
107
+ warnings << message
108
+ end
109
+ end
110
+ end
111
+ 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 AiConfig
8
+ # Manages context window configuration settings.
9
+ #
10
+ # Tracks max tokens, output reserve, and truncation strategy
11
+ # for compilation into engine-native settings.
12
+ class ContextWindow
13
+ TRUNCATION_STRATEGIES = ['tail', 'head', 'middle'].freeze
14
+ DEFAULT_MAX_TOKENS = 200_000
15
+ DEFAULT_RESERVE = 8192
16
+
17
+ attr_reader :max_tokens, :output_reserve, :truncation_strategy
18
+
19
+ # @param max_tokens [Integer] maximum context window tokens
20
+ # @param output_reserve [Integer] tokens reserved for output
21
+ # @param truncation_strategy [String] one of {TRUNCATION_STRATEGIES}
22
+ def initialize(max_tokens: DEFAULT_MAX_TOKENS, output_reserve: DEFAULT_RESERVE,
23
+ truncation_strategy: 'tail')
24
+ validate_strategy!(truncation_strategy)
25
+
26
+ @max_tokens = max_tokens
27
+ @output_reserve = output_reserve
28
+ @truncation_strategy = truncation_strategy.freeze
29
+ end
30
+
31
+ # @return [Integer] effective input tokens (max minus reserve)
32
+ def effective_input_tokens
33
+ @max_tokens - @output_reserve
34
+ end
35
+
36
+ # @return [Hash] serializable representation
37
+ def to_h
38
+ {
39
+ 'max_tokens' => @max_tokens,
40
+ 'output_reserve' => @output_reserve,
41
+ 'truncation_strategy' => @truncation_strategy
42
+ }
43
+ end
44
+
45
+ private
46
+
47
+ def validate_strategy!(strategy)
48
+ return if TRUNCATION_STRATEGIES.include?(strategy)
49
+
50
+ raise ArgumentError,
51
+ "Invalid truncation strategy '#{strategy}'. Allowed: #{TRUNCATION_STRATEGIES.join(', ')}"
52
+ end
53
+ end
54
+ end
55
+ end