parapet-rails 0.1.0

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 (231) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +48 -0
  4. data/Rakefile +7 -0
  5. data/app/assets/config/parapet/grammar-registry.json +52 -0
  6. data/app/assets/images/parapet/icons/activities/component-inspector.svg +4 -0
  7. data/app/assets/images/parapet/icons/activities/coverage-heatmap.svg +3 -0
  8. data/app/assets/images/parapet/icons/activities/discovery-sidebar.svg +3 -0
  9. data/app/assets/images/parapet/icons/activities/environment-toggles.svg +4 -0
  10. data/app/assets/images/parapet/icons/activities/failure-inspector.svg +5 -0
  11. data/app/assets/images/parapet/icons/activities/flaky-history.svg +3 -0
  12. data/app/assets/images/parapet/icons/activities/flight-recorder.svg +5 -0
  13. data/app/assets/images/parapet/icons/activities/health-graph.svg +3 -0
  14. data/app/assets/images/parapet/icons/activities/injection-engine.svg +3 -0
  15. data/app/assets/images/parapet/icons/activities/live-sql-stream.svg +5 -0
  16. data/app/assets/images/parapet/icons/activities/live-web-preview.svg +4 -0
  17. data/app/assets/images/parapet/icons/activities/monaco-editor.svg +5 -0
  18. data/app/assets/images/parapet/icons/activities/progress-area.svg +3 -0
  19. data/app/assets/images/parapet/icons/activities/registry-metadata.svg +5 -0
  20. data/app/assets/images/parapet/icons/activities/seed-inspector.svg +4 -0
  21. data/app/assets/images/parapet/icons/activities/slow-test-bottlenecks.svg +4 -0
  22. data/app/assets/images/parapet/icons/activities/smart-search.svg +4 -0
  23. data/app/assets/images/parapet/icons/activities/snippet-library.svg +4 -0
  24. data/app/assets/images/parapet/icons/activities/terminal.svg +4 -0
  25. data/app/assets/images/parapet/icons/activities/unified-schema-view.svg +4 -0
  26. data/app/assets/images/parapet/icons/activities/visual-comparison.svg +3 -0
  27. data/app/assets/images/parapet/icons/activities/visual-summary-bar.svg +5 -0
  28. data/app/assets/images/parapet/icons/nav/ideas.svg +4 -0
  29. data/app/assets/images/parapet/icons/nav/languages.svg +8 -0
  30. data/app/assets/images/parapet/icons/nav/profile.svg +4 -0
  31. data/app/assets/images/parapet/icons/nav/pulse.svg +7 -0
  32. data/app/assets/images/parapet/icons/nav/settings.svg +4 -0
  33. data/app/assets/images/parapet/icons/nav/state.svg +5 -0
  34. data/app/assets/images/parapet/icons/nav/styles.svg +7 -0
  35. data/app/assets/images/parapet/icons/nav/tests.svg +4 -0
  36. data/app/assets/images/parapet/icons/nav/traffic.svg +12 -0
  37. data/app/assets/javascripts/parapet/activity-card.js +439 -0
  38. data/app/assets/javascripts/parapet/activity-cards.js +291 -0
  39. data/app/assets/javascripts/parapet/activity-registry.js +181 -0
  40. data/app/assets/javascripts/parapet/activity-toggles.js +76 -0
  41. data/app/assets/javascripts/parapet/application.js +37 -0
  42. data/app/assets/javascripts/parapet/card-failure-inspector.js +134 -0
  43. data/app/assets/javascripts/parapet/card-file-editor.js +1722 -0
  44. data/app/assets/javascripts/parapet/card-progress-area.js +297 -0
  45. data/app/assets/javascripts/parapet/card-smart-search.js +114 -0
  46. data/app/assets/javascripts/parapet/card-snippet-library.js +251 -0
  47. data/app/assets/javascripts/parapet/explorer-resizer.js +92 -0
  48. data/app/assets/javascripts/parapet/parapet_qss.js +342 -0
  49. data/app/assets/javascripts/parapet/proficiency-filter.js +53 -0
  50. data/app/assets/javascripts/parapet/sidebar-resizer.js +197 -0
  51. data/app/assets/javascripts/parapet/styles-workbench.js +819 -0
  52. data/app/assets/javascripts/parapet/tree-logic.js +320 -0
  53. data/app/assets/stylesheets/parapet/_qss_variables.scss +122 -0
  54. data/app/assets/stylesheets/parapet/parapet_qss.scss +69 -0
  55. data/app/assets/stylesheets/parapet/qss_application.css +260940 -0
  56. data/app/assets/stylesheets/parapet/qss_application.css.map +1 -0
  57. data/app/assets/stylesheets/parapet/qss_application.scss +5 -0
  58. data/app/assets/stylesheets/parapet/qss_workspace.scss +209 -0
  59. data/app/assets/stylesheets/parapet/sample.qss +22 -0
  60. data/app/controllers/parapet/application_controller.rb +54 -0
  61. data/app/controllers/parapet/base_controller.rb +18 -0
  62. data/app/controllers/parapet/build_suite/dashboard_controller.rb +10 -0
  63. data/app/controllers/parapet/css_suite/compiler_controller.rb +95 -0
  64. data/app/controllers/parapet/css_suite/dashboard_controller.rb +10 -0
  65. data/app/controllers/parapet/css_suite/preview_controller.rb +46 -0
  66. data/app/controllers/parapet/dashboard_controller.rb +8 -0
  67. data/app/controllers/parapet/drafts_controller.rb +30 -0
  68. data/app/controllers/parapet/explorers_controller.rb +53 -0
  69. data/app/controllers/parapet/files_controller.rb +208 -0
  70. data/app/controllers/parapet/footers_controller.rb +24 -0
  71. data/app/controllers/parapet/intentions_suite/dashboard_controller.rb +10 -0
  72. data/app/controllers/parapet/landing_controller.rb +14 -0
  73. data/app/controllers/parapet/monitor_suite/dashboard_controller.rb +10 -0
  74. data/app/controllers/parapet/pm_suite/publish_controller.rb +16 -0
  75. data/app/controllers/parapet/polyglot_suite/dashboard_controller.rb +10 -0
  76. data/app/controllers/parapet/runner_controller.rb +34 -0
  77. data/app/controllers/parapet/sessions_controller.rb +35 -0
  78. data/app/controllers/parapet/signal_suite/dashboard_controller.rb +10 -0
  79. data/app/controllers/parapet/test_suite/dashboard_controller.rb +10 -0
  80. data/app/controllers/parapet/widgets_controller.rb +33 -0
  81. data/app/helpers/parapet/application_helper.rb +23 -0
  82. data/app/helpers/parapet/icon_helper.rb +57 -0
  83. data/app/jobs/parapet/application_job.rb +4 -0
  84. data/app/mailers/parapet/application_mailer.rb +6 -0
  85. data/app/models/parapet/activity.rb +14 -0
  86. data/app/models/parapet/application_record.rb +5 -0
  87. data/app/models/parapet/current.rb +6 -0
  88. data/app/models/parapet/draft_recovery.rb +9 -0
  89. data/app/models/parapet/internal_record.rb +34 -0
  90. data/app/models/parapet/run_history.rb +22 -0
  91. data/app/models/parapet/runtime_setting.rb +8 -0
  92. data/app/models/parapet/session.rb +7 -0
  93. data/app/models/parapet/suite_assignment.rb +30 -0
  94. data/app/models/parapet/user.rb +12 -0
  95. data/app/services/parapet/activity_phrase_renderer.rb +35 -0
  96. data/app/services/parapet/activity_writer.rb +26 -0
  97. data/app/services/parapet/authorization.rb +198 -0
  98. data/app/services/parapet/consistency_auditor.rb +58 -0
  99. data/app/services/parapet/directory_scanner.rb +159 -0
  100. data/app/services/parapet/draft_recovery_store.rb +18 -0
  101. data/app/services/parapet/explorer_manager.rb +402 -0
  102. data/app/services/parapet/file_save_service.rb +21 -0
  103. data/app/services/parapet/git_side_ref_committer.rb +50 -0
  104. data/app/services/parapet/noun_discovery.rb +26 -0
  105. data/app/services/parapet/publish_gate.rb +83 -0
  106. data/app/services/parapet/test_runner.rb +178 -0
  107. data/app/views/layouts/parapet/application.html.erb +83 -0
  108. data/app/views/layouts/parapet/landing.html.erb +19 -0
  109. data/app/views/parapet/activities/_ai_error_explainer.html.erb +33 -0
  110. data/app/views/parapet/activities/_ai_explainer_widget.html.erb +20 -0
  111. data/app/views/parapet/activities/_calendar_widget.html.erb +31 -0
  112. data/app/views/parapet/activities/_code_snippets_widget.html.erb +22 -0
  113. data/app/views/parapet/activities/_coverage_explorer.html.erb +1 -0
  114. data/app/views/parapet/activities/_dropdown_test.html.erb +20 -0
  115. data/app/views/parapet/activities/_explorer_node.html.erb +102 -0
  116. data/app/views/parapet/activities/_explorer_tree.html.erb +16 -0
  117. data/app/views/parapet/activities/_file_editor.html.erb +183 -0
  118. data/app/views/parapet/activities/_file_explorer.html.erb +23 -0
  119. data/app/views/parapet/activities/_fix_suggestions_widget.html.erb +13 -0
  120. data/app/views/parapet/activities/_generic.html.erb +6 -0
  121. data/app/views/parapet/activities/_live_preview.html.erb +13 -0
  122. data/app/views/parapet/activities/_perspective_filters.html.erb +36 -0
  123. data/app/views/parapet/activities/_progress_area.html.erb +37 -0
  124. data/app/views/parapet/activities/_project_explorer.html.erb +1 -0
  125. data/app/views/parapet/activities/_registry_explorer.html.erb +1 -0
  126. data/app/views/parapet/activities/_run_history.html.erb +2 -0
  127. data/app/views/parapet/activities/_smart_search.html.erb +11 -0
  128. data/app/views/parapet/activities/file_explorer/_perspective_scripts.html.erb +58 -0
  129. data/app/views/parapet/activities/file_explorer/_perspectives.html.erb +0 -0
  130. data/app/views/parapet/activities/file_explorer/_viewport_scripts.html.erb +33 -0
  131. data/app/views/parapet/css_suite/_workbench.html.erb +51 -0
  132. data/app/views/parapet/dashboards/index.html.erb +113 -0
  133. data/app/views/parapet/debug_panel.html.erb +79 -0
  134. data/app/views/parapet/debug_panel_tailwind.html.erb +98 -0
  135. data/app/views/parapet/erb_patch_test.html.erb +5 -0
  136. data/app/views/parapet/explorers/show.html.erb +1 -0
  137. data/app/views/parapet/git_viewer.html.erb +62 -0
  138. data/app/views/parapet/git_viewer_after.html.erb +14 -0
  139. data/app/views/parapet/git_viewer_before.html.erb +14 -0
  140. data/app/views/parapet/git_viewer_governed.html.erb +88 -0
  141. data/app/views/parapet/git_viewer_governed_after.html.erb +36 -0
  142. data/app/views/parapet/git_viewer_governed_before.html.erb +36 -0
  143. data/app/views/parapet/git_viewer_tailwind.html.erb +88 -0
  144. data/app/views/parapet/git_viewer_thin_wrapper.html.erb +85 -0
  145. data/app/views/parapet/hierarchical_layout_test.html.erb +85 -0
  146. data/app/views/parapet/hybrid_stress_test.html.erb +25 -0
  147. data/app/views/parapet/index.html.erb +35 -0
  148. data/app/views/parapet/mutation_stress_test.html.erb +82 -0
  149. data/app/views/parapet/recursive_anchoring_test.html.erb +66 -0
  150. data/app/views/parapet/sessions/new.html.erb +28 -0
  151. data/app/views/parapet/shared/_cad_inspector.html.erb +92 -0
  152. data/app/views/parapet/shared/_discovery_sidebar.html.erb +48 -0
  153. data/app/views/parapet/shared/_explorer_bar.html.erb +71 -0
  154. data/app/views/parapet/shared/_footer.html.erb +10 -0
  155. data/app/views/parapet/shared/_footer_ideas.html.erb +36 -0
  156. data/app/views/parapet/shared/_footer_languages.html.erb +33 -0
  157. data/app/views/parapet/shared/_footer_pulse.html.erb +33 -0
  158. data/app/views/parapet/shared/_footer_state.html.erb +33 -0
  159. data/app/views/parapet/shared/_footer_styles.html.erb +37 -0
  160. data/app/views/parapet/shared/_footer_tests.html.erb +39 -0
  161. data/app/views/parapet/shared/_footer_traffic.html.erb +45 -0
  162. data/app/views/parapet/shared/_global_nav.html.erb +55 -0
  163. data/app/views/parapet/shared/_head.html.erb +4 -0
  164. data/app/views/parapet/shared/_header.html.erb +20 -0
  165. data/app/views/parapet/shared/_object_inspector.html.erb +76 -0
  166. data/app/views/parapet/shared/_ribbon_header.html.erb +3 -0
  167. data/app/views/parapet/shared/_root_selector_modal.html.erb +15 -0
  168. data/app/views/parapet/shared/_tab_bar.html.erb +62 -0
  169. data/app/views/parapet/shared/activities_ribbon/_agent_group.html.erb +15 -0
  170. data/app/views/parapet/shared/activities_ribbon/_analysis_group.html.erb +15 -0
  171. data/app/views/parapet/shared/activities_ribbon/_architect_group.html.erb +21 -0
  172. data/app/views/parapet/shared/activities_ribbon/_explorer_group.html.erb +12 -0
  173. data/app/views/parapet/shared/activities_ribbon/_inspector_group.html.erb +15 -0
  174. data/app/views/parapet/shared/activities_ribbon/_proficiency_filter.html.erb +24 -0
  175. data/app/views/parapet/shared/activities_ribbon/_runner_group.html.erb +21 -0
  176. data/app/views/parapet/shared/activities_ribbon/_spatial_inspector_group.html.erb +18 -0
  177. data/app/views/parapet/shared/activities_ribbon/_studio_group.html.erb +18 -0
  178. data/app/views/parapet/shared/activities_ribbon/_stylist_group.html.erb +18 -0
  179. data/app/views/parapet/shared/explorers/_base.html.erb +107 -0
  180. data/app/views/parapet/shared/explorers/_css.html.erb +1 -0
  181. data/app/views/parapet/shared/explorers/_intentions.html.erb +1 -0
  182. data/app/views/parapet/shared/explorers/_project.html.erb +1 -0
  183. data/app/views/parapet/shared/explorers/_tests.html.erb +1 -0
  184. data/app/views/parapet/shared/head/_meta.html.erb +3 -0
  185. data/app/views/parapet/shared/head/_styles_activities.html.erb +152 -0
  186. data/app/views/parapet/shared/head/_styles_shared.html.erb +52 -0
  187. data/app/views/parapet/shared/head/_styles_workspace.html.erb +67 -0
  188. data/app/views/parapet/shared/ribbon_header/_diagnostic_controls.html.erb +17 -0
  189. data/app/views/parapet/shared/ribbon_header/_execution_controls.html.erb +28 -0
  190. data/app/views/parapet/shared/ribbon_header/_focus_search.html.erb +5 -0
  191. data/app/views/parapet/shared/root_selector/_header.html.erb +10 -0
  192. data/app/views/parapet/shared/root_selector/_path_input.html.erb +13 -0
  193. data/app/views/parapet/shared/root_selector/_quick_select.html.erb +20 -0
  194. data/app/views/parapet/shared/root_selector/_scripts.html.erb +61 -0
  195. data/app/views/parapet/shared/root_selector/_warning.html.erb +11 -0
  196. data/app/views/parapet/shared/widgets/_base.html.erb +6 -0
  197. data/app/views/parapet/shared/widgets/_css.html.erb +79 -0
  198. data/app/views/parapet/widgets/_explainer.html.erb +17 -0
  199. data/app/views/parapet/widgets/_hotkeys.html.erb +20 -0
  200. data/app/views/parapet/widgets/_search.html.erb +68 -0
  201. data/app/views/parapet/widgets/_spatial_inspector.html.erb +54 -0
  202. data/app/views/parapet/widgets/_widget-explainer.html.erb +17 -0
  203. data/app/views/parapet/widgets/_widget-hotkeys.html.erb +20 -0
  204. data/app/views/parapet/widgets/_widget-search.html.erb +68 -0
  205. data/app/views/parapet/widgets/_widget-snippets.html.erb +18 -0
  206. data/app/views/parapet/widgets/_widget-suggestions.html.erb +15 -0
  207. data/app/views/parapet/widgets/show.html.erb +3 -0
  208. data/app/views/shared/_footer.html.erb +0 -0
  209. data/config/parapet/qss_config.yml +117 -0
  210. data/config/parapet/qss_registry.yml +14 -0
  211. data/config/routes.rb +40 -0
  212. data/db/migrate/20260916035300_create_run_histories.rb +16 -0
  213. data/db/migrate/20261002000000_expand_run_histories_for_activity_tracking.rb +7 -0
  214. data/db/parapet_migrate/20260929184200_create_parapet_users.rb +12 -0
  215. data/db/parapet_migrate/20260929184300_create_parapet_sessions.rb +9 -0
  216. data/db/parapet_migrate/20260930000000_create_parapet_runtime_settings.rb +12 -0
  217. data/db/parapet_migrate/20260930000100_create_parapet_suite_assignments.rb +14 -0
  218. data/db/parapet_migrate/20261002000000_create_parapet_activities.rb +16 -0
  219. data/db/parapet_migrate/20261002000100_create_parapet_draft_recoveries.rb +13 -0
  220. data/db/parapet_schema.rb +64 -0
  221. data/db/schema.rb +21 -0
  222. data/db/schema.released.rb +0 -0
  223. data/lib/parapet/engine.rb +11 -0
  224. data/lib/parapet/environment_guard.rb +14 -0
  225. data/lib/parapet/version.rb +3 -0
  226. data/lib/parapet-rails.rb +1 -0
  227. data/lib/parapet.rb +59 -0
  228. data/lib/tasks/parapet_qss.rake +26 -0
  229. data/lib/tasks/parapet_tasks.rake +25 -0
  230. data/lib/tasks/randomize_explorer_health.rb +101 -0
  231. metadata +423 -0
