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,117 @@
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 Migration
8
+ # Detects configuration files that need migration to the current schema.
9
+ #
10
+ # Scans behaviour YAML, design YAML, and rai config files for
11
+ # deprecated patterns or missing required fields. Returns structured
12
+ # findings that CLI commands can display to the user.
13
+ #
14
+ # @author hugo
15
+ # @author claude
16
+ # @see conf/design/backward_compatibility.yml
17
+ class Detector
18
+ # @return [Pathname] configuration directory to scan
19
+ attr_reader :config_dir
20
+
21
+ # @param config_dir [Pathname, String] configuration root to scan
22
+ def initialize(config_dir:)
23
+ @config_dir = Pathname.new(config_dir)
24
+ end
25
+
26
+ # Scan all config files and return migration findings.
27
+ #
28
+ # @return [Array<Hash{Symbol => String}>] list of findings with
29
+ # :file, :type, :field, and :message keys
30
+ def detect
31
+ findings = []
32
+ findings.concat(scan_behaviour_files)
33
+ findings.concat(scan_design_files)
34
+ findings
35
+ end
36
+
37
+ # Convenience check for whether any migrations are needed.
38
+ #
39
+ # @return [Boolean]
40
+ def migrations_needed?
41
+ detect.any?
42
+ end
43
+
44
+ private
45
+
46
+ def scan_behaviour_files
47
+ behaviour_dir = config_dir.join('behaviour')
48
+ return [] unless behaviour_dir.directory?
49
+
50
+ scan_directory(behaviour_dir, method(:check_behaviour))
51
+ end
52
+
53
+ def scan_design_files
54
+ design_dir = config_dir.join('design')
55
+ return [] unless design_dir.directory?
56
+
57
+ scan_directory(design_dir, method(:check_design))
58
+ end
59
+
60
+ def scan_directory(dir, checker)
61
+ findings = []
62
+ Dir.glob(dir.join('*.yml').to_s).each do |path|
63
+ data = load_yaml(path)
64
+ next unless data.is_a?(Hash)
65
+
66
+ checker.call(path, data, findings)
67
+ end
68
+ findings
69
+ end
70
+
71
+ def check_behaviour(path, data, findings)
72
+ check_missing_field(path, data, 'version', findings)
73
+ check_missing_field(path, data, 'rules', findings)
74
+ check_deprecated_field(path, data, 'target', 'engine', findings)
75
+ end
76
+
77
+ def check_design(path, data, findings)
78
+ check_missing_field(path, data, 'domain', findings)
79
+ check_missing_field(path, data, 'status', findings)
80
+ check_deprecated_field(path, data, 'target', 'engine', findings)
81
+ end
82
+
83
+ def check_missing_field(path, data, field, findings)
84
+ return if data.key?(field)
85
+
86
+ findings << build_finding(
87
+ file: path,
88
+ type: :missing_field,
89
+ field: field,
90
+ message: ::I18n.t('rosett_ai.migration.missing_field', field: field, file: path)
91
+ )
92
+ end
93
+
94
+ def check_deprecated_field(path, data, old_field, new_field, findings)
95
+ return unless data.key?(old_field)
96
+
97
+ findings << build_finding(
98
+ file: path,
99
+ type: :deprecated_field,
100
+ field: old_field,
101
+ message: ::I18n.t('rosett_ai.migration.deprecated_field',
102
+ field: old_field, replacement: new_field, file: path)
103
+ )
104
+ end
105
+
106
+ def build_finding(file:, type:, field:, message:)
107
+ { file: file, type: type, field: field, message: message }
108
+ end
109
+
110
+ def load_yaml(path)
111
+ YAML.safe_load_file(path, permitted_classes: [Date])
112
+ rescue Psych::SyntaxError, Errno::ENOENT
113
+ nil
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,94 @@
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 'fileutils'
7
+
8
+ module RosettAi
9
+ module Migration
10
+ # Migrates legacy ~/.config/nncc/ to ~/.config/rosett-ai/.
11
+ #
12
+ # Steps:
13
+ # 1. Detect ~/.config/nncc/ existence
14
+ # 2. Copy contents to ~/.config/rosett-ai/
15
+ # 3. Rename source to ~/.config/nncc.migrated-<date>/
16
+ # 4. Verify the copy succeeded
17
+ #
18
+ # @author hugo
19
+ # @author claude
20
+ class NnccConfigMigrator
21
+ DEFAULT_SOURCE = '~/.config/nncc'
22
+ DEFAULT_TARGET = '~/.config/rosett-ai'
23
+
24
+ attr_reader :source, :target, :migrated
25
+
26
+ def initialize(source: DEFAULT_SOURCE, target: DEFAULT_TARGET)
27
+ @source = Pathname.new(File.expand_path(source))
28
+ @target = Pathname.new(File.expand_path(target))
29
+ @migrated = []
30
+ end
31
+
32
+ # @return [Boolean] true if legacy config directory exists
33
+ def migration_needed?
34
+ source.directory? && !source.basename.to_s.include?('.migrated-')
35
+ end
36
+
37
+ # Performs the migration.
38
+ #
39
+ # @return [Array<String>] list of migrated relative paths
40
+ def migrate!
41
+ @migrated = []
42
+ return @migrated unless migration_needed?
43
+
44
+ FileUtils.mkdir_p(target)
45
+ copy_tree(source, target)
46
+ mark_source_migrated
47
+ @migrated
48
+ end
49
+
50
+ # Returns a plan of what will be migrated.
51
+ #
52
+ # @return [Array<Hash>] list of files with :path and :size keys
53
+ def plan
54
+ return [] unless migration_needed?
55
+
56
+ source.glob('**/*').select(&:file?).map do |file|
57
+ rel = file.relative_path_from(source).to_s
58
+ { path: rel, size: file.size }
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def copy_tree(src, dst)
65
+ src.children.each do |entry|
66
+ dest_path = dst.join(entry.basename)
67
+ copy_entry(entry, dest_path)
68
+ end
69
+ end
70
+
71
+ def copy_entry(entry, dest_path)
72
+ if entry.directory?
73
+ FileUtils.mkdir_p(dest_path)
74
+ copy_tree(entry, dest_path)
75
+ elsif !dest_path.exist?
76
+ FileUtils.cp(entry.to_s, dest_path.to_s)
77
+ @migrated << entry.relative_path_from(source).to_s
78
+ end
79
+ end
80
+
81
+ def mark_source_migrated
82
+ return if @migrated.empty?
83
+
84
+ date_suffix = Time.now.strftime('%Y%m%d')
85
+ migrated_path = source.parent.join("nncc.migrated-#{date_suffix}")
86
+ return if migrated_path.exist?
87
+
88
+ FileUtils.mv(source.to_s, migrated_path.to_s)
89
+ rescue Errno::EACCES => e
90
+ RosettAi.logger.warn("Cannot rename legacy config dir: #{e.message}")
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,90 @@
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 'fileutils'
7
+
8
+ module RosettAi
9
+ module Migration
10
+ # Migrates legacy .nncc/ project directories to .rosett-ai/.
11
+ #
12
+ # Can operate on a single project root or scan an entire workspace
13
+ # for projects containing .nncc/ directories.
14
+ #
15
+ # @author hugo
16
+ # @author claude
17
+ class NnccProjectMigrator
18
+ attr_reader :results
19
+
20
+ def initialize
21
+ @results = []
22
+ end
23
+
24
+ # Migrate a single project's .nncc/ to .rosett-ai/.
25
+ #
26
+ # @param project_root [Pathname, String] path to project root
27
+ # @return [Hash] result with :path, :status, :message keys
28
+ def migrate_project(project_root)
29
+ root = Pathname.new(project_root)
30
+ perform_migration(root)
31
+ rescue Errno::EACCES => e
32
+ record_result(root, :error, "Permission denied: #{e.message}")
33
+ end
34
+
35
+ # Scan a workspace for projects with .nncc/ and migrate them.
36
+ #
37
+ # @param workspace [Pathname, String] root directory to scan
38
+ # @return [Array<Hash>] results for each project found
39
+ def migrate_workspace(workspace)
40
+ find_nncc_projects(workspace).each { |path| migrate_project(path) }
41
+ @results
42
+ end
43
+
44
+ # Detect projects with .nncc/ in a workspace (dry-run).
45
+ #
46
+ # @param workspace [Pathname, String] root directory to scan
47
+ # @return [Array<String>] list of project paths with .nncc/
48
+ def detect(workspace)
49
+ find_nncc_projects(workspace)
50
+ end
51
+
52
+ private
53
+
54
+ def perform_migration(root)
55
+ nncc_dir = root.join('.nncc')
56
+ rosett_dir = root.join('.rosett-ai')
57
+
58
+ return record_result(root, :skipped, 'No .nncc/ directory found') unless nncc_dir.directory?
59
+ return record_result(root, :skipped, '.rosett-ai/ already exists') if rosett_dir.directory?
60
+
61
+ FileUtils.mv(nncc_dir.to_s, rosett_dir.to_s)
62
+ update_config_yml(rosett_dir)
63
+ record_result(root, :migrated, '.nncc/ renamed to .rosett-ai/')
64
+ end
65
+
66
+ def find_nncc_projects(workspace)
67
+ Dir.glob(Pathname.new(workspace).join('**', '.nncc'), File::FNM_DOTMATCH).map do |nncc_path|
68
+ File.dirname(nncc_path)
69
+ end
70
+ end
71
+
72
+ def record_result(root, status, message)
73
+ result = { path: root.to_s, status: status, message: message }
74
+ @results << result
75
+ result
76
+ end
77
+
78
+ def update_config_yml(rosett_dir)
79
+ config = rosett_dir.join('config.yml')
80
+ return unless config.exist?
81
+
82
+ content = File.read(config)
83
+ updated = content.gsub('.nncc', '.rosett-ai').gsub(/\bnncc\b/, 'rosett-ai')
84
+ return if content == updated
85
+
86
+ File.write(config, updated)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,123 @@
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 'fileutils'
7
+
8
+ module RosettAi
9
+ module Migration
10
+ # Migrates behaviour/schema files from legacy ~/.claude/conf/ to
11
+ # XDG-compliant ~/.config/rosett-ai/conf/.
12
+ #
13
+ # Created after WP#943: global compile scope was reading from the
14
+ # installation directory because init placed files at ~/.claude/conf/
15
+ # instead of the XDG path specified in scope_hierarchy.yml.
16
+ class XdgMigrator
17
+ LEGACY_CONF = File.expand_path('~/.claude/conf')
18
+ MIGRATED_SUFFIX = '.migrated-to-xdg'
19
+
20
+ attr_reader :legacy_dir, :xdg_dir, :migrated
21
+
22
+ def initialize
23
+ @legacy_dir = Pathname.new(LEGACY_CONF)
24
+ @xdg_dir = RosettAi.paths.rai_conf_dir
25
+ @migrated = []
26
+ end
27
+
28
+ # Detects whether migration is needed.
29
+ #
30
+ # @return [Boolean] true if legacy dir has files and XDG dir is empty/missing
31
+ def migration_needed?
32
+ return false unless legacy_dir.join('behaviour').exist?
33
+ return false if legacy_dir.basename.to_s.end_with?(MIGRATED_SUFFIX)
34
+
35
+ legacy_files = Dir.glob(legacy_dir.join('behaviour', '*.yml'))
36
+ return false if legacy_files.empty?
37
+
38
+ xdg_files = xdg_dir.join('behaviour').exist? ? Dir.glob(xdg_dir.join('behaviour', '*.yml')) : []
39
+ xdg_files.empty? || legacy_has_newer_files?(legacy_files, xdg_files)
40
+ end
41
+
42
+ # Performs the migration: copy files, then rename legacy directory.
43
+ #
44
+ # @return [Array<String>] list of migrated file basenames
45
+ def migrate!
46
+ @migrated = []
47
+ FileUtils.mkdir_p(xdg_dir.join('behaviour'))
48
+ FileUtils.mkdir_p(xdg_dir.join('schemas'))
49
+
50
+ migrate_directory('behaviour', '*.yml')
51
+ migrate_directory('schemas', '*.json')
52
+ migrate_file('adopt_redactions.yml')
53
+
54
+ mark_legacy_migrated
55
+ @migrated
56
+ end
57
+
58
+ # Returns a user-facing warning message about the pending migration.
59
+ #
60
+ # @return [String] warning text
61
+ def warning_message
62
+ legacy_count = Dir.glob(legacy_dir.join('behaviour', '*.yml')).size
63
+ <<~MSG.strip
64
+ [MIGRATION] Found #{legacy_count} behaviour file(s) at legacy path:
65
+ #{legacy_dir}/behaviour/
66
+ These will be migrated to XDG-compliant path:
67
+ #{xdg_dir}/behaviour/
68
+ Original files will be preserved at #{legacy_dir}#{MIGRATED_SUFFIX}/
69
+ MSG
70
+ end
71
+
72
+ private
73
+
74
+ def legacy_has_newer_files?(legacy_files, xdg_files)
75
+ xdg_names = xdg_files.map { |f| File.basename(f) }
76
+ legacy_files.any? { |f| !xdg_names.include?(File.basename(f)) }
77
+ end
78
+
79
+ def migrate_directory(subdir, pattern)
80
+ source = legacy_dir.join(subdir)
81
+ return unless source.exist?
82
+
83
+ target_dir = xdg_dir.join(subdir)
84
+ FileUtils.mkdir_p(target_dir)
85
+
86
+ Dir.glob(source.join(pattern)).each do |file|
87
+ basename = File.basename(file)
88
+ target = target_dir.join(basename)
89
+
90
+ if target.exist?
91
+ RosettAi.logger.debug("XDG migration: #{subdir}/#{basename} already exists, skipping")
92
+ next
93
+ end
94
+
95
+ FileUtils.cp(file, target)
96
+ @migrated << "#{subdir}/#{basename}"
97
+ end
98
+ end
99
+
100
+ def migrate_file(filename)
101
+ source = legacy_dir.join(filename)
102
+ return unless source.exist?
103
+
104
+ target = xdg_dir.join(filename)
105
+ return if target.exist?
106
+
107
+ FileUtils.cp(source.to_s, target.to_s)
108
+ @migrated << filename
109
+ end
110
+
111
+ def mark_legacy_migrated
112
+ return if @migrated.empty?
113
+
114
+ migrated_path = Pathname.new("#{legacy_dir}#{MIGRATED_SUFFIX}")
115
+ return if migrated_path.exist?
116
+
117
+ FileUtils.mv(legacy_dir.to_s, migrated_path.to_s)
118
+ rescue Errno::EACCES => e
119
+ RosettAi.logger.warn("Cannot rename legacy conf dir: #{e.message}")
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,108 @@
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 PackageManager
8
+ # APT package manager backend for Debian-based systems.
9
+ #
10
+ # Uses pkexec for privilege escalation (install/remove).
11
+ # All commands use array-form system() for security.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class Apt < Base
16
+ DPKG_QUERY = 'dpkg-query'
17
+ APT_CACHE = 'apt-cache'
18
+ PKEXEC = 'pkexec'
19
+ APT_GET = 'apt-get'
20
+
21
+ # @return [String]
22
+ def backend_name
23
+ 'apt'
24
+ end
25
+
26
+ # @return [Boolean]
27
+ def available?
28
+ system(DPKG_QUERY, '--version', out: File::NULL, err: File::NULL) == true
29
+ end
30
+
31
+ # Install a .deb package via pkexec + apt-get.
32
+ #
33
+ # @param package_name [String]
34
+ # @return [Boolean]
35
+ def install(package_name)
36
+ validate_package_name!(package_name)
37
+ system(PKEXEC, APT_GET, 'install', '-y', package_name, out: File::NULL)
38
+ end
39
+
40
+ # Remove a .deb package via pkexec + apt-get.
41
+ #
42
+ # @param package_name [String]
43
+ # @return [Boolean]
44
+ def remove(package_name)
45
+ validate_package_name!(package_name)
46
+ system(PKEXEC, APT_GET, 'remove', '-y', package_name, out: File::NULL)
47
+ end
48
+
49
+ # Check if a .deb package is installed via dpkg-query.
50
+ #
51
+ # @param package_name [String]
52
+ # @return [Boolean]
53
+ def installed?(package_name)
54
+ validate_package_name!(package_name)
55
+ system(DPKG_QUERY, '-W', '-f', '${Status}', package_name,
56
+ out: File::NULL, err: File::NULL) == true
57
+ end
58
+
59
+ # Search for available .deb packages via apt-cache.
60
+ #
61
+ # @param pattern [String]
62
+ # @return [Array<Hash>]
63
+ def available_packages(pattern)
64
+ validate_package_name!(pattern)
65
+ output = run_command(APT_CACHE, 'search', pattern)
66
+ return [] unless output
67
+
68
+ output.each_line.filter_map do |line|
69
+ name, description = line.strip.split(' - ', 2)
70
+ next unless name && description
71
+
72
+ { name: name.strip, description: description.strip }
73
+ end
74
+ end
75
+
76
+ # Update APT index via pkexec + apt-get.
77
+ #
78
+ # @return [Boolean]
79
+ def update_index
80
+ system(PKEXEC, APT_GET, 'update', out: File::NULL)
81
+ end
82
+
83
+ private
84
+
85
+ # Validate package name to prevent command injection.
86
+ # Debian package names: lowercase letters, digits, +, -, .
87
+ #
88
+ # @param name [String]
89
+ # @raise [PackageManagerError] if invalid
90
+ def validate_package_name!(name)
91
+ return if name.match?(/\A[a-z0-9][a-z0-9.+-]*\z/)
92
+
93
+ raise PackageManagerError, "Invalid package name: #{name}"
94
+ end
95
+
96
+ # Run a command and capture stdout.
97
+ #
98
+ # @return [String, nil] stdout or nil on failure
99
+ def run_command(*)
100
+ require 'open3'
101
+ stdout, _status = Open3.capture2(*)
102
+ stdout
103
+ rescue StandardError
104
+ nil
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,68 @@
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 PackageManager
8
+ # Abstract base class for system package management.
9
+ #
10
+ # Concrete implementations handle apt (Debian) and gem backends.
11
+ # All backends use array-form system() for security.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class Base
16
+ class PackageManagerError < RosettAi::Error; end
17
+
18
+ # Install a package by name.
19
+ #
20
+ # @param package_name [String] package to install
21
+ # @return [Boolean] true if installed successfully
22
+ def install(package_name)
23
+ raise NotImplementedError, "#{self.class}#install must be implemented"
24
+ end
25
+
26
+ # Remove a package by name.
27
+ #
28
+ # @param package_name [String] package to remove
29
+ # @return [Boolean] true if removed successfully
30
+ def remove(package_name)
31
+ raise NotImplementedError, "#{self.class}#remove must be implemented"
32
+ end
33
+
34
+ # Check if a package is installed.
35
+ #
36
+ # @param package_name [String] package to check
37
+ # @return [Boolean]
38
+ def installed?(package_name)
39
+ raise NotImplementedError, "#{self.class}#installed? must be implemented"
40
+ end
41
+
42
+ # Search for available packages matching a pattern.
43
+ #
44
+ # @param pattern [String] search pattern
45
+ # @return [Array<Hash>] matching packages with :name and :version keys
46
+ def available_packages(pattern)
47
+ raise NotImplementedError, "#{self.class}#available_packages must be implemented"
48
+ end
49
+
50
+ # Update the package index.
51
+ #
52
+ # @return [Boolean] true if updated successfully
53
+ def update_index
54
+ raise NotImplementedError, "#{self.class}#update_index must be implemented"
55
+ end
56
+
57
+ # @return [String] backend identifier
58
+ def backend_name
59
+ raise NotImplementedError, "#{self.class}#backend_name must be implemented"
60
+ end
61
+
62
+ # @return [Boolean] true if this backend is available on the system
63
+ def available?
64
+ raise NotImplementedError, "#{self.class}#available? must be implemented"
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,90 @@
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 PackageManager
8
+ # RubyGems package manager backend.
9
+ #
10
+ # Does NOT require privilege escalation. Installs gems into
11
+ # the user's gem directory or the system gem path.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class GemBackend < Base
16
+ GEM_COMMAND = 'gem'
17
+
18
+ # @return [String]
19
+ def backend_name
20
+ 'gem'
21
+ end
22
+
23
+ # @return [Boolean]
24
+ def available?
25
+ defined?(::Gem) == 'constant'
26
+ end
27
+
28
+ # Install a gem.
29
+ #
30
+ # @param package_name [String]
31
+ # @return [Boolean]
32
+ def install(package_name)
33
+ validate_gem_name!(package_name)
34
+ system(GEM_COMMAND, 'install', package_name, '--no-document', out: File::NULL)
35
+ end
36
+
37
+ # Remove a gem.
38
+ #
39
+ # @param package_name [String]
40
+ # @return [Boolean]
41
+ def remove(package_name)
42
+ validate_gem_name!(package_name)
43
+ system(GEM_COMMAND, 'uninstall', package_name, '-x', out: File::NULL)
44
+ end
45
+
46
+ # Check if a gem is installed.
47
+ #
48
+ # @param package_name [String]
49
+ # @return [Boolean]
50
+ def installed?(package_name)
51
+ validate_gem_name!(package_name)
52
+ !Gem::Specification.find_all_by_name(package_name).empty?
53
+ rescue Gem::MissingSpecError
54
+ false
55
+ end
56
+
57
+ # Search for available gems matching a pattern.
58
+ #
59
+ # @param pattern [String]
60
+ # @return [Array<Hash>]
61
+ def available_packages(pattern)
62
+ validate_gem_name!(pattern)
63
+ specs = Gem::Specification.find_all_by_name(/#{Regexp.escape(pattern)}/)
64
+ specs.map { |s| { name: s.name, version: s.version.to_s } }
65
+ rescue Gem::MissingSpecError
66
+ []
67
+ end
68
+
69
+ # No-op for gems (no index to update).
70
+ #
71
+ # @return [Boolean]
72
+ def update_index # rubocop:disable Naming/PredicateMethod -- imperative action (Base interface), not a predicate
73
+ true
74
+ end
75
+
76
+ private
77
+
78
+ # Validate gem name to prevent abuse.
79
+ # Gem names: alphanumeric, hyphens, underscores.
80
+ #
81
+ # @param name [String]
82
+ # @raise [PackageManagerError] if invalid
83
+ def validate_gem_name!(name)
84
+ return if name.match?(/\A[a-zA-Z0-9][a-zA-Z0-9._-]*\z/)
85
+
86
+ raise PackageManagerError, "Invalid gem name: #{name}"
87
+ end
88
+ end
89
+ end
90
+ end