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,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'yaml'
7
+
8
+ module RosettAi
9
+ module Project
10
+ # Handles upstream synchronization for rosett-ai-managed projects.
11
+ #
12
+ # Detects breaking changes when upstream templates are updated and
13
+ # applies updates with user confirmation or --force flag.
14
+ #
15
+ # @author hugo
16
+ # @author claude
17
+ class SyncManager
18
+ # @param project_root [Pathname] root directory of the project
19
+ # @param force [Boolean] apply breaking changes without prompting
20
+ def initialize(project_root:, force: false)
21
+ @project_root = project_root
22
+ @project_dir = project_root.join('.rosett-ai')
23
+ @force = force
24
+ end
25
+
26
+ # Synchronizes the project with upstream template updates.
27
+ #
28
+ # @return [Hash] result with :updated, :conflicts, :skipped arrays
29
+ def sync
30
+ validate!
31
+ origin = load_origin
32
+ template_name = origin['template']
33
+ template_dir = RosettAi.root.join(TemplateApplier::TEMPLATES_DIR, template_name)
34
+
35
+ unless template_dir.directory?
36
+ return { updated: [], conflicts: [], skipped: [],
37
+ error: 'Template no longer available' }
38
+ end
39
+
40
+ results = { updated: [], conflicts: [], skipped: [] }
41
+
42
+ Dir.glob(template_dir.join('**', '*').to_s).each do |tmpl_path|
43
+ tmpl = Pathname.new(tmpl_path)
44
+ next if tmpl.directory?
45
+
46
+ relative = tmpl.relative_path_from(template_dir)
47
+ dest = @project_dir.join(relative)
48
+
49
+ sync_file(tmpl, dest, relative, results)
50
+ end
51
+
52
+ update_origin_timestamp(origin) unless results[:updated].empty?
53
+ results
54
+ end
55
+
56
+ # Preview what sync would change without applying.
57
+ #
58
+ # @return [Hash] same structure as sync but without writing files
59
+ def simulate
60
+ validate!
61
+ origin = load_origin
62
+ template_name = origin['template']
63
+ template_dir = RosettAi.root.join(TemplateApplier::TEMPLATES_DIR, template_name)
64
+
65
+ return { updated: [], conflicts: [], skipped: [] } unless template_dir.directory?
66
+
67
+ results = { updated: [], conflicts: [], skipped: [] }
68
+
69
+ Dir.glob(template_dir.join('**', '*').to_s).each do |tmpl_path|
70
+ tmpl = Pathname.new(tmpl_path)
71
+ next if tmpl.directory?
72
+
73
+ relative = tmpl.relative_path_from(template_dir)
74
+ dest = @project_dir.join(relative)
75
+
76
+ classify_change(tmpl, dest, relative, results)
77
+ end
78
+
79
+ results
80
+ end
81
+
82
+ private
83
+
84
+ def validate!
85
+ raise RosettAi::ProjectError, 'Project .rosett-ai/ directory not found' unless @project_dir.directory?
86
+
87
+ origin_file = @project_dir.join('.template_origin.yml')
88
+ return if origin_file.exist?
89
+
90
+ raise RosettAi::ProjectError,
91
+ 'No template origin found. Run rai project apply-template first.'
92
+ end
93
+
94
+ def load_origin
95
+ YAML.safe_load(@project_dir.join('.template_origin.yml').read)
96
+ end
97
+
98
+ def sync_file(tmpl, dest, relative, results)
99
+ unless dest.exist?
100
+ dest.dirname.mkpath
101
+ FileUtils.cp(tmpl.to_s, dest.to_s)
102
+ results[:updated] << relative.to_s
103
+ return
104
+ end
105
+
106
+ return if files_identical?(tmpl, dest)
107
+
108
+ if @force
109
+ FileUtils.cp(tmpl.to_s, dest.to_s)
110
+ results[:updated] << relative.to_s
111
+ else
112
+ results[:conflicts] << relative.to_s
113
+ end
114
+ end
115
+
116
+ def classify_change(tmpl, dest, relative, results)
117
+ unless dest.exist?
118
+ results[:updated] << relative.to_s
119
+ return
120
+ end
121
+
122
+ return if files_identical?(tmpl, dest)
123
+
124
+ results[:conflicts] << relative.to_s
125
+ end
126
+
127
+ def files_identical?(file_a, file_b)
128
+ Digest::SHA256.file(file_a.to_s).hexdigest == Digest::SHA256.file(file_b.to_s).hexdigest
129
+ end
130
+
131
+ def update_origin_timestamp(origin)
132
+ origin['synced_at'] = Time.now.utc.iso8601
133
+ origin['rai_version'] = RosettAi::VERSION
134
+ File.write(@project_dir.join('.template_origin.yml'), YAML.dump(origin))
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ require 'yaml'
7
+ require 'fileutils'
8
+
9
+ module RosettAi
10
+ module Project
11
+ # Applies project templates to populate .rosett-ai/ with starter configs.
12
+ #
13
+ # Templates are directories under conf/templates/ containing behaviour
14
+ # and design YAML files. Application never overwrites user-modified
15
+ # files without explicit --force flag.
16
+ #
17
+ # @author hugo
18
+ # @author claude
19
+ class TemplateApplier
20
+ TEMPLATES_DIR = 'conf/templates'
21
+
22
+ # @param project_root [Pathname] root directory of the project
23
+ # @param template_name [String] name of the template to apply
24
+ # @param force [Boolean] overwrite existing files
25
+ def initialize(project_root:, template_name:, force: false)
26
+ @project_root = project_root
27
+ @template_name = template_name
28
+ @force = force
29
+ @project_dir = project_root.join('.rosett-ai')
30
+ end
31
+
32
+ # Applies the template to the project.
33
+ #
34
+ # @return [Hash] result with :applied, :skipped, :errors arrays
35
+ def apply
36
+ validate!
37
+ results = { applied: [], skipped: [], errors: [] }
38
+
39
+ template_files.each do |src_path|
40
+ relative = src_path.relative_path_from(template_dir)
41
+ dest = @project_dir.join(relative)
42
+ apply_file(src_path, dest, results)
43
+ end
44
+
45
+ record_template_origin
46
+ results
47
+ end
48
+
49
+ # @return [Boolean] true if the template exists
50
+ def template_exists?
51
+ template_dir.directory?
52
+ end
53
+
54
+ # @return [Array<String>] list of available template names
55
+ def self.available_templates
56
+ dir = RosettAi.root.join(TEMPLATES_DIR)
57
+ return [] unless dir.directory?
58
+
59
+ Dir.children(dir.to_s)
60
+ .select { |name| dir.join(name).directory? }
61
+ .sort
62
+ end
63
+
64
+ private
65
+
66
+ def validate!
67
+ raise RosettAi::ProjectError, "Template '#{@template_name}' not found" unless template_exists?
68
+ raise RosettAi::ProjectError, 'Project .rosett-ai/ directory not found' unless @project_dir.directory?
69
+ end
70
+
71
+ def template_dir
72
+ RosettAi.root.join(TEMPLATES_DIR, @template_name)
73
+ end
74
+
75
+ def template_files
76
+ Dir.glob(template_dir.join('**', '*').to_s)
77
+ .map { |path| Pathname.new(path) }
78
+ .reject(&:directory?)
79
+ end
80
+
81
+ def apply_file(src, dest, results)
82
+ dest.dirname.mkpath
83
+
84
+ if dest.exist? && !@force
85
+ results[:skipped] << dest.relative_path_from(@project_root).to_s
86
+ else
87
+ FileUtils.cp(src.to_s, dest.to_s)
88
+ results[:applied] << dest.relative_path_from(@project_root).to_s
89
+ end
90
+ rescue StandardError => e
91
+ results[:errors] << "#{dest}: #{e.message}"
92
+ end
93
+
94
+ def record_template_origin
95
+ origin_file = @project_dir.join('.template_origin.yml')
96
+ data = {
97
+ 'template' => @template_name,
98
+ 'applied_at' => Time.now.utc.iso8601,
99
+ 'rai_version' => RosettAi::VERSION
100
+ }
101
+ File.write(origin_file, YAML.dump(data))
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-License-Identifier: GPL-3.0-only
4
+ # Copyright (C) 2026 Hugo Antonio Sepulveda Manriquez / NeatNerds
5
+
6
+ module RosettAi
7
+ # Detects whether Dir.pwd is inside an rosett-ai-managed project by walking
8
+ # up from the start directory looking for a `.rosett-ai/` marker directory.
9
+ #
10
+ # When found, content commands (design, behaviour, compile) resolve
11
+ # their source paths relative to the project's `.rosett-ai/` directory
12
+ # instead of the Rosett-AI installation root.
13
+ class ProjectContext
14
+ # Directory name that marks the root of an rai project.
15
+ PROJECT_MARKER = '.rosett-ai'
16
+
17
+ # @return [Pathname, nil] absolute path to the project root, or nil if no project detected
18
+ attr_reader :project_root
19
+
20
+ # @param start_dir [String] directory to begin searching from.
21
+ # Defaults to `RAI_ORIGINAL_PWD` env var (set by the `.deb` wrapper
22
+ # to preserve the user's directory), falling back to `Dir.pwd`.
23
+ def initialize(start_dir: ENV.fetch('RAI_ORIGINAL_PWD', Dir.pwd))
24
+ @project_root = detect_project_root(start_dir)
25
+ end
26
+
27
+ # @return [Boolean] true if a `.rosett-ai/` marker was found
28
+ def project?
29
+ !@project_root.nil?
30
+ end
31
+
32
+ # @return [Boolean] true if the detected project is the Rosett-AI installation itself
33
+ # or the Rosett-AI source tree (when running the installed .deb from source)
34
+ def rai_internal?
35
+ @project_root == RosettAi.root || nncc_source_tree?
36
+ end
37
+
38
+ # @return [Pathname] the `.rosett-ai/` directory if inside a project, otherwise `RosettAi.root`
39
+ #
40
+ # When inside the rosett-ai source tree, returns the source tree root (not `.rosett-ai/`)
41
+ # so that `conf/` resolves to the real configuration directory containing
42
+ # behaviour and design YAML files, rather than the empty `.rosett-ai/conf/`
43
+ # placeholder created by `rai init --project`.
44
+ def conf_root
45
+ return @project_root if rai_internal?
46
+
47
+ project? ? @project_root.join(PROJECT_MARKER) : RosettAi.root
48
+ end
49
+
50
+ # @return [Pathname] path to the `conf/` subdirectory for behaviour/design files
51
+ def project_conf_dir
52
+ conf_root.join('conf')
53
+ end
54
+
55
+ private
56
+
57
+ # Detects the Rosett-AI source tree by checking for the gem's main entry
58
+ # point and gemspec. This combination is unique to rosett-ai — no other
59
+ # rosett-ai-managed project will have both files at the project root.
60
+ #
61
+ # Handles the case where the installed .deb package (RosettAi.root = /opt/rosett-ai/app)
62
+ # is run from the Rosett-AI source tree (project_root = ~/git/.../rosett-ai).
63
+ # Without this check, rai_internal? returns false and conf_root resolves
64
+ # to the empty .rosett-ai/conf/ placeholder instead of the real conf/ directory.
65
+ #
66
+ # @return [Boolean]
67
+ def nncc_source_tree?
68
+ return false unless @project_root
69
+
70
+ @project_root.join('lib', 'rosett_ai.rb').exist? &&
71
+ @project_root.join('rosett_ai.gemspec').exist?
72
+ end
73
+
74
+ def detect_project_root(dir)
75
+ path = Pathname.new(dir).expand_path
76
+ path.ascend do |ancestor|
77
+ marker = ancestor.join(PROJECT_MARKER)
78
+ return ancestor if marker.directory?
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,63 @@
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 Provenance
8
+ # A single provenance entry recording AI involvement in a commit.
9
+ #
10
+ # Each entry captures the commit SHA, contributor identity, AI tool used,
11
+ # the AI's role, and which files were involved.
12
+ class Entry
13
+ ALLOWED_ROLES = [
14
+ 'AI-Generated-By',
15
+ 'AI-Co-Author',
16
+ 'AI-Assisted-By',
17
+ 'AI-Reviewed-By'
18
+ ].freeze
19
+
20
+ attr_reader :commit, :contributor, :ai_tool, :ai_role, :files, :timestamp
21
+
22
+ # @param commit [String] git commit SHA
23
+ # @param contributor [String] contributor name and email
24
+ # @param ai_tool [String] AI tool identifier (e.g. "Claude Opus 4.6 (Anthropic)")
25
+ # @param ai_role [String] one of {ALLOWED_ROLES}
26
+ # @param files [Array<Hash>] file-level source references
27
+ # @param timestamp [String, nil] ISO 8601 timestamp (defaults to now)
28
+ # rubocop:disable Metrics/ParameterLists -- provenance entry requires all fields
29
+ def initialize(commit:, contributor:, ai_tool:, ai_role:, files: [], timestamp: nil)
30
+ validate_role!(ai_role)
31
+
32
+ @commit = commit.freeze
33
+ @contributor = contributor.freeze
34
+ @ai_tool = ai_tool.freeze
35
+ @ai_role = ai_role.freeze
36
+ @files = files.freeze
37
+ @timestamp = (timestamp || Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')).freeze
38
+ end
39
+ # rubocop:enable Metrics/ParameterLists
40
+
41
+ # @return [Hash] serializable representation for YAML output
42
+ def to_h
43
+ {
44
+ 'commit' => @commit,
45
+ 'timestamp' => @timestamp,
46
+ 'contributor' => @contributor,
47
+ 'ai_tool' => @ai_tool,
48
+ 'ai_role' => @ai_role,
49
+ 'files' => @files.map { |file| file.is_a?(Hash) ? file : file.to_h }
50
+ }
51
+ end
52
+
53
+ private
54
+
55
+ def validate_role!(role)
56
+ return if ALLOWED_ROLES.include?(role)
57
+
58
+ raise ArgumentError,
59
+ "Invalid AI role '#{role}'. Allowed: #{ALLOWED_ROLES.join(', ')}"
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,32 @@
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 Provenance
8
+ # Represents a file-level source reference within a provenance entry.
9
+ #
10
+ # Tracks which files were involved in an AI contribution and
11
+ # what lines or sections were affected.
12
+ class FileSource
13
+ attr_reader :path, :lines, :source_type
14
+
15
+ # @param path [String] relative path from project root
16
+ # @param lines [String, nil] line range (e.g. "1-50") or nil for whole file
17
+ # @param source_type [String] one of Source::ALLOWED_TYPES
18
+ def initialize(path:, source_type:, lines: nil)
19
+ @path = path.freeze
20
+ @lines = lines&.freeze
21
+ @source_type = source_type.freeze
22
+ end
23
+
24
+ # @return [Hash] serializable representation
25
+ def to_h
26
+ hash = { 'path' => @path, 'source_type' => @source_type }
27
+ hash['lines'] = @lines if @lines
28
+ hash
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
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 Provenance
8
+ # Classifies the source type of an AI contribution.
9
+ #
10
+ # Source types track where AI-generated code drew its knowledge from,
11
+ # enabling license traceability and compliance auditing.
12
+ class Source
13
+ ALLOWED_TYPES = [
14
+ 'library_api',
15
+ 'project_code',
16
+ 'documentation',
17
+ 'pattern',
18
+ 'external_source'
19
+ ].freeze
20
+
21
+ attr_reader :type, :reference, :url
22
+
23
+ # @param type [String] one of {ALLOWED_TYPES}
24
+ # @param reference [String] human-readable description of the source
25
+ # @param url [String, nil] URL for external_source type
26
+ # @raise [ArgumentError] if type is not in {ALLOWED_TYPES}
27
+ # @raise [ArgumentError] if external_source has bare domain URL
28
+ def initialize(type:, reference:, url: nil)
29
+ validate_type!(type)
30
+ validate_url!(type, url)
31
+
32
+ @type = type.freeze
33
+ @reference = reference.freeze
34
+ @url = url&.freeze
35
+ end
36
+
37
+ # @return [Hash] serializable representation
38
+ def to_h
39
+ hash = { 'type' => @type, 'reference' => @reference }
40
+ hash['url'] = @url if @url
41
+ hash
42
+ end
43
+
44
+ private
45
+
46
+ def validate_type!(type)
47
+ return if ALLOWED_TYPES.include?(type)
48
+
49
+ raise ArgumentError,
50
+ "Invalid source type '#{type}'. Allowed: #{ALLOWED_TYPES.join(', ')}"
51
+ end
52
+
53
+ def validate_url!(type, url)
54
+ return unless type == 'external_source'
55
+ return if url&.include?('/') && url.count('/') > 2
56
+
57
+ raise ArgumentError,
58
+ 'external_source requires a specific URL, not a bare domain'
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,153 @@
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 'digest'
7
+ require 'yaml'
8
+
9
+ module RosettAi
10
+ module Provenance
11
+ # Append-only YAML store for `.ai-provenance.yml`.
12
+ #
13
+ # Manages the provenance file lifecycle: creation, appending entries,
14
+ # reading, querying, and hash chain integrity. Entries are never
15
+ # modified or deleted. Each entry includes a SHA-256 hash of the
16
+ # previous entry to create a tamper-evident chain.
17
+ #
18
+ # @author hugo
19
+ # @author claude
20
+ class Store
21
+ FILENAME = '.ai-provenance.yml'
22
+ MAX_FILE_SIZE = 1_048_576 # 1 MB
23
+ SCHEMA_VERSION = 1
24
+ ZERO_HASH = ('0' * 64).freeze
25
+
26
+ attr_reader :root
27
+
28
+ # @param root [Pathname, String] project root directory
29
+ def initialize(root:)
30
+ @root = Pathname.new(root)
31
+ end
32
+
33
+ # @return [Pathname] full path to the provenance file
34
+ def path
35
+ @root.join(FILENAME)
36
+ end
37
+
38
+ # @return [Boolean] true if the provenance file exists
39
+ def exist?
40
+ path.exist?
41
+ end
42
+
43
+ # Creates a new provenance file with version header.
44
+ #
45
+ # @return [Pathname] path to the created file
46
+ # @raise [RosettAi::ProvenanceError] if file already exists
47
+ def init
48
+ raise RosettAi::ProvenanceError, "Provenance file already exists: #{path}" if exist?
49
+
50
+ data = { 'provenance_version' => SCHEMA_VERSION, 'entries' => [] }
51
+ path.write(YAML.dump(data))
52
+ path
53
+ end
54
+
55
+ # Appends an entry to the provenance file with hash chain linkage.
56
+ #
57
+ # @param entry [Entry] provenance entry to append
58
+ # @return [Pathname] path to the updated file
59
+ # @raise [RosettAi::ProvenanceError] if provenance file does not exist
60
+ def append(entry)
61
+ raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?
62
+
63
+ warn_file_size if file_over_limit?
64
+ data = read_data
65
+ entry_hash = chain_hash(data['entries'])
66
+ serialized = entry.to_h.merge('chain_hash' => entry_hash)
67
+ data['entries'] << serialized
68
+ path.write(YAML.dump(data))
69
+ path
70
+ end
71
+
72
+ # Reads and parses the provenance file.
73
+ #
74
+ # @return [Hash] parsed provenance data
75
+ # @raise [RosettAi::ProvenanceError] if provenance file is missing
76
+ def read
77
+ raise RosettAi::ProvenanceError, "Provenance file not found: #{path}" unless exist?
78
+
79
+ read_data
80
+ end
81
+
82
+ # Returns all entries for a specific commit.
83
+ #
84
+ # @param commit_sha [String] full or partial commit SHA
85
+ # @return [Array<Hash>] matching entries
86
+ def entries_for_commit(commit_sha)
87
+ return [] unless exist?
88
+
89
+ read['entries'].select { |entry| entry['commit']&.start_with?(commit_sha) }
90
+ end
91
+
92
+ # Returns all entries that reference a specific file.
93
+ #
94
+ # @param file_path [String] relative file path
95
+ # @return [Array<Hash>] matching entries
96
+ def entries_for_file(file_path)
97
+ return [] unless exist?
98
+
99
+ read['entries'].select do |entry|
100
+ Array(entry['files']).any? { |file| file['path'] == file_path }
101
+ end
102
+ end
103
+
104
+ # @return [Integer] number of entries in the provenance file
105
+ def entry_count
106
+ return 0 unless exist?
107
+
108
+ read['entries'].size
109
+ end
110
+
111
+ # Verifies the integrity of the hash chain.
112
+ #
113
+ # @return [Array<String>] list of integrity violations (empty if valid)
114
+ def verify_chain
115
+ return [] unless exist?
116
+
117
+ entries = read['entries']
118
+ violations = []
119
+ entries.each_with_index do |entry, index|
120
+ expected = chain_hash(entries[0...index])
121
+ actual = entry['chain_hash']
122
+ next if actual == expected
123
+
124
+ violations << "Entry ##{index + 1} (#{entry['commit']}): " \
125
+ "expected #{expected[0, 16]}..., got #{(actual || 'nil')[0, 16]}..."
126
+ end
127
+ violations
128
+ end
129
+
130
+ private
131
+
132
+ def read_data
133
+ YAML.safe_load(path.read, permitted_classes: [Date, Time])
134
+ end
135
+
136
+ def chain_hash(entries)
137
+ return ZERO_HASH if entries.empty?
138
+
139
+ previous = entries.last.reject { |k| k == 'chain_hash' }
140
+ Digest::SHA256.hexdigest(YAML.dump(previous))
141
+ end
142
+
143
+ def file_over_limit?
144
+ exist? && path.size > MAX_FILE_SIZE
145
+ end
146
+
147
+ def warn_file_size
148
+ warn 'Provenance file exceeds 1 MB — run `rai provenance archive` ' \
149
+ 'to move older entries to a dated archive'
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,62 @@
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 Provenance
8
+ # MCP-facing facade for provenance operations.
9
+ #
10
+ # Delegates to {Store} for the actual provenance logic, providing
11
+ # the simplified interface expected by {Mcp::Tools::ProvenanceTool}.
12
+ #
13
+ # @author hugo
14
+ # @author claude
15
+ class Tracker
16
+ # @param root [Pathname, String] project root (defaults to pwd)
17
+ def initialize(root: Dir.pwd)
18
+ @store = Store.new(root: root)
19
+ end
20
+
21
+ # Validates all provenance entries and hash chain integrity.
22
+ #
23
+ # @return [Hash] with :errors key
24
+ def validate
25
+ errors = []
26
+ errors << 'Provenance file not found' unless @store.exist?
27
+ errors.concat(@store.verify_chain) if @store.exist?
28
+ { errors: errors }
29
+ end
30
+
31
+ # Finds provenance entries for a specific commit.
32
+ #
33
+ # @param commit [String] commit SHA
34
+ # @return [Hash, nil] matching entry
35
+ def find_by_commit(commit)
36
+ entries = @store.entries_for_commit(commit)
37
+ entries.first
38
+ end
39
+
40
+ # Finds provenance entries referencing a file.
41
+ #
42
+ # @param file [String] file path
43
+ # @return [Array<Hash>] matching entries
44
+ def find_by_file(file)
45
+ @store.entries_for_file(file)
46
+ end
47
+
48
+ # Returns provenance log, optionally filtered by role.
49
+ #
50
+ # @param role [String, nil] AI role filter
51
+ # @return [Array<Hash>] provenance entries
52
+ def log(role: nil)
53
+ return [] unless @store.exist?
54
+
55
+ entries = @store.read['entries'] || []
56
+ return entries unless role
57
+
58
+ entries.select { |e| e['role'] == role }
59
+ end
60
+ end
61
+ end
62
+ end