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,371 @@
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_relative 'gui_logger'
7
+
8
+ module RosettAi
9
+ module Desktop
10
+ # Main GTK4/Adwaita application for rai desktop integration.
11
+ #
12
+ # SAFETY PRINCIPLES:
13
+ # 1. All event handlers are wrapped with safe_action() - NEVER crash on user action
14
+ # 2. D-Bus client is defensive - returns safe defaults, never raises
15
+ # 3. Widget construction is wrapped - missing features degrade gracefully
16
+ # 4. Error dialogs inform users instead of crashing
17
+ #
18
+ # This is a standalone app that communicates with rosett-ai exclusively
19
+ # via D-Bus. It does NOT import Ruby Rosett-AI modules for business logic.
20
+ # All operations go through the DbusClient.
21
+ #
22
+ # Follows GNOME HIG: single-window, instant-apply preferences,
23
+ # headerbar with title and buttons.
24
+ class Gtk4App
25
+ APPLICATION_ID = 'be.neatnerds.rosettai'
26
+ UNAVAILABLE_MESSAGE = 'GTK4 app requires the adwaita gem. Install: gem install adwaita'
27
+
28
+ attr_reader :application, :dbus_client
29
+
30
+ def initialize
31
+ @dbus_client = DbusClient.new
32
+ @window = nil
33
+ @application = nil
34
+ @toast_overlay = nil
35
+ end
36
+
37
+ # Check if GTK4/Adwaita is available.
38
+ #
39
+ # @return [Boolean]
40
+ def self.available?
41
+ require 'adwaita'
42
+ true
43
+ rescue LoadError
44
+ false
45
+ end
46
+
47
+ # Run the GTK4 application.
48
+ # SAFETY: Wrapped in rescue to show error dialog on startup failure.
49
+ #
50
+ # @param _argv [Array<String>] command-line arguments (ignored)
51
+ # @return [Integer] exit code
52
+ def run(_argv = ARGV)
53
+ GuiLogger.info('Application starting', version: safe_version)
54
+ require_adwaita!
55
+
56
+ @application = Adw::Application.new(APPLICATION_ID, Gio::ApplicationFlags::FLAGS_NONE)
57
+ setup_actions
58
+ @application.signal_connect('activate') { safe_action(:on_activate) }
59
+ @application.signal_connect('shutdown') { GuiLogger.info('Application shutdown') }
60
+ GuiLogger.info('Application run loop starting')
61
+ @application.run([])
62
+ rescue StandardError => e
63
+ # Last resort error handling - show dialog if possible, print to stderr
64
+ show_fatal_error("Application failed to start: #{e.message}")
65
+ 1
66
+ end
67
+
68
+ private
69
+
70
+ # Execute a block safely, catching all errors and showing dialog.
71
+ # This is the core safety mechanism for all GUI event handlers.
72
+ #
73
+ # @param action_name [Symbol] name of the action for error reporting
74
+ # @yield the action to execute
75
+ def safe_action(action_name)
76
+ GuiLogger.action(action_name) do
77
+ send(action_name)
78
+ end
79
+ rescue StandardError => e
80
+ show_error_dialog('An error occurred', "#{action_name}: #{e.message}")
81
+ log_error("Action #{action_name} failed", e)
82
+ end
83
+
84
+ def require_adwaita!
85
+ require 'adwaita'
86
+ # Alias Adwaita to Adw for GNOME SDK naming convention
87
+ Object.const_set(:Adw, Adwaita) unless Object.const_defined?(:Adw)
88
+ rescue LoadError
89
+ raise RosettAi::Error, UNAVAILABLE_MESSAGE
90
+ end
91
+
92
+ def on_activate
93
+ @window ||= build_main_window
94
+ @window.present
95
+ end
96
+
97
+ # ========== Window Building ==========
98
+
99
+ def build_main_window
100
+ window = Adw::ApplicationWindow.new(@application)
101
+ window.title = 'NeatNerds Code Companion'
102
+ window.set_default_size(600, 450)
103
+
104
+ # Main vertical box to hold header + content
105
+ main_box = Gtk::Box.new(:vertical, 0)
106
+
107
+ # Header bar (GNOME HIG style)
108
+ header = build_header_bar
109
+ main_box.append(header)
110
+
111
+ # Toast overlay for notifications (wraps scrolled content)
112
+ @toast_overlay = Adw::ToastOverlay.new
113
+
114
+ # Scrolled content area
115
+ scrolled = Gtk::ScrolledWindow.new
116
+ scrolled.hscrollbar_policy = Gtk::PolicyType::NEVER
117
+ scrolled.vscrollbar_policy = Gtk::PolicyType::AUTOMATIC
118
+ scrolled.vexpand = true
119
+
120
+ # Clamp for responsive width (GNOME HIG)
121
+ clamp = Adw::Clamp.new
122
+ clamp.maximum_size = 600
123
+ clamp.tightening_threshold = 400
124
+
125
+ content = build_main_content
126
+ clamp.child = content
127
+ scrolled.child = clamp
128
+
129
+ @toast_overlay.child = scrolled
130
+ main_box.append(@toast_overlay)
131
+
132
+ window.content = main_box
133
+ window
134
+ end
135
+
136
+ def build_header_bar
137
+ header = Adw::HeaderBar.new
138
+ header.title_widget = build_title_widget
139
+
140
+ # Primary menu button (hamburger) on the right
141
+ menu_button = Gtk::MenuButton.new
142
+ menu_button.icon_name = 'open-menu-symbolic'
143
+ menu_button.menu_model = build_app_menu
144
+ # primary= may not exist in older GTK4 versions
145
+ menu_button.primary = true if menu_button.respond_to?(:primary=)
146
+ header.pack_end(menu_button)
147
+
148
+ header
149
+ end
150
+
151
+ def build_title_widget
152
+ Adw::WindowTitle.new('NeatNerds Code Companion', '')
153
+ end
154
+
155
+ def build_main_content
156
+ box = Gtk::Box.new(:vertical, 12)
157
+ box.margin_start = 24
158
+ box.margin_end = 24
159
+ box.margin_top = 24
160
+ box.margin_bottom = 24
161
+
162
+ # Status group
163
+ status_group = build_status_group
164
+ box.append(status_group)
165
+
166
+ # Actions group
167
+ actions_group = build_actions_group
168
+ box.append(actions_group)
169
+
170
+ box
171
+ end
172
+
173
+ def build_status_group
174
+ group = Adw::PreferencesGroup.new
175
+ group.title = 'Service Status'
176
+
177
+ # D-Bus connection status
178
+ dbus_row = Adw::ActionRow.new
179
+ dbus_row.title = 'D-Bus Service'
180
+ dbus_row.subtitle = dbus_status_text
181
+ group.add(dbus_row)
182
+
183
+ # Current context row
184
+ context_row = Adw::ActionRow.new
185
+ context_row.title = 'Current Context'
186
+ context_row.subtitle = current_context_text
187
+ group.add(context_row)
188
+
189
+ # Current focus row
190
+ focus_row = Adw::ActionRow.new
191
+ focus_row.title = 'Active Application'
192
+ focus_row.subtitle = current_focus_text
193
+ group.add(focus_row)
194
+
195
+ group
196
+ end
197
+
198
+ def build_actions_group
199
+ group = Adw::PreferencesGroup.new
200
+ group.title = 'Quick Actions'
201
+
202
+ # Compile button
203
+ compile_row = Adw::ActionRow.new
204
+ compile_row.title = 'Compile Rules'
205
+ compile_row.subtitle = 'Run compilation via D-Bus service'
206
+ compile_row.activatable = true
207
+ compile_row.signal_connect('activated') { safe_action(:on_compile_clicked) }
208
+ compile_row.add_suffix(Gtk::Image.new(icon_name: 'go-next-symbolic'))
209
+ group.add(compile_row)
210
+
211
+ group
212
+ end
213
+
214
+ def build_app_menu
215
+ menu = Gio::Menu.new
216
+ menu.append('Preferences', 'app.preferences')
217
+ menu.append('About', 'app.about')
218
+ menu.append('Quit', 'app.quit')
219
+ menu
220
+ end
221
+
222
+ # ========== Action Setup ==========
223
+
224
+ def setup_actions
225
+ setup_preferences_action
226
+ setup_about_action
227
+ setup_quit_action
228
+ end
229
+
230
+ def setup_preferences_action
231
+ prefs_action = Gio::SimpleAction.new('preferences', nil)
232
+ prefs_action.signal_connect('activate') { safe_action(:show_preferences) }
233
+ @application.add_action(prefs_action)
234
+ end
235
+
236
+ def setup_about_action
237
+ about_action = Gio::SimpleAction.new('about', nil)
238
+ about_action.signal_connect('activate') { safe_action(:show_about) }
239
+ @application.add_action(about_action)
240
+ end
241
+
242
+ def setup_quit_action
243
+ quit_action = Gio::SimpleAction.new('quit', nil)
244
+ quit_action.signal_connect('activate') { @application.quit }
245
+ @application.add_action(quit_action)
246
+ @application.set_accels_for_action('app.quit', ['<Control>q'])
247
+ end
248
+
249
+ # ========== Action Handlers ==========
250
+
251
+ def show_preferences
252
+ dialog = Gtk4Preferences.new(@window, @dbus_client)
253
+ dialog.present
254
+ end
255
+
256
+ def show_about
257
+ about = Adw::AboutWindow.new
258
+ about.transient_for = @window
259
+ about.application_name = 'NeatNerds Code Companion'
260
+ about.application_icon = APPLICATION_ID
261
+ about.version = safe_version
262
+ about.developer_name = 'NeatNerds'
263
+ about.website = 'https://neatnerds.be'
264
+ about.issue_url = 'https://github.com/NeatNerds/rosett-ai/issues'
265
+ about.license_type = Gtk::License::GPL_3_0_ONLY
266
+ about.copyright = '2026 Hugo Antonio Sepulveda Manriquez / NeatNerds'
267
+ about.present
268
+ end
269
+
270
+ def on_compile_clicked
271
+ unless @dbus_client.service_available?
272
+ show_toast('D-Bus service not available. Start with: rai dbus start')
273
+ return
274
+ end
275
+
276
+ result = @dbus_client.compile
277
+ show_toast(result)
278
+ end
279
+
280
+ # ========== Toast & Dialog Helpers ==========
281
+
282
+ def show_toast(message)
283
+ return unless @toast_overlay
284
+
285
+ toast = Adw::Toast.new(message.to_s)
286
+ toast.timeout = 3
287
+ @toast_overlay.add_toast(toast)
288
+ rescue StandardError => e
289
+ log_error('Toast display failed', e)
290
+ end
291
+
292
+ def show_error_dialog(title, message)
293
+ return unless @window
294
+
295
+ dialog = Adw::MessageDialog.new(@window, title, message)
296
+ dialog.add_response('ok', 'OK')
297
+ dialog.default_response = 'ok'
298
+ dialog.present
299
+ rescue StandardError => e
300
+ # If dialog fails, at least log it
301
+ log_error('Error dialog failed', e)
302
+ warn "[rai] #{title}: #{message}"
303
+ end
304
+
305
+ def show_fatal_error(message)
306
+ warn "[rai] FATAL: #{message}"
307
+ # Try to show a basic GTK dialog if possible
308
+ return unless defined?(Gtk) && Gtk.respond_to?(:init)
309
+
310
+ Gtk.init
311
+ dialog = Gtk::MessageDialog.new(
312
+ message: message,
313
+ type: :error,
314
+ buttons: :close
315
+ )
316
+ dialog.run
317
+ dialog.destroy
318
+ rescue StandardError
319
+ # Last resort - just print to stderr
320
+ nil
321
+ end
322
+
323
+ # ========== Status Text Helpers ==========
324
+ # All methods return safe strings, never raise
325
+
326
+ def dbus_status_text
327
+ @dbus_client.service_available? ? 'Connected' : 'Not available'
328
+ rescue StandardError
329
+ 'Unknown'
330
+ end
331
+
332
+ def current_context_text
333
+ return 'Service not running' unless @dbus_client.service_available?
334
+
335
+ status = @dbus_client.status
336
+ status['current_context'] || 'default'
337
+ rescue StandardError
338
+ 'Unknown'
339
+ end
340
+
341
+ def current_focus_text
342
+ return 'Service not running' unless @dbus_client.service_available?
343
+
344
+ app_id, title = @dbus_client.current_focus
345
+ return 'None' if app_id.nil? || app_id.empty?
346
+
347
+ "#{app_id}: #{title}"
348
+ rescue StandardError
349
+ 'Unknown'
350
+ end
351
+
352
+ def safe_version
353
+ defined?(RosettAi::VERSION) ? RosettAi::VERSION : '0.0.0'
354
+ rescue StandardError
355
+ '0.0.0'
356
+ end
357
+
358
+ # ========== Logging ==========
359
+
360
+ def log_error(context, error)
361
+ GuiLogger.error(context, error, component: 'Gtk4App')
362
+
363
+ return unless defined?(RosettAi.logger) && RosettAi.logger
364
+
365
+ RosettAi.logger.error("[Gtk4App] #{context}: #{error.class} - #{error.message}")
366
+ rescue StandardError
367
+ nil
368
+ end
369
+ end
370
+ end
371
+ end
@@ -0,0 +1,331 @@
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_relative 'gui_logger'
7
+
8
+ module RosettAi
9
+ module Desktop
10
+ # Preferences dialog for GTK4 app using AdwPreferencesDialog.
11
+ #
12
+ # SAFETY PRINCIPLES:
13
+ # 1. All callbacks are wrapped with safe_callback() - NEVER crash on user action
14
+ # 2. D-Bus client operations are defensive - always handle failures gracefully
15
+ # 3. Missing libadwaita features degrade gracefully
16
+ # 4. All status queries have rescue blocks
17
+ #
18
+ # Follows GNOME HIG instant-apply pattern - changes are applied
19
+ # immediately via D-Bus, no OK/Apply buttons.
20
+ class Gtk4Preferences
21
+ def initialize(parent_window, dbus_client)
22
+ @parent = parent_window
23
+ @dbus_client = dbus_client
24
+ @dialog = nil
25
+ end
26
+
27
+ # Build and return the preferences dialog.
28
+ # SAFETY: Returns nil on failure instead of raising.
29
+ #
30
+ # @return [Adw::PreferencesDialog, nil]
31
+ def build
32
+ require 'adwaita'
33
+ Object.const_set(:Adw, Adwaita) unless Object.const_defined?(:Adw)
34
+
35
+ # Try PreferencesDialog first (libadwaita 1.4+), fall back to PreferencesWindow
36
+ @dialog = create_preferences_dialog
37
+ return nil unless @dialog
38
+
39
+ safe_add_page(:add_general_page)
40
+ safe_add_page(:add_context_page)
41
+ safe_add_page(:add_advanced_page)
42
+
43
+ @dialog
44
+ rescue StandardError => e
45
+ log_error('Failed to build preferences dialog', e)
46
+ nil
47
+ end
48
+
49
+ # Present the preferences dialog.
50
+ # SAFETY: Silently fails if dialog cannot be built.
51
+ def present
52
+ GuiLogger.action(:present_preferences) do
53
+ build unless @dialog
54
+ return unless @dialog
55
+
56
+ # PreferencesDialog (1.4+) uses present(parent), PreferencesWindow (1.0+) uses present()
57
+ # Use defined? check since PreferencesDialog may not exist in older libadwaita
58
+ if defined?(Adw::PreferencesDialog) && @dialog.is_a?(Adw::PreferencesDialog)
59
+ @dialog.present(@parent)
60
+ else
61
+ @dialog.transient_for = @parent if @dialog.respond_to?(:transient_for=)
62
+ @dialog.present
63
+ end
64
+ end
65
+ rescue StandardError => e
66
+ log_error('Failed to present preferences dialog', e)
67
+ end
68
+
69
+ FALLBACK_ENGINES = [
70
+ { name: 'claude', display_name: 'Claude' },
71
+ { name: 'generic', display_name: 'Generic' },
72
+ { name: 'agents_md', display_name: 'AGENTS.md' }
73
+ ].freeze
74
+
75
+ private
76
+
77
+ # Create preferences dialog with fallback for older libadwaita.
78
+ #
79
+ # @return [Object, nil] PreferencesDialog or PreferencesWindow
80
+ def create_preferences_dialog
81
+ # Try PreferencesDialog first (libadwaita 1.4+)
82
+ if defined?(Adw::PreferencesDialog)
83
+ dialog = Adw::PreferencesDialog.new
84
+ dialog.title = 'Preferences'
85
+ return dialog
86
+ end
87
+
88
+ # Fall back to PreferencesWindow (libadwaita 1.0+)
89
+ if defined?(Adw::PreferencesWindow)
90
+ window = Adw::PreferencesWindow.new
91
+ window.title = 'Preferences'
92
+ return window
93
+ end
94
+
95
+ log_error('No preferences dialog available', StandardError.new('libadwaita too old'))
96
+ nil
97
+ rescue StandardError => e
98
+ log_error('Failed to create preferences dialog', e)
99
+ nil
100
+ end
101
+
102
+ # Safely add a page, catching any errors.
103
+ #
104
+ # @param method_name [Symbol] page builder method to call
105
+ def safe_add_page(method_name)
106
+ send(method_name)
107
+ rescue StandardError => e
108
+ log_error("Failed to add page #{method_name}", e)
109
+ end
110
+
111
+ # Wrap callback execution with error handling.
112
+ #
113
+ # @param name [String] callback name for logging
114
+ # @yield the callback block
115
+ def safe_callback(name)
116
+ GuiLogger.callback(name, component: 'Gtk4Preferences')
117
+ yield
118
+ rescue StandardError => e
119
+ log_error("Callback #{name} failed", e)
120
+ end
121
+
122
+ def add_general_page
123
+ page = Adw::PreferencesPage.new
124
+ page.title = 'General'
125
+ page.icon_name = 'preferences-system-symbolic'
126
+
127
+ # Compilation group
128
+ compile_group = Adw::PreferencesGroup.new
129
+ compile_group.title = 'Compilation'
130
+ compile_group.description = 'Configure rule compilation behaviour'
131
+
132
+ # Verbose mode switch
133
+ verbose_row = create_switch_row(
134
+ 'Verbose Output',
135
+ 'Show detailed compilation messages'
136
+ ) { |active| safe_callback('verbose') { on_verbose_changed(active) } }
137
+ compile_group.add(verbose_row) if verbose_row
138
+
139
+ # Strict mode switch
140
+ strict_row = create_switch_row(
141
+ 'Strict Mode',
142
+ 'Treat warnings as errors'
143
+ ) { |active| safe_callback('strict') { on_strict_changed(active) } }
144
+ compile_group.add(strict_row) if strict_row
145
+
146
+ page.add(compile_group)
147
+
148
+ # Engine group
149
+ engine_group = Adw::PreferencesGroup.new
150
+ engine_group.title = 'Engine'
151
+ engine_group.description = 'Default compilation engine'
152
+
153
+ engine_row = create_combo_row(
154
+ 'Default Engine',
155
+ 'Engine used when not specified',
156
+ engine_display_names
157
+ ) { |index| safe_callback('engine') { on_engine_changed(index) } }
158
+ engine_group.add(engine_row) if engine_row
159
+
160
+ page.add(engine_group)
161
+ @dialog.add(page)
162
+ end
163
+
164
+ def add_context_page
165
+ page = Adw::PreferencesPage.new
166
+ page.title = 'Contexts'
167
+ page.icon_name = 'view-list-symbolic'
168
+
169
+ # Current context group
170
+ context_group = Adw::PreferencesGroup.new
171
+ context_group.title = 'Context Switching'
172
+ context_group.description = 'AI context configuration'
173
+
174
+ # Auto-switch toggle
175
+ auto_switch_row = create_switch_row(
176
+ 'Automatic Context Switching',
177
+ 'Switch context based on focused application'
178
+ ) { |active| safe_callback('auto_switch') { on_auto_switch_changed(active) } }
179
+ context_group.add(auto_switch_row) if auto_switch_row
180
+
181
+ page.add(context_group)
182
+ @dialog.add(page)
183
+ end
184
+
185
+ def add_advanced_page
186
+ page = Adw::PreferencesPage.new
187
+ page.title = 'Advanced'
188
+ page.icon_name = 'applications-engineering-symbolic'
189
+
190
+ # D-Bus group
191
+ dbus_group = Adw::PreferencesGroup.new
192
+ dbus_group.title = 'D-Bus Service'
193
+ dbus_group.description = 'Service connection settings'
194
+
195
+ # Status row
196
+ status_row = Adw::ActionRow.new
197
+ status_row.title = 'Service Status'
198
+ status_row.subtitle = service_status_text
199
+ dbus_group.add(status_row)
200
+
201
+ # Reconnect button
202
+ reconnect_row = Adw::ActionRow.new
203
+ reconnect_row.title = 'Reconnect'
204
+ reconnect_row.subtitle = 'Reconnect to D-Bus service'
205
+ reconnect_row.activatable = true
206
+ reconnect_row.signal_connect('activated') do
207
+ safe_callback('reconnect') { on_reconnect_clicked }
208
+ end
209
+ reconnect_row.add_suffix(Gtk::Image.new(icon_name: 'view-refresh-symbolic'))
210
+ dbus_group.add(reconnect_row)
211
+
212
+ page.add(dbus_group)
213
+ @dialog.add(page)
214
+ end
215
+
216
+ # ========== Widget Factories ==========
217
+ # These handle missing widget types gracefully
218
+
219
+ def create_switch_row(title, subtitle, &block)
220
+ return nil unless defined?(Adw::SwitchRow)
221
+
222
+ row = Adw::SwitchRow.new
223
+ row.title = title
224
+ row.subtitle = subtitle
225
+ row.signal_connect('notify::active') { |r| yield(r.active?) } if block
226
+ row
227
+ rescue StandardError => e
228
+ log_error("Failed to create switch row '#{title}'", e)
229
+ nil
230
+ end
231
+
232
+ def create_combo_row(title, subtitle, options, &block)
233
+ return nil unless defined?(Adw::ComboRow)
234
+
235
+ row = Adw::ComboRow.new
236
+ row.title = title
237
+ row.subtitle = subtitle
238
+ model = Gtk::StringList.new(options)
239
+ row.model = model
240
+ row.signal_connect('notify::selected') { |r| yield(r.selected) } if block
241
+ row
242
+ rescue StandardError => e
243
+ log_error("Failed to create combo row '#{title}'", e)
244
+ nil
245
+ end
246
+
247
+ # ========== Engine Helpers ==========
248
+
249
+ def engine_list
250
+ engines = @dbus_client.list_engines
251
+ engines.empty? ? FALLBACK_ENGINES : engines
252
+ rescue StandardError
253
+ FALLBACK_ENGINES
254
+ end
255
+
256
+ def engine_display_names
257
+ engine_list.map { |e| e[:display_name] || e['display_name'] || e[:name] || e['name'] }
258
+ end
259
+
260
+ def engine_names
261
+ engine_list.map { |e| e[:name] || e['name'] }
262
+ end
263
+
264
+ # ========== Status Helpers ==========
265
+
266
+ def service_status_text
267
+ return 'Not connected' unless @dbus_client.service_available?
268
+
269
+ status = @dbus_client.status
270
+ version = status['version'] || 'unknown'
271
+ "Connected (v#{version})"
272
+ rescue StandardError
273
+ 'Unknown'
274
+ end
275
+
276
+ # ========== Callbacks ==========
277
+ # These are called via safe_callback, so they can safely log/update state
278
+
279
+ def on_verbose_changed(active)
280
+ log_debug("Verbose mode: #{active}")
281
+ @dbus_client.set_config('verbose', active.to_s)
282
+ end
283
+
284
+ def on_strict_changed(active)
285
+ log_debug("Strict mode: #{active}")
286
+ @dbus_client.set_config('strict', active.to_s)
287
+ end
288
+
289
+ def on_engine_changed(index)
290
+ names = engine_names
291
+ engine = names[index] || 'claude'
292
+ log_debug("Engine changed: #{engine}")
293
+ @dbus_client.set_config('default_engine', engine)
294
+ end
295
+
296
+ def on_auto_switch_changed(active)
297
+ log_debug("Auto-switch: #{active}")
298
+ @dbus_client.set_config('auto_switch', active.to_s)
299
+ end
300
+
301
+ def on_reconnect_clicked
302
+ @dbus_client.disconnect
303
+ connected = @dbus_client.connect
304
+ log_debug("Reconnect attempt: #{connected ? 'success' : 'failed'}")
305
+ # Would update status row when we have a reference to it
306
+ end
307
+
308
+ # ========== Logging ==========
309
+
310
+ def log_error(context, error)
311
+ GuiLogger.error(context, error, component: 'Gtk4Preferences')
312
+
313
+ return unless defined?(RosettAi.logger) && RosettAi.logger
314
+
315
+ RosettAi.logger.error("[Gtk4Preferences] #{context}: #{error.class} - #{error.message}")
316
+ rescue StandardError
317
+ nil
318
+ end
319
+
320
+ def log_debug(message)
321
+ GuiLogger.debug(message, component: 'Gtk4Preferences')
322
+
323
+ return unless defined?(RosettAi.logger) && RosettAi.logger
324
+
325
+ RosettAi.logger.debug("[Gtk4Preferences] #{message}")
326
+ rescue StandardError
327
+ nil
328
+ end
329
+ end
330
+ end
331
+ end