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,50 @@
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 Retrofit
8
+ module Parsers
9
+ # Retrofit parser for AGENTS.md files.
10
+ #
11
+ # Discovers and parses AGENTS.md in the project root into
12
+ # a structured Rosett-AI YAML representation.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class AgentsMdParser < BaseParser
17
+ def engine_name
18
+ 'agents_md'
19
+ end
20
+
21
+ def discover
22
+ project_root = Pathname.new(ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd))
23
+ agents_md = project_root.join('AGENTS.md')
24
+
25
+ return [agents_md] if agents_md.exist? && safe_path?(agents_md)
26
+
27
+ []
28
+ end
29
+
30
+ def parse(path)
31
+ validate_path!(path)
32
+ content = File.read(path)
33
+
34
+ {
35
+ data: { 'agents_md_content' => content },
36
+ unknown_keys: [],
37
+ source_file: path.to_s,
38
+ scope: 'project'
39
+ }
40
+ end
41
+
42
+ private
43
+
44
+ def validate_path!(path)
45
+ raise RosettAi::RetrofitError, "Path not safe: #{path}" unless safe_path?(path)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,69 @@
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 'json'
7
+
8
+ module RosettAi
9
+ module Retrofit
10
+ module Parsers
11
+ # Retrofit parser for Claude Code configuration files.
12
+ #
13
+ # Discovers and parses ~/.claude/settings.json and project-level
14
+ # .claude/settings.json into Rosett-AI YAML structure.
15
+ #
16
+ # @author hugo
17
+ # @author claude
18
+ class ClaudeParser < BaseParser
19
+ KNOWN_KEYS = [
20
+ 'permissions', 'allowedTools', 'deniedTools', 'trustedTools', 'env', 'hooks', 'mcpServers', 'customApiKeyResponses', 'contextProviders', 'preferences'
21
+ ].freeze
22
+
23
+ def engine_name
24
+ 'claude'
25
+ end
26
+
27
+ def discover
28
+ paths = []
29
+ global = Pathname.new(Dir.home).join('.claude', 'settings.json')
30
+ paths << global if global.exist? && safe_path?(global)
31
+
32
+ local = Pathname.new(ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd)).join('.claude', 'settings.json')
33
+ paths << local if local.exist? && safe_path?(local) && local != global
34
+
35
+ paths
36
+ end
37
+
38
+ def parse(path)
39
+ validate_path!(path)
40
+ content = File.read(path)
41
+ data = JSON.parse(content)
42
+
43
+ unknown_keys = data.keys.reject { |key| KNOWN_KEYS.include?(key) }
44
+
45
+ {
46
+ data: data,
47
+ unknown_keys: unknown_keys.map { |key| sanitize(key) },
48
+ source_file: path.to_s,
49
+ scope: detect_scope(path)
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def validate_path!(path)
56
+ raise RosettAi::RetrofitError, "Path not safe: #{path}" unless safe_path?(path)
57
+ end
58
+
59
+ def detect_scope(path)
60
+ if path.to_s.start_with?(Dir.home)
61
+ 'user'
62
+ else
63
+ 'project'
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'json'
7
+
8
+ module RosettAi
9
+ module Retrofit
10
+ module Parsers
11
+ # Retrofit parser for Cursor configuration files.
12
+ #
13
+ # Discovers and parses .cursor/settings.json and .cursorrules
14
+ # into Rosett-AI YAML structure.
15
+ #
16
+ # @author hugo
17
+ # @author claude
18
+ class CursorParser < BaseParser
19
+ KNOWN_KEYS = [
20
+ 'rules', 'model', 'temperature', 'maxTokens', 'contextFiles', 'ignoreFiles'
21
+ ].freeze
22
+
23
+ def engine_name
24
+ 'cursor'
25
+ end
26
+
27
+ def discover
28
+ paths = []
29
+ project_root = Pathname.new(ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd))
30
+
31
+ settings = project_root.join('.cursor', 'settings.json')
32
+ paths << settings if settings.exist? && safe_path?(settings)
33
+
34
+ rules = project_root.join('.cursorrules')
35
+ paths << rules if rules.exist? && safe_path?(rules)
36
+
37
+ paths
38
+ end
39
+
40
+ def parse(path)
41
+ validate_path!(path)
42
+
43
+ if path.extname == '.json'
44
+ parse_json(path)
45
+ else
46
+ parse_rules(path)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def validate_path!(path)
53
+ raise RosettAi::RetrofitError, "Path not safe: #{path}" unless safe_path?(path)
54
+ end
55
+
56
+ def parse_json(path)
57
+ content = File.read(path)
58
+ data = JSON.parse(content)
59
+ unknown_keys = data.keys.reject { |key| KNOWN_KEYS.include?(key) }
60
+
61
+ {
62
+ data: data,
63
+ unknown_keys: unknown_keys.map { |key| sanitize(key) },
64
+ source_file: path.to_s,
65
+ scope: 'project'
66
+ }
67
+ end
68
+
69
+ def parse_rules(path)
70
+ content = File.read(path)
71
+
72
+ {
73
+ data: { 'rules' => content },
74
+ unknown_keys: [],
75
+ source_file: path.to_s,
76
+ scope: 'project'
77
+ }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,65 @@
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 Retrofit
8
+ # Validates that retrofit output round-trips through compilation.
9
+ #
10
+ # Compares the original native config with the result of compiling
11
+ # the retrofitted YAML to verify semantic equivalence.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class RoundTripValidator
16
+ # @param original [Hash] parsed original native config
17
+ # @param compiled [Hash] parsed output of compiling retrofitted YAML
18
+ # @return [Hash] validation result with :valid and :differences keys
19
+ def validate(original:, compiled:)
20
+ differences = find_differences(original, compiled)
21
+
22
+ {
23
+ valid: differences.empty?,
24
+ differences: differences
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def find_differences(original, compiled, path = '')
31
+ diffs = []
32
+ compare_original_keys(original, compiled, path, diffs)
33
+ find_added_keys(original, compiled, path, diffs)
34
+ diffs
35
+ end
36
+
37
+ def compare_original_keys(original, compiled, path, diffs)
38
+ original.each do |key, orig_val|
39
+ full_path = build_path(path, key)
40
+ comp_val = compiled[key]
41
+
42
+ if comp_val.nil?
43
+ diffs << { path: full_path, type: 'missing', original: orig_val }
44
+ elsif orig_val.is_a?(Hash) && comp_val.is_a?(Hash)
45
+ diffs.concat(find_differences(orig_val, comp_val, full_path))
46
+ elsif orig_val != comp_val
47
+ diffs << { path: full_path, type: 'changed', original: orig_val, compiled: comp_val }
48
+ end
49
+ end
50
+ end
51
+
52
+ def find_added_keys(original, compiled, path, diffs)
53
+ compiled.each_key do |key|
54
+ next if original.key?(key)
55
+
56
+ diffs << { path: build_path(path, key), type: 'added', compiled: compiled[key] }
57
+ end
58
+ end
59
+
60
+ def build_path(path, key)
61
+ path.empty? ? key.to_s : "#{path}.#{key}"
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,47 @@
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 Retrofit
8
+ # MCP-facing facade for retrofit operations.
9
+ #
10
+ # Delegates to {Engine} for the actual retrofit logic, providing
11
+ # the simplified interface expected by {Mcp::Tools::RetrofitTool}.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class Scanner
16
+ def initialize(output_dir: Pathname.new(Dir.pwd))
17
+ @output_dir = output_dir
18
+ end
19
+
20
+ # @return [Array<String>] available parser/engine names
21
+ def available_parsers
22
+ Engine.available_engines
23
+ end
24
+
25
+ # Scans for native AI tool configs without writing files.
26
+ #
27
+ # @return [Array<Hash>] scan findings
28
+ def scan
29
+ engine = Engine.new(output_dir: @output_dir)
30
+ engine.simulate[:generated]
31
+ end
32
+
33
+ # Converts native configs to Rosett-AI YAML.
34
+ #
35
+ # @param engine [String, nil] specific engine name
36
+ # @param options [Hash] :dry_run for simulation mode
37
+ # @return [Hash] with :converted and :files keys
38
+ def convert(engine: nil, options: {})
39
+ dry_run = options.fetch(:dry_run, true)
40
+ engines = engine ? [engine] : nil
41
+ retrofitter = Engine.new(output_dir: @output_dir, engines: engines)
42
+ result = dry_run ? retrofitter.simulate : retrofitter.retrofit
43
+ { converted: result[:generated].size, files: result[:generated] }
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,87 @@
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 Retrofit
8
+ # Detects sensitive values in parsed configuration data and replaces
9
+ # them with ${secret:env:NAME} references.
10
+ #
11
+ # Recognises API keys, tokens, passwords, and other secrets by
12
+ # key name patterns and value heuristics (length, entropy, prefixes).
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class SecretDetector
17
+ SENSITIVE_KEY_PATTERNS = [
18
+ /api[_-]?key/i,
19
+ /secret/i,
20
+ /token/i,
21
+ /password/i,
22
+ /credential/i,
23
+ /auth/i,
24
+ /private[_-]?key/i
25
+ ].freeze
26
+
27
+ SECRET_VALUE_PREFIXES = ['sk-', 'pk-', 'ghp_', 'gho_', 'ghs_', 'github_pat_', 'xoxb-', 'xoxp-'].freeze
28
+
29
+ MIN_SECRET_LENGTH = 20
30
+
31
+ # @param warnings [Array<String>] mutable array for warning messages
32
+ def initialize(warnings: [])
33
+ @warnings = warnings
34
+ end
35
+
36
+ # Scans a hash and replaces sensitive values with secret references.
37
+ #
38
+ # @param data [Hash] parsed configuration data
39
+ # @param prefix [String] key path prefix for env var naming
40
+ # @return [Hash] data with secrets replaced
41
+ def redact(data, prefix: '')
42
+ data.each_with_object({}) do |(key, value), result|
43
+ full_key = prefix.empty? ? key.to_s : "#{prefix}_#{key}"
44
+ result[key] = redact_value(key.to_s, value, full_key)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def redact_value(key, value, full_key)
51
+ case value
52
+ when Hash
53
+ redact(value, prefix: full_key)
54
+ when Array
55
+ value.map.with_index { |item, idx| redact_value("#{key}_#{idx}", item, "#{full_key}_#{idx}") }
56
+ when String
57
+ maybe_redact_string(key, value, full_key)
58
+ else
59
+ value
60
+ end
61
+ end
62
+
63
+ def maybe_redact_string(key, value, full_key)
64
+ return value unless sensitive?(key, value)
65
+
66
+ env_name = full_key.upcase.gsub(/[^A-Z0-9]/, '_').squeeze('_')
67
+ @warnings << "Sensitive value detected for '#{key}' — replaced with secret reference. " \
68
+ "Set #{env_name} in your environment."
69
+ "${secret:env:#{env_name}}"
70
+ end
71
+
72
+ def sensitive?(key, value)
73
+ return true if SENSITIVE_KEY_PATTERNS.any? { |pattern| key.match?(pattern) } && value.length >= 8
74
+ return true if SECRET_VALUE_PREFIXES.any? { |prefix| value.start_with?(prefix) }
75
+ return true if value.length >= MIN_SECRET_LENGTH && high_entropy?(value)
76
+
77
+ false
78
+ end
79
+
80
+ def high_entropy?(value)
81
+ chars = value.chars
82
+ unique_ratio = chars.uniq.size.to_f / chars.size
83
+ unique_ratio > 0.6 && value.match?(/[a-zA-Z]/) && value.match?(/[0-9]/)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,71 @@
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
+ # Resolves secrets through an ordered fallback chain.
8
+ # Resolution order: ENV variable > secrets file (0600).
9
+ # System keyring support planned for GUI phase (P3).
10
+ module SecretsResolver
11
+ SAFE_PERMISSIONS = 0o600
12
+
13
+ module_function
14
+
15
+ # Resolves a secret through the fallback chain (ENV > secrets file).
16
+ #
17
+ # @param env_key [String] the environment variable name to look up
18
+ # @return [String] the resolved secret value
19
+ # @raise [RosettAi::Error] if the key is not found in any source
20
+ def resolve(env_key)
21
+ from_env(env_key) || from_secrets_file(env_key) || raise_missing(env_key)
22
+ end
23
+
24
+ # Reads a secret from the environment.
25
+ #
26
+ # @param env_key [String] the environment variable name
27
+ # @return [String, nil] the value, or nil if unset or empty
28
+ def from_env(env_key)
29
+ value = ENV.fetch(env_key, nil)
30
+ value unless value.nil? || value.empty?
31
+ end
32
+
33
+ # Reads a secret from the secrets YAML file.
34
+ #
35
+ # @param env_key [String] the key to look up in the secrets file
36
+ # @return [String, nil] the value, or nil if the file is missing or key absent
37
+ def from_secrets_file(env_key)
38
+ secrets_path = RosettAi.paths.secrets_file.to_s
39
+ return nil unless File.exist?(secrets_path)
40
+
41
+ validate_permissions!(secrets_path)
42
+ data = RosettAi::YamlLoader.load_file(secrets_path)
43
+ return nil unless data.is_a?(Hash)
44
+
45
+ data[env_key]
46
+ end
47
+
48
+ # Validates that the file has strict 0600 permissions.
49
+ #
50
+ # @param path [String] absolute path to the file
51
+ # @return [void]
52
+ # @raise [RosettAi::Error] if the file permissions are not 0600
53
+ def validate_permissions!(path)
54
+ mode = File.stat(path).mode & 0o777
55
+ return if mode == SAFE_PERMISSIONS
56
+
57
+ raise RosettAi::Error,
58
+ "Secrets file #{path} has insecure permissions #{format('%04o', mode)}. " \
59
+ "Expected 0600. Fix with: chmod 600 #{path}"
60
+ end
61
+
62
+ def raise_missing(env_key)
63
+ secrets_path = RosettAi.paths.secrets_file
64
+ raise RosettAi::Error, <<~MSG.chomp
65
+ #{env_key} not found. Set it via one of:
66
+ 1. export #{env_key}=<value>
67
+ 2. Add '#{env_key}: <value>' to #{secrets_path} (chmod 600)
68
+ MSG
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,83 @@
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 SmartFeedback
8
+ # Provides "Did you mean?" suggestions using Levenshtein distance.
9
+ #
10
+ # Matches mistyped commands, subcommands, and flags against known
11
+ # candidates. Returns up to MAX_SUGGESTIONS closest matches within
12
+ # the distance threshold.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ class Suggester
17
+ MAX_SUGGESTIONS = 3
18
+ COMMAND_THRESHOLD = 2
19
+ FLAG_THRESHOLD = 3
20
+
21
+ # Find suggestion matches for a given input.
22
+ #
23
+ # @param input [String] the mistyped word
24
+ # @param candidates [Array<String>] valid options
25
+ # @param threshold [Integer] maximum edit distance
26
+ # @return [Array<String>] sorted suggestions (closest first)
27
+ def suggest(input, candidates, threshold: COMMAND_THRESHOLD)
28
+ input = input.to_s.downcase
29
+ matches = candidates.filter_map do |candidate|
30
+ candidate = candidate.to_s
31
+ dist = levenshtein(input, candidate.downcase)
32
+ [candidate, dist] if dist <= threshold
33
+ end
34
+
35
+ matches.sort_by(&:last)
36
+ .first(MAX_SUGGESTIONS)
37
+ .map(&:first)
38
+ end
39
+
40
+ # Format suggestion message.
41
+ #
42
+ # @param type [String] 'command', 'subcommand', or 'option'
43
+ # @param input [String] the mistyped word
44
+ # @param suggestions [Array<String>] suggested alternatives
45
+ # @return [String] formatted message
46
+ def format_message(type, input, suggestions)
47
+ if suggestions.empty?
48
+ "Unknown #{type} '#{input}'. Run `raictl help` for available #{type}s."
49
+ elsif suggestions.size == 1
50
+ "Unknown #{type} '#{input}'. Did you mean `#{suggestions.first}`?"
51
+ else
52
+ list = suggestions.map { |s| "`#{s}`" }.join(', ')
53
+ "Unknown #{type} '#{input}'. Did you mean one of: #{list}?"
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Compute Levenshtein edit distance between two strings.
60
+ #
61
+ # @param str_a [String] first string
62
+ # @param str_b [String] second string
63
+ # @return [Integer] edit distance
64
+ def levenshtein(str_a, str_b) # rubocop:disable Metrics/AbcSize -- Wagner-Fischer algorithm is inherently branchy
65
+ return str_b.length if str_a.empty?
66
+ return str_a.length if str_b.empty?
67
+
68
+ matrix = Array.new(str_a.length + 1) { |i| i }
69
+ (1..str_b.length).each do |j|
70
+ prev = matrix[0]
71
+ matrix[0] = j
72
+ (1..str_a.length).each do |i|
73
+ temp = matrix[i]
74
+ cost = str_a[i - 1] == str_b[j - 1] ? 0 : 1
75
+ matrix[i] = [matrix[i] + 1, matrix[i - 1] + 1, prev + cost].min
76
+ prev = temp
77
+ end
78
+ end
79
+ matrix[str_a.length]
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,84 @@
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 SmartFeedback
8
+ # Middleware that intercepts Thor::UndefinedCommandError and
9
+ # Thor::UnknownArgumentError to provide "Did you mean?" suggestions.
10
+ #
11
+ # Integrates with Thor's error handling to enhance error messages
12
+ # without replacing the standard error flow.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ module ThorMiddleware
17
+ # Enhance a Thor CLI class with smart feedback.
18
+ #
19
+ # @param cli_class [Class] the Thor CLI class to enhance
20
+ def self.install(cli_class)
21
+ cli_class.singleton_class.prepend(CommandDispatch)
22
+ end
23
+
24
+ # Prepended module that wraps Thor command dispatch with smart feedback.
25
+ module CommandDispatch
26
+ def dispatch(meth, given_args, given_opts, config)
27
+ super
28
+ rescue ::Thor::UndefinedCommandError => e
29
+ handle_unknown_command(e, given_args)
30
+ rescue ::Thor::UnknownArgumentError => e
31
+ handle_unknown_argument(e)
32
+ end
33
+
34
+ private
35
+
36
+ def handle_unknown_command(error, given_args)
37
+ command_name = given_args&.first.to_s
38
+ candidates = all_commands.keys.reject { |k| k == 'help' }
39
+ suggester = Suggester.new
40
+ suggestions = suggester.suggest(command_name, candidates)
41
+ message = suggester.format_message('command', command_name, suggestions)
42
+ raise ::Thor::Error, message
43
+ rescue ::Thor::Error
44
+ raise
45
+ rescue StandardError
46
+ raise error
47
+ end
48
+
49
+ def handle_unknown_argument(error)
50
+ msg = error.message
51
+ flag_match = msg.match(/Unknown switches ['"]([^'"]+)['"]/) ||
52
+ msg.match(/could not be found.*['"]([^'"]+)['"]/)
53
+
54
+ raise error unless flag_match
55
+
56
+ flag = flag_match[1]
57
+ suggester = Suggester.new
58
+ suggestions = suggester.suggest(
59
+ flag.delete_prefix('--'),
60
+ available_flags,
61
+ threshold: Suggester::FLAG_THRESHOLD
62
+ )
63
+
64
+ raise error if suggestions.empty?
65
+
66
+ message = suggester.format_message('option', flag, suggestions.map { |s| "--#{s}" })
67
+ raise ::Thor::Error, message
68
+ rescue ::Thor::Error
69
+ raise
70
+ rescue StandardError
71
+ raise error
72
+ end
73
+
74
+ def available_flags
75
+ all_commands.each_value.flat_map do |cmd|
76
+ next [] unless cmd.respond_to?(:options) && cmd.options
77
+
78
+ cmd.options.keys.map(&:to_s)
79
+ end.uniq
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end