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,139 @@
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
+ # Centralized error handler for CLI commands.
8
+ #
9
+ # Catches +RosettAi::Error+ (and subclasses), formats the error message with
10
+ # what/why/fix structure, and exits with the mapped exit code. Stack traces
11
+ # are only shown when +RAI_LOG_LEVEL=DEBUG+.
12
+ #
13
+ # TTY-aware: uses colour and formatting when stdout is a TTY, plain text
14
+ # when piped.
15
+ #
16
+ # @author hugo
17
+ # @author claude
18
+ # @see ExitCodes
19
+ # @see conf/design/error_handling.yml
20
+ class ErrorHandler
21
+ # Wraps a block, rescuing +RosettAi::Error+ and formatting the output.
22
+ #
23
+ # @param stderr [IO] output stream for errors (default: $stderr)
24
+ # @yield the CLI command block to execute
25
+ # @return [Integer] exit code (0 on success)
26
+ def self.handle(stderr: $stderr, &)
27
+ new(stderr: stderr).handle(&)
28
+ end
29
+
30
+ # @param stderr [IO] output stream for errors
31
+ def initialize(stderr: $stderr)
32
+ @stderr = stderr
33
+ end
34
+
35
+ # Executes the block and handles errors.
36
+ #
37
+ # @yield the CLI command block
38
+ # @return [Integer] exit code
39
+ def handle
40
+ yield
41
+ ExitCodes::SUCCESS
42
+ rescue RosettAi::Error => e
43
+ report(e)
44
+ ExitCodes.for(e)
45
+ end
46
+
47
+ private
48
+
49
+ # Formats and writes the error to stderr.
50
+ #
51
+ # @param error [RosettAi::Error] the caught error
52
+ # @return [void]
53
+ def report(error)
54
+ exit_code = ExitCodes.for(error)
55
+ parts = parse_message(error.message)
56
+
57
+ if tty?
58
+ report_tty(error, parts, exit_code)
59
+ else
60
+ report_plain(error, parts, exit_code)
61
+ end
62
+
63
+ report_backtrace(error) if debug?
64
+ end
65
+
66
+ # Parses a message for what/why/fix structure.
67
+ #
68
+ # Messages can use the convention:
69
+ # "What failed. Why: reason. Fix: suggestion"
70
+ # or just be a plain string.
71
+ #
72
+ # @param message [String]
73
+ # @return [Hash{Symbol => String, nil}]
74
+ def parse_message(message)
75
+ parts = { what: message, why: nil, fix: nil }
76
+
77
+ if message.include?(' Fix: ')
78
+ before_fix, parts[:fix] = message.split(' Fix: ', 2)
79
+ if before_fix.include?(' Why: ')
80
+ parts[:what], parts[:why] = before_fix.split(' Why: ', 2)
81
+ else
82
+ parts[:what] = before_fix
83
+ end
84
+ elsif message.include?(' Why: ')
85
+ parts[:what], parts[:why] = message.split(' Why: ', 2)
86
+ end
87
+
88
+ parts
89
+ end
90
+
91
+ # TTY-formatted error output with colour.
92
+ #
93
+ # @param error [RosettAi::Error]
94
+ # @param parts [Hash]
95
+ # @param exit_code [Integer]
96
+ # @return [void]
97
+ def report_tty(error, parts, exit_code)
98
+ @stderr.puts Rainbow("Error [#{exit_code}]: #{parts[:what]}").red.bright
99
+ @stderr.puts Rainbow(" Why: #{parts[:why]}").yellow if parts[:why]
100
+ @stderr.puts Rainbow(" Fix: #{parts[:fix]}").green if parts[:fix]
101
+ @stderr.puts Rainbow(" Type: #{error.class}").faint
102
+ end
103
+
104
+ # Plain text error output for pipes.
105
+ #
106
+ # @param error [RosettAi::Error]
107
+ # @param parts [Hash]
108
+ # @param exit_code [Integer]
109
+ # @return [void]
110
+ def report_plain(error, parts, exit_code)
111
+ @stderr.puts "Error [#{exit_code}]: #{parts[:what]}"
112
+ @stderr.puts " Why: #{parts[:why]}" if parts[:why]
113
+ @stderr.puts " Fix: #{parts[:fix]}" if parts[:fix]
114
+ @stderr.puts " Type: #{error.class}"
115
+ end
116
+
117
+ # Outputs backtrace when RAI_LOG_LEVEL=DEBUG.
118
+ #
119
+ # @param error [RosettAi::Error]
120
+ # @return [void]
121
+ def report_backtrace(error)
122
+ return unless error.backtrace
123
+
124
+ @stderr.puts ''
125
+ @stderr.puts 'Backtrace (first 10 frames):'
126
+ error.backtrace.first(10).each { |frame| @stderr.puts " #{frame}" }
127
+ end
128
+
129
+ # @return [Boolean] true if stderr is a TTY
130
+ def tty?
131
+ @stderr.respond_to?(:tty?) && @stderr.tty?
132
+ end
133
+
134
+ # @return [Boolean] true if RAI_LOG_LEVEL is DEBUG
135
+ def debug?
136
+ ENV.fetch('RAI_LOG_LEVEL', '').casecmp('debug').zero?
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,76 @@
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
+ # Maps error categories to numeric exit codes for CLI process exit.
8
+ #
9
+ # Exit codes follow Unix conventions:
10
+ # - 0: success
11
+ # - 1: general/runtime error
12
+ # - 2: validation/input error
13
+ # - 3: configuration error
14
+ # - 4: permission/access error
15
+ # - 5: dependency/environment error
16
+ #
17
+ # @author hugo
18
+ # @author claude
19
+ module ExitCodes
20
+ SUCCESS = 0
21
+ GENERAL = 1
22
+ VALIDATION = 2
23
+ CONFIG = 3
24
+ PERMISSION = 4
25
+ DEPENDENCY = 5
26
+
27
+ # Maps each RosettAi error class to its exit code.
28
+ #
29
+ # @return [Hash{Class => Integer}]
30
+ MAPPING = {
31
+ RosettAi::Error => GENERAL,
32
+ RosettAi::ValidationError => VALIDATION,
33
+ RosettAi::BehaviourError => VALIDATION,
34
+ RosettAi::DesignError => VALIDATION,
35
+ RosettAi::CompositionError => VALIDATION,
36
+ RosettAi::CompileError => VALIDATION,
37
+ RosettAi::ConfigurationError => CONFIG,
38
+ RosettAi::AiConfigError => CONFIG,
39
+ RosettAi::PolicyError => CONFIG,
40
+ RosettAi::InitError => CONFIG,
41
+ RosettAi::AdoptError => GENERAL,
42
+ RosettAi::BackupError => GENERAL,
43
+ RosettAi::BuildError => GENERAL,
44
+ RosettAi::DocumentationError => GENERAL,
45
+ RosettAi::ReleaseError => GENERAL,
46
+ RosettAi::ToolingError => VALIDATION,
47
+ RosettAi::LicenseError => CONFIG,
48
+ RosettAi::ContentError => GENERAL,
49
+ RosettAi::ProvenanceError => GENERAL,
50
+ RosettAi::AuthorshipError => GENERAL,
51
+ RosettAi::DoctorError => GENERAL,
52
+ RosettAi::FeatureFlagError => GENERAL,
53
+ RosettAi::ComplianceError => VALIDATION,
54
+ RosettAi::WorkflowError => GENERAL,
55
+ RosettAi::ProjectError => GENERAL,
56
+ RosettAi::RetrofitError => CONFIG,
57
+ RosettAi::GitHooksError => GENERAL,
58
+ RosettAi::McpError => GENERAL
59
+ }.freeze
60
+
61
+ # Resolves the exit code for a given error.
62
+ #
63
+ # Walks the class hierarchy to find the most specific mapping.
64
+ # Falls back to GENERAL (1) for unmapped error classes.
65
+ #
66
+ # @param error [Exception] the error to map
67
+ # @return [Integer] the exit code (0-5)
68
+ def self.for(error)
69
+ klass = error.is_a?(Class) ? error : error.class
70
+ MAPPING.fetch(klass) do
71
+ ancestor = klass.ancestors.find { |ancestor_class| MAPPING.key?(ancestor_class) }
72
+ ancestor ? MAPPING[ancestor] : GENERAL
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,102 @@
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
+ # Environment-variable-based feature gating for experimental features.
8
+ #
9
+ # Setting +RAI_EXPERIMENTAL=workflow,mcp+ enables only the listed features;
10
+ # all others remain hidden. Flags are parsed once at module load time into
11
+ # a frozen Set for O(1) membership checks.
12
+ #
13
+ # @example Check if a feature is enabled
14
+ # RosettAi::FeatureFlags.enabled?(:workflow) # => true if RAI_EXPERIMENTAL includes "workflow"
15
+ #
16
+ # @example Gate a command (raises if not enabled)
17
+ # RosettAi::FeatureFlags.gate!(:workflow)
18
+ #
19
+ # @author hugo
20
+ # @author claude
21
+ module FeatureFlags
22
+ # All recognised experimental feature names. When a feature is promoted
23
+ # to stable, remove it from this set.
24
+ KNOWN_FLAGS = Set['workflow', 'mcp', 'comply', 'retrofit'].freeze
25
+
26
+ # Raised when a gated command is invoked without its feature flag enabled.
27
+ class ExperimentalFeatureError < RosettAi::Error
28
+ # @return [String] the feature name that was not enabled
29
+ attr_reader :feature
30
+
31
+ def initialize(feature)
32
+ @feature = feature.to_s
33
+ super(::I18n.t('rosett_ai.feature_flags.not_enabled',
34
+ feature: @feature,
35
+ env_example: "RAI_EXPERIMENTAL=#{@feature}"))
36
+ end
37
+ end
38
+
39
+ class << self
40
+ # Returns true if the given feature is currently enabled.
41
+ #
42
+ # @param feature [Symbol, String] the feature flag name
43
+ # @return [Boolean]
44
+ def enabled?(feature)
45
+ active_flags.include?(feature.to_s)
46
+ end
47
+
48
+ # Raises {ExperimentalFeatureError} unless the given feature is enabled.
49
+ # Used as a guard in CLI commands that are experimental.
50
+ #
51
+ # @param feature [Symbol, String] the feature flag name
52
+ # @raise [ExperimentalFeatureError] if the feature is not enabled
53
+ # @return [void]
54
+ def gate!(feature)
55
+ return if enabled?(feature)
56
+
57
+ raise ExperimentalFeatureError, feature
58
+ end
59
+
60
+ # Returns the Set of currently active (enabled) flags.
61
+ #
62
+ # @return [Set<String>]
63
+ def all_enabled
64
+ active_flags
65
+ end
66
+
67
+ # Re-parses RAI_EXPERIMENTAL from the environment. Intended for
68
+ # test isolation only.
69
+ #
70
+ # @return [void]
71
+ def reset!
72
+ @active_flags = nil
73
+ end
74
+
75
+ private
76
+
77
+ def active_flags
78
+ @active_flags ||= parse_flags
79
+ end
80
+
81
+ def parse_flags
82
+ raw = ENV.fetch('RAI_EXPERIMENTAL', '')
83
+ return Set.new.freeze if raw.empty?
84
+
85
+ requested = raw.split(',').map(&:strip).reject(&:empty?).to_set
86
+ unknown = requested - KNOWN_FLAGS
87
+
88
+ warn_unknown(unknown) unless unknown.empty?
89
+
90
+ (requested & KNOWN_FLAGS).freeze
91
+ end
92
+
93
+ def warn_unknown(unknown)
94
+ unknown.sort.each do |flag|
95
+ warn ::I18n.t('rosett_ai.feature_flags.unknown_flag',
96
+ flag: flag,
97
+ known: KNOWN_FLAGS.sort.join(', '))
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,33 @@
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
+ # Human-readable formatting for byte sizes and durations.
8
+ # Include in any class that needs to display file sizes or elapsed times.
9
+ module Formatting
10
+ SIZE_UNITS = ['B', 'KB', 'MB', 'GB'].freeze
11
+
12
+ # Formats a byte count as a human-readable string (e.g. "2.0 KB").
13
+ def format_size(bytes)
14
+ unit_index = 0
15
+ size = bytes.to_f
16
+ while size >= 1024 && unit_index < SIZE_UNITS.length - 1
17
+ size /= 1024
18
+ unit_index += 1
19
+ end
20
+ format('%<size>.1f %<unit>s', size: size, unit: SIZE_UNITS[unit_index])
21
+ end
22
+
23
+ # Formats a duration in seconds as a human-readable string.
24
+ # Returns "Xs" for < 60s, "Xm Ys" for >= 60s.
25
+ def format_duration(seconds)
26
+ if seconds >= 60
27
+ format('%<min>dm %<sec>.0fs', min: (seconds / 60).to_i, sec: seconds % 60)
28
+ else
29
+ format('%<sec>.1fs', sec: seconds)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,199 @@
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 'pathname'
7
+
8
+ module RosettAi
9
+ # Checks that gem version references across the codebase are consistent
10
+ # with the resolved versions in Gemfile.lock and the constraints in the gemspec.
11
+ #
12
+ # Parses Gemfile.lock for resolved runtime dependency versions, compares them
13
+ # against gemspec constraints, and scans project files for stale gem version
14
+ # references.
15
+ class GemConsistencyChecker
16
+ EXCLUDED_DIRS = ['vendor', 'tmp', 'coverage', '.git', '.bundle'].freeze
17
+ EXCLUDED_FILES = ['Gemfile.lock', 'Gemfile', 'rosett_ai.gemspec',
18
+ 'spec/rosett_ai/gem_consistency_checker_spec.rb',
19
+ 'spec/examples.txt'].freeze
20
+ EXCLUDED_PREFIXES = ['doc/changes/', 'doc/claude-sessions/', 'conf/design/'].freeze
21
+ EXCLUDED_EXTENSIONS = ['.aux', '.toc', '.out', '.ptc', '.log', '.idx', '.ind',
22
+ '.ilg', '.bbl', '.blg', '.bcf', '.fls', '.json'].freeze
23
+
24
+ attr_reader :results
25
+
26
+ def initialize(project_dir: Dir.pwd)
27
+ @project_dir = Pathname.new(project_dir)
28
+ @results = { gems: [], stale_references: [], consistent: true }
29
+ end
30
+
31
+ def check
32
+ constraints = parse_gemspec_constraints
33
+ locked = parse_lockfile_versions
34
+
35
+ gems = build_gem_audit(constraints, locked)
36
+ dependency_versions = locked.select { |name, _| constraints.key?(name) }
37
+ stale = scan_for_stale_references(dependency_versions)
38
+
39
+ @results = {
40
+ gems: gems,
41
+ stale_references: stale,
42
+ consistent: gems.all? { |entry| entry[:status] == :ok } && stale.empty?
43
+ }
44
+ end
45
+
46
+ def consistent?
47
+ check if @results[:gems].empty? && @results[:stale_references].empty?
48
+ @results[:consistent]
49
+ end
50
+
51
+ private
52
+
53
+ def parse_gemspec_constraints
54
+ gemspec_path = find_gemspec
55
+ return {} unless gemspec_path
56
+
57
+ constraints = {}
58
+ File.readlines(gemspec_path, encoding: 'UTF-8').each do |line|
59
+ match = line.match(/add_dependency\s+['"]([^'"]+)['"]\s*,\s*['"]([^'"]+)['"]/)
60
+ next unless match
61
+
62
+ constraints[match[1]] = match[2]
63
+ end
64
+ constraints
65
+ end
66
+
67
+ def find_gemspec
68
+ pattern = @project_dir.join('*.gemspec')
69
+ files = Dir.glob(pattern)
70
+ files.first
71
+ end
72
+
73
+ def parse_lockfile_versions
74
+ lockfile = @project_dir.join('Gemfile.lock')
75
+ return {} unless lockfile.exist?
76
+
77
+ gem_specs_section = extract_gem_specs(File.readlines(lockfile, encoding: 'UTF-8'))
78
+ parse_specs_lines(gem_specs_section)
79
+ end
80
+
81
+ def extract_gem_specs(lines)
82
+ in_gem = false
83
+ in_specs = false
84
+ specs = []
85
+
86
+ lines.each do |line|
87
+ in_gem = true if line.strip == 'GEM'
88
+ in_specs = true if in_gem && line.strip == 'specs:'
89
+ break if in_gem && in_specs && !line.start_with?(' ')
90
+
91
+ specs << line if in_specs
92
+ end
93
+ specs
94
+ end
95
+
96
+ def parse_specs_lines(lines)
97
+ versions = {}
98
+ lines.each do |line|
99
+ match = line.match(/^\s{4}(\S+)\s+\((\d+\.\d+[\d.]*)\)/)
100
+ versions[match[1]] = match[2] if match
101
+ end
102
+ versions
103
+ end
104
+
105
+ def build_gem_audit(constraints, locked)
106
+ constraints.map do |gem_name, constraint|
107
+ resolved = locked[gem_name]
108
+ status = gem_status(constraint, resolved)
109
+
110
+ { name: gem_name, constraint: constraint, resolved: resolved, status: status }
111
+ end
112
+ end
113
+
114
+ def gem_status(constraint, resolved)
115
+ return :missing unless resolved
116
+
117
+ requirement = Gem::Requirement.new(constraint)
118
+ version = Gem::Version.new(resolved)
119
+ requirement.satisfied_by?(version) ? :ok : :conflict
120
+ end
121
+
122
+ def scan_for_stale_references(locked)
123
+ return [] if locked.empty?
124
+
125
+ stale = []
126
+ project_files.each do |file|
127
+ scan_file_for_gems(file, locked, stale)
128
+ end
129
+ stale
130
+ end
131
+
132
+ def scan_file_for_gems(file, locked, stale)
133
+ relative = Pathname.new(file).relative_path_from(@project_dir).to_s
134
+
135
+ File.readlines(file, encoding: 'UTF-8').each_with_index do |line, index|
136
+ next if constraint_line?(line)
137
+ next if suppressed_line?(line)
138
+
139
+ check_line_for_stale_gems(line, locked, relative, index, stale)
140
+ end
141
+ rescue ArgumentError, Encoding::InvalidByteSequenceError
142
+ # Skip files with encoding issues
143
+ end
144
+
145
+ def check_line_for_stale_gems(line, locked, relative, index, stale)
146
+ locked.each do |gem_name, locked_version|
147
+ pattern = /\b#{Regexp.escape(gem_name)}\b\s+v?(\d+\.\d+[\d.]*)\b/
148
+ line.scan(pattern).each do |match|
149
+ found_version = match[0]
150
+ next if found_version == locked_version
151
+
152
+ stale << {
153
+ file: relative,
154
+ line: index + 1,
155
+ gem: gem_name,
156
+ found: found_version,
157
+ expected: locked_version
158
+ }
159
+ end
160
+ end
161
+ end
162
+
163
+ def project_files
164
+ Dir.glob(@project_dir.join('**', '*'), File::FNM_DOTMATCH)
165
+ .select { |f| File.file?(f) }
166
+ .reject { |f| excluded?(f) }
167
+ .reject { |f| binary?(f) }
168
+ .sort
169
+ end
170
+
171
+ def excluded?(file)
172
+ relative = Pathname.new(file).relative_path_from(@project_dir).to_s
173
+
174
+ return true if EXCLUDED_DIRS.any? { |dir| relative.start_with?("#{dir}/") }
175
+ return true if EXCLUDED_FILES.include?(relative)
176
+ return true if EXCLUDED_PREFIXES.any? { |prefix| relative.start_with?(prefix) }
177
+ return true if EXCLUDED_EXTENSIONS.any? { |ext| relative.end_with?(ext) }
178
+
179
+ false
180
+ end
181
+
182
+ def binary?(file)
183
+ sample = File.read(file, 512)
184
+ return true if sample.nil?
185
+
186
+ sample.include?("\x00")
187
+ rescue ArgumentError
188
+ true
189
+ end
190
+
191
+ def constraint_line?(line)
192
+ line.match?(/[><=!~]{1,2}\s*\d+\.\d+/)
193
+ end
194
+
195
+ def suppressed_line?(line)
196
+ line.include?('rai:no-version-check')
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,86 @@
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 GitHooks
8
+ # Detects existing git hook managers and hook scripts in a repository.
9
+ #
10
+ # Identifies overcommit, husky, lefthook, and plain shell hooks
11
+ # to inform the installer about chaining requirements.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class ChainDetector
16
+ KNOWN_MANAGERS = {
17
+ 'overcommit' => { marker: /overcommit/i, config: '.overcommit.yml' },
18
+ 'husky' => { marker: /husky/i, config: '.husky' },
19
+ 'lefthook' => { marker: /lefthook/i, config: 'lefthook.yml' }
20
+ }.freeze
21
+
22
+ # @param project_root [Pathname] path to the project root
23
+ def initialize(project_root:)
24
+ @project_root = Pathname.new(project_root)
25
+ @hooks_dir = @project_root.join('.git', 'hooks')
26
+ end
27
+
28
+ # Detects which hook managers are present in the project.
29
+ #
30
+ # @return [Array<Hash>] list of detected managers with :name and :type
31
+ def detect
32
+ managers = []
33
+
34
+ KNOWN_MANAGERS.each do |name, info|
35
+ config_path = @project_root.join(info[:config])
36
+ managers << { name: name, type: :config } if config_path.exist?
37
+ end
38
+
39
+ managers
40
+ end
41
+
42
+ # Checks if a specific hook has an existing non-rosett-ai script.
43
+ #
44
+ # @param hook_name [String] the git hook name
45
+ # @return [Hash, nil] info about the existing hook or nil
46
+ def existing_hook(hook_name)
47
+ path = @hooks_dir.join(hook_name)
48
+ return nil unless path.exist?
49
+ return nil if rai_managed?(path)
50
+
51
+ content = File.read(path)
52
+ manager = identify_manager(content)
53
+
54
+ {
55
+ path: path.to_s,
56
+ manager: manager,
57
+ symlink: path.symlink?,
58
+ executable: path.executable?
59
+ }
60
+ end
61
+
62
+ # Checks all supported hooks for existing non-rosett-ai scripts.
63
+ #
64
+ # @return [Hash{String => Hash}] map of hook names to existing hook info
65
+ def all_existing
66
+ Installer::SUPPORTED_HOOKS.each_with_object({}) do |hook, result|
67
+ info = existing_hook(hook)
68
+ result[hook] = info if info
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def rai_managed?(path)
75
+ File.read(path).include?(Installer::RAI_MARKER)
76
+ end
77
+
78
+ def identify_manager(content)
79
+ KNOWN_MANAGERS.each do |name, info|
80
+ return name if content.match?(info[:marker])
81
+ end
82
+ 'unknown'
83
+ end
84
+ end
85
+ end
86
+ end