@@ -0,0 +1,208 @@
1
+ module Parapet
2
+ class FilesController < ApplicationController
3
+ def index
4
+ # Use Engine.root to find the mock file stored within the engine
5
+ mock_path = Parapet::Engine.root.join("storage", "seeds-project-explorer.json")
6
+
7
+ if File.exist?(mock_path)
8
+ render json: File.read(mock_path)
9
+ else
10
+ # Fallback to real filesystem scanning
11
+ root_path = Parapet::Engine.root
12
+ render json: build_tree(root_path)
13
+ end
14
+ end
15
+
16
+ def show
17
+ raw_path = params[:path].to_s
18
+
19
+ # Security: Get absolute root path
20
+ root_path = File.expand_path(Parapet::Engine.root.to_s)
21
+
22
+ # On Windows, a path may arrive as an absolute path with a drive letter
23
+ if Gem.win_platform? && raw_path.match?(/^[A-Z]:/i)
24
+ full_path = File.expand_path(raw_path)
25
+ else
26
+ # Strip leading slash to ensure it's relative to root_path
27
+ relative_path = raw_path.sub(/^\//, '')
28
+ full_path = File.expand_path(File.join(root_path, relative_path))
29
+ end
30
+
31
+ # Fallback: Deduplication logic for cases like spec/spec/file.rb
32
+ if !File.exist?(full_path)
33
+ # Try to remove duplicated first segment if it exists
34
+ relative_path = raw_path.sub(/^\//, '')
35
+ parts = relative_path.split('/')
36
+ if parts.size > 1 && parts[0] == parts[1]
37
+ alternate_relative = parts[1..-1].join('/')
38
+ alternate_path = File.expand_path(File.join(root_path, alternate_relative))
39
+ if File.exist?(alternate_path)
40
+ Rails.logger.warn "Parapet Explorer: Found file via de-duplication: #{alternate_path}"
41
+ full_path = alternate_path
42
+ end
43
+ end
44
+ end
45
+
46
+ # Final security and existence check
47
+ if full_path.start_with?(root_path)
48
+ # Timing mitigation (Windows/AV suspected; the cause was never demonstrated): retry once
49
+ # if the file is not found. The retry is logged when it actually finds the file, so we
50
+ # learn whether it is ever needed. If that line never appears in the log, remove it.
51
+ exists = File.exist?(full_path)
52
+ if !exists && !File.directory?(full_path)
53
+ sleep 0.05
54
+ exists = File.exist?(full_path)
55
+ Rails.logger.warn "Parapet Files: #{full_path} was not found on the first check but was found on the retry (timing mitigation was needed)" if exists
56
+ end
57
+
58
+ if exists && !File.directory?(full_path)
59
+ content = File.read(full_path)
60
+ render json: { content: content, path: params[:path] }
61
+ else
62
+ render json: {
63
+ content: "# Error: Access Denied or File Not Found\n# Target: #{full_path}\n# Root: #{root_path}",
64
+ path: params[:path]
65
+ }, status: :not_found
66
+ end
67
+ else
68
+ render json: { error: "Access Denied" }, status: :forbidden
69
+ end
70
+ end
71
+
72
+
73
+ def update
74
+ relative_path = params[:path].to_s.sub(/^\//, '')
75
+ root_path = File.expand_path(Parapet::Engine.root.to_s)
76
+ full_path = File.expand_path(File.join(root_path, relative_path))
77
+
78
+ # Security check: Ensure we are within root
79
+ unless full_path.start_with?(root_path)
80
+ render json: { error: "Access Denied" }, status: :forbidden
81
+ return
82
+ end
83
+
84
+ begin
85
+ Parapet::FileSaveService.new(
86
+ user: current_user,
87
+ full_path: full_path,
88
+ relative_path: relative_path,
89
+ content: params[:content]
90
+ ).save!
91
+ render json: {
92
+ status: "success",
93
+ path: params[:path],
94
+ flash: { notice: "File persisted successfully to #{params[:path]}" }
95
+ }
96
+ rescue => e
97
+ render json: {
98
+ error: e.message,
99
+ flash: { alert: "Failed to persist file: #{e.message}" }
100
+ }, status: :internal_server_error
101
+ end
102
+ end
103
+
104
+ def move
105
+ old_relative = params[:old_path].to_s.sub(/^\//, '')
106
+ new_relative = params[:new_path].to_s.sub(/^\//, '')
107
+ root_path = File.expand_path(Parapet::Engine.root.to_s)
108
+
109
+ old_full = File.expand_path(File.join(root_path, old_relative))
110
+ new_full = File.expand_path(File.join(root_path, new_relative))
111
+
112
+ # Security check
113
+ unless old_full.start_with?(root_path) && new_full.start_with?(root_path)
114
+ render json: { error: "Access Denied" }, status: :forbidden
115
+ return
116
+ end
117
+
118
+ if File.exist?(old_full)
119
+ begin
120
+ # Ensure target directory exists
121
+ FileUtils.mkdir_p(File.dirname(new_full))
122
+ File.rename(old_full, new_full)
123
+ render json: {
124
+ status: "success",
125
+ old_path: params[:old_path],
126
+ new_path: params[:new_path],
127
+ flash: { notice: "Renamed: #{File.basename(old_full)} -> #{File.basename(new_full)}" }
128
+ }
129
+ rescue => e
130
+ render json: { error: e.message }, status: :internal_server_error
131
+ end
132
+ else
133
+ render json: { error: "File not found" }, status: :not_found
134
+ end
135
+ end
136
+
137
+ def assets
138
+ root_path = Parapet::Engine.root.to_s
139
+ image_extensions = %w[svg png jpg jpeg webp gif]
140
+
141
+ assets = []
142
+ begin
143
+ Dir.glob(File.join(root_path, "app/assets/images/**/*.{#{image_extensions.join(',')}}")).each do |path|
144
+ rel_path = path.sub(root_path, '').sub(/^[\\\/]/, '').gsub('\\', '/')
145
+ assets << {
146
+ name: File.basename(path),
147
+ path: rel_path,
148
+ url: (view_context.asset_path("parapet/#{rel_path.sub('app/assets/images/parapet/', '')}") rescue rel_path)
149
+ }
150
+ end
151
+ rescue => e
152
+ Rails.logger.error "Parapet Assets Error: #{e.message}"
153
+ end
154
+
155
+ render json: assets
156
+ end
157
+
158
+ private
159
+
160
+ def build_tree(path)
161
+ root_path = Parapet::Engine.root.to_s
162
+ name = File.basename(path)
163
+ return nil if name.start_with?('.') && name != '.'
164
+
165
+ # Basic ignore list for performance
166
+ ignored = %w[node_modules vendor .git tmp log .bundle .zencoder]
167
+ return nil if ignored.include?(name)
168
+
169
+ # Editable assets for the Style Workbench
170
+ allowed_extensions = %w[erb scss css rb]
171
+
172
+ # Calculate relative path without leading slash
173
+ rel_path = path.to_s.sub(root_path, '').sub(/^[\\\/]/, '').gsub('\\', '/')
174
+
175
+ if File.directory?(path)
176
+ children = Dir.children(path).sort.map { |child| build_tree(File.join(path, child)) }.compact
177
+
178
+ # Only return directory if it has children (prune empty branches)
179
+ return nil if children.empty?
180
+
181
+ {
182
+ name: name == '.' ? 'root' : name,
183
+ type: 'directory',
184
+ path: rel_path.empty? ? '/' : rel_path,
185
+ children: children
186
+ }
187
+ else
188
+ ext = File.extname(path).delete('.')
189
+
190
+ # Strict filtering for relevant file types
191
+ # Note: .html.erb has ext "erb"
192
+ return nil unless allowed_extensions.include?(ext)
193
+
194
+ # If it's .rb, ensure it's in a relevant directory (optional, but safer)
195
+ # For now, we'll keep all .rb (presenters/models) as they might contain logic
196
+
197
+ {
198
+ name: name,
199
+ type: 'file',
200
+ path: rel_path,
201
+ ext: ext
202
+ }
203
+ end
204
+ rescue Errno::EACCES
205
+ nil
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,24 @@
1
+ module Parapet
2
+ class FootersController < ApplicationController
3
+ SUITE_MAP = {
4
+ "tests" => "parapet/shared/footer_tests",
5
+ "styles" => "parapet/shared/footer_styles",
6
+ "state" => "parapet/shared/footer_state",
7
+ "pulse" => "parapet/shared/footer_pulse",
8
+ "traffic" => "parapet/shared/footer_traffic",
9
+ "languages" => "parapet/shared/footer_languages",
10
+ "ideas" => "parapet/shared/footer_ideas"
11
+ }.freeze
12
+
13
+ def show
14
+ suite = params[:suite]
15
+ partial = SUITE_MAP[suite]
16
+
17
+ if partial
18
+ render partial: partial
19
+ else
20
+ render plain: "Footer not found", status: :not_found
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Parapet
2
+ module IntentionsSuite
3
+ class DashboardController < Parapet::BaseController
4
+ def index
5
+ @active_suite = 'intentions'
6
+ render "parapet/dashboards/index"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,14 @@
1
+ module Parapet
2
+ class LandingController < ApplicationController
3
+ layout 'parapet/landing'
4
+
5
+ def index
6
+ render 'parapet/index'
7
+ end
8
+
9
+ def login
10
+ # Placeholder for login action logic
11
+ redirect_to css_suite_path
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ module Parapet
2
+ module MonitorSuite
3
+ class DashboardController < Parapet::BaseController
4
+ def index
5
+ @active_suite = 'monitor'
6
+ render "parapet/dashboards/index"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ module Parapet
2
+ module PmSuite
3
+ class PublishController < ApplicationController
4
+ def create
5
+ result = Parapet::PublishGate.new(user: current_user).run
6
+
7
+ render json: {
8
+ status: result.run_history.status,
9
+ exit_code: result.run_history.exit_code,
10
+ raw_output: result.run_history.raw_output,
11
+ run_history_id: result.run_history.id
12
+ }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Parapet
2
+ module PolyglotSuite
3
+ class DashboardController < Parapet::BaseController
4
+ def index
5
+ @active_suite = 'polyglot'
6
+ render "parapet/dashboards/index"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,34 @@
1
+ require 'open3'
2
+
3
+ module Parapet
4
+ class RunnerController < ApplicationController
5
+ def detect_frameworks
6
+ runner = Parapet::TestRunner.new
7
+ frameworks = runner.detect_frameworks
8
+ render json: { frameworks: frameworks }
9
+ end
10
+
11
+ def execute
12
+ runner = Parapet::TestRunner.new(user: current_user)
13
+ result = runner.execute(
14
+ command_type: params[:command],
15
+ path: params[:path]
16
+ )
17
+
18
+ render json: {
19
+ status: "success",
20
+ command: result[:command],
21
+ output: result[:output],
22
+ summary: result[:summary],
23
+ exit_code: result[:exit_code]
24
+ }
25
+ rescue => e
26
+ render json: { status: "error", error: e.message }
27
+ end
28
+
29
+ def status
30
+ # Placeholder for long-running test status checks
31
+ render json: { status: "idle" }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ module Parapet
2
+ class SessionsController < ApplicationController
3
+ layout "parapet/landing"
4
+
5
+ skip_before_action :require_authentication, only: %i[new create]
6
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to login_path, alert: "Try again later." }
7
+
8
+ def new
9
+ end
10
+
11
+ def create
12
+ user = User.authenticate_by(params.permit(:email_address, :password))
13
+
14
+ if user
15
+ Current.session&.destroy
16
+ Current.session = user.sessions.create!
17
+ cookies.encrypted.permanent[:parapet_session_id] = {
18
+ value: Current.session.id,
19
+ httponly: true,
20
+ same_site: :lax
21
+ }
22
+ redirect_to root_path
23
+ else
24
+ flash.now[:alert] = "Invalid email address or password."
25
+ render :new, status: :unprocessable_entity
26
+ end
27
+ end
28
+
29
+ def destroy
30
+ Current.session&.destroy
31
+ cookies.delete(:parapet_session_id)
32
+ redirect_to login_path
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,10 @@
1
+ module Parapet
2
+ module SignalSuite
3
+ class DashboardController < Parapet::BaseController
4
+ def index
5
+ @active_suite = 'signal'
6
+ render "parapet/dashboards/index"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ module Parapet
2
+ module TestSuite
3
+ class DashboardController < Parapet::BaseController
4
+ def index
5
+ @active_suite = 'tests'
6
+ render "parapet/dashboards/index"
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,33 @@
1
+ module Parapet
2
+ class WidgetsController < ApplicationController
3
+ def show
4
+ @widget_type = params[:type]
5
+ @audit_stats = Parapet::ExplorerManager.audit_data(@widget_type)
6
+
7
+ # Determine which partial to render based on suite type
8
+ # We look for a suite-specific widget partial first
9
+ suite_partial = "parapet/shared/widgets/#{@widget_type}"
10
+
11
+ if lookup_context.exists?(suite_partial, [], true)
12
+ render partial: suite_partial
13
+ else
14
+ # Fallback to legacy partial map or base widget
15
+ partial_map = {
16
+ "search" => "parapet/widgets/search",
17
+ "explainer" => "parapet/widgets/explainer",
18
+ "snippets" => "parapet/widgets/snippets",
19
+ "suggestions" => "parapet/widgets/suggestions",
20
+ "hotkeys" => "parapet/widgets/hotkeys"
21
+ }
22
+
23
+ @partial_path = partial_map[@widget_type]
24
+
25
+ if @partial_path && lookup_context.exists?(@partial_path, [], true)
26
+ render partial: @partial_path
27
+ else
28
+ render partial: "parapet/shared/widgets/base"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ module Parapet
2
+ module ApplicationHelper
3
+ include IconHelper
4
+
5
+ def storage_path(filename = nil)
6
+ ExplorerManager.storage_path(filename)
7
+ end
8
+
9
+ def explorer_summary(type = 'project')
10
+ ExplorerManager.summary(type)
11
+ end
12
+
13
+ def qss
14
+ if Rails.env.development?
15
+ # Force reload of QSS lib during development
16
+ qss_path = File.expand_path('../../../../lib/parapet/qss.rb', __FILE__)
17
+ load qss_path if File.exist?(qss_path)
18
+ ::QSS.instance_variable_set(:@objects, nil)
19
+ end
20
+ ::QSS
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,57 @@
1
+ require 'nokogiri'
2
+
3
+ module Parapet
4
+ module IconHelper
5
+ def pp_icon(name, **options)
6
+ css_class = options.delete(:class)
7
+
8
+ # Try resolving custom SVG icon first
9
+ asset = resolve_icon_asset(name)
10
+
11
+ content = if asset
12
+ render_svg_icon(asset, name, options)
13
+ else
14
+ render_lucide_icon(name, options)
15
+ end
16
+
17
+ content_tag(:span, content, class: "qss-icon-wrap qss-w-4 qss-h-4 #{css_class}".strip, **options)
18
+ end
19
+
20
+ private
21
+
22
+ def resolve_icon_asset(name)
23
+ paths = [
24
+ "parapet/icons/activities/#{name}.svg",
25
+ "parapet/icons/nav/#{name}.svg",
26
+ "parapet/icons/#{name}.svg"
27
+ ]
28
+
29
+ paths.each do |path|
30
+ # Propshaft resolution API
31
+ found = Rails.application.assets.load_path.find(path)
32
+ return found if found
33
+ end
34
+ nil
35
+ end
36
+
37
+ def render_svg_icon(asset, name, options)
38
+ raw_svg = File.read(asset.path)
39
+ doc = Nokogiri::HTML::DocumentFragment.parse(raw_svg)
40
+ svg = doc.at_css('svg')
41
+
42
+ if svg
43
+ # Ensure structural sizing classes are present
44
+ svg['class'] = "qss-icon-raw qss-w-full qss-h-full #{svg['class']}".strip
45
+ svg.remove_attribute('width')
46
+ svg.remove_attribute('height')
47
+ svg['aria-hidden'] = 'true'
48
+ end
49
+
50
+ doc.to_html.html_safe
51
+ end
52
+
53
+ def render_lucide_icon(name, options)
54
+ content_tag(:i, nil, class: "lucide-#{name} qss-icon-lucide qss-w-full qss-h-full", aria: { hidden: true })
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,4 @@
1
+ module Parapet
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Parapet
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: "from@example.com"
4
+ layout "mailer"
5
+ end
6
+ end
@@ -0,0 +1,14 @@
1
+ module Parapet
2
+ class Activity < InternalRecord
3
+ self.table_name = "activities"
4
+
5
+ belongs_to :user, class_name: "Parapet::User", optional: true
6
+ belongs_to :trackable, polymorphic: true, optional: true
7
+
8
+ validates :activity_type, presence: true
9
+ validates :action, presence: true
10
+ validates :performed_via, inclusion: { in: %w[human ai_agent] }
11
+
12
+ scope :recent, -> { order(created_at: :desc) }
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ module Parapet
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,6 @@
1
+ module Parapet
2
+ class Current < ActiveSupport::CurrentAttributes
3
+ attribute :session
4
+ delegate :user, to: :session, allow_nil: true
5
+ end
6
+ end
@@ -0,0 +1,9 @@
1
+ module Parapet
2
+ class DraftRecovery < InternalRecord
3
+ self.table_name = "draft_recoveries"
4
+
5
+ belongs_to :user, class_name: "Parapet::User"
6
+
7
+ validates :file_path, presence: true, uniqueness: { scope: :user_id }
8
+ end
9
+ end
@@ -0,0 +1,34 @@
1
+ require "fileutils"
2
+
3
+ module Parapet
4
+ class InternalRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+
7
+ database_filename = Rails.env.test? ? "parapet-test.sqlite3" : "parapet.sqlite3"
8
+ database_path = Rails.root.join("storage", database_filename)
9
+ FileUtils.mkdir_p(database_path.dirname)
10
+
11
+ establish_connection(
12
+ adapter: "sqlite3",
13
+ database: database_path.to_s,
14
+ pool: ENV.fetch("RAILS_MAX_THREADS", 5),
15
+ timeout: 5000,
16
+ pragmas: { journal_mode: "wal" }
17
+ )
18
+
19
+ def self.migrate!
20
+ database_tasks = ActiveRecord::Tasks::DatabaseTasks
21
+ original_migration_class = database_tasks.method(:migration_class)
22
+ database_tasks.define_singleton_method(:migration_class) { Parapet::InternalRecord }
23
+
24
+ migration_context = ActiveRecord::MigrationContext.new(
25
+ [ Parapet::Engine.root.join("db/parapet_migrate").to_s ],
26
+ connection_pool.schema_migration,
27
+ connection_pool.internal_metadata
28
+ )
29
+ migration_context.migrate
30
+ ensure
31
+ database_tasks.define_singleton_method(:migration_class, original_migration_class) if original_migration_class
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module Parapet
2
+ class RunHistory < ApplicationRecord
3
+ self.table_name = "run_history"
4
+
5
+ belongs_to :user, class_name: "Parapet::User", optional: true
6
+
7
+ validates :run_type, presence: true
8
+ validates :command, presence: true
9
+ validates :status, presence: true
10
+ validates :triggered_by_actor_type, presence: true
11
+ validates :performed_via, inclusion: { in: %w[human ai_agent] }
12
+ validates :started_at, presence: true
13
+
14
+ def passed?
15
+ status == "passed"
16
+ end
17
+
18
+ def failed?
19
+ status == "failed"
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,8 @@
1
+ module Parapet
2
+ class RuntimeSetting < InternalRecord
3
+ self.table_name = "runtime_settings"
4
+
5
+ validates :setting_key, presence: true, uniqueness: true
6
+ validates :value, presence: true
7
+ end
8
+ end
@@ -0,0 +1,7 @@
1
+ module Parapet
2
+ class Session < InternalRecord
3
+ self.table_name = "sessions"
4
+
5
+ belongs_to :user
6
+ end
7
+ end
@@ -0,0 +1,30 @@
1
+ module Parapet
2
+ class SuiteAssignment < InternalRecord
3
+ self.table_name = "suite_assignments"
4
+
5
+ SUITES = %w[
6
+ authentication
7
+ authorization
8
+ licensing
9
+ activity_tracking
10
+ intentions_suite
11
+ style_suite
12
+ test_suite
13
+ build_suite
14
+ database_suite
15
+ signal_suite
16
+ process_monitor
17
+ polyglot_suite
18
+ pm_suite
19
+ ].freeze
20
+
21
+ TIERS = %w[novice intermediate advanced power].freeze
22
+
23
+ belongs_to :user, class_name: "Parapet::User"
24
+
25
+ validates :suite, inclusion: { in: SUITES }
26
+ validates :suite, uniqueness: { scope: :user_id }
27
+ validates :role, presence: true, allow_nil: true, length: { maximum: 64 }
28
+ validates :tier, inclusion: { in: TIERS }
29
+ end
30
+ end