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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +48 -0
- data/Rakefile +7 -0
- data/app/assets/config/parapet/grammar-registry.json +52 -0
- data/app/assets/images/parapet/icons/activities/component-inspector.svg +4 -0
- data/app/assets/images/parapet/icons/activities/coverage-heatmap.svg +3 -0
- data/app/assets/images/parapet/icons/activities/discovery-sidebar.svg +3 -0
- data/app/assets/images/parapet/icons/activities/environment-toggles.svg +4 -0
- data/app/assets/images/parapet/icons/activities/failure-inspector.svg +5 -0
- data/app/assets/images/parapet/icons/activities/flaky-history.svg +3 -0
- data/app/assets/images/parapet/icons/activities/flight-recorder.svg +5 -0
- data/app/assets/images/parapet/icons/activities/health-graph.svg +3 -0
- data/app/assets/images/parapet/icons/activities/injection-engine.svg +3 -0
- data/app/assets/images/parapet/icons/activities/live-sql-stream.svg +5 -0
- data/app/assets/images/parapet/icons/activities/live-web-preview.svg +4 -0
- data/app/assets/images/parapet/icons/activities/monaco-editor.svg +5 -0
- data/app/assets/images/parapet/icons/activities/progress-area.svg +3 -0
- data/app/assets/images/parapet/icons/activities/registry-metadata.svg +5 -0
- data/app/assets/images/parapet/icons/activities/seed-inspector.svg +4 -0
- data/app/assets/images/parapet/icons/activities/slow-test-bottlenecks.svg +4 -0
- data/app/assets/images/parapet/icons/activities/smart-search.svg +4 -0
- data/app/assets/images/parapet/icons/activities/snippet-library.svg +4 -0
- data/app/assets/images/parapet/icons/activities/terminal.svg +4 -0
- data/app/assets/images/parapet/icons/activities/unified-schema-view.svg +4 -0
- data/app/assets/images/parapet/icons/activities/visual-comparison.svg +3 -0
- data/app/assets/images/parapet/icons/activities/visual-summary-bar.svg +5 -0
- data/app/assets/images/parapet/icons/nav/ideas.svg +4 -0
- data/app/assets/images/parapet/icons/nav/languages.svg +8 -0
- data/app/assets/images/parapet/icons/nav/profile.svg +4 -0
- data/app/assets/images/parapet/icons/nav/pulse.svg +7 -0
- data/app/assets/images/parapet/icons/nav/settings.svg +4 -0
- data/app/assets/images/parapet/icons/nav/state.svg +5 -0
- data/app/assets/images/parapet/icons/nav/styles.svg +7 -0
- data/app/assets/images/parapet/icons/nav/tests.svg +4 -0
- data/app/assets/images/parapet/icons/nav/traffic.svg +12 -0
- data/app/assets/javascripts/parapet/activity-card.js +439 -0
- data/app/assets/javascripts/parapet/activity-cards.js +291 -0
- data/app/assets/javascripts/parapet/activity-registry.js +181 -0
- data/app/assets/javascripts/parapet/activity-toggles.js +76 -0
- data/app/assets/javascripts/parapet/application.js +37 -0
- data/app/assets/javascripts/parapet/card-failure-inspector.js +134 -0
- data/app/assets/javascripts/parapet/card-file-editor.js +1722 -0
- data/app/assets/javascripts/parapet/card-progress-area.js +297 -0
- data/app/assets/javascripts/parapet/card-smart-search.js +114 -0
- data/app/assets/javascripts/parapet/card-snippet-library.js +251 -0
- data/app/assets/javascripts/parapet/explorer-resizer.js +92 -0
- data/app/assets/javascripts/parapet/parapet_qss.js +342 -0
- data/app/assets/javascripts/parapet/proficiency-filter.js +53 -0
- data/app/assets/javascripts/parapet/sidebar-resizer.js +197 -0
- data/app/assets/javascripts/parapet/styles-workbench.js +819 -0
- data/app/assets/javascripts/parapet/tree-logic.js +320 -0
- data/app/assets/stylesheets/parapet/_qss_variables.scss +122 -0
- data/app/assets/stylesheets/parapet/parapet_qss.scss +69 -0
- data/app/assets/stylesheets/parapet/qss_application.css +260940 -0
- data/app/assets/stylesheets/parapet/qss_application.css.map +1 -0
- data/app/assets/stylesheets/parapet/qss_application.scss +5 -0
- data/app/assets/stylesheets/parapet/qss_workspace.scss +209 -0
- data/app/assets/stylesheets/parapet/sample.qss +22 -0
- data/app/controllers/parapet/application_controller.rb +54 -0
- data/app/controllers/parapet/base_controller.rb +18 -0
- data/app/controllers/parapet/build_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/css_suite/compiler_controller.rb +95 -0
- data/app/controllers/parapet/css_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/css_suite/preview_controller.rb +46 -0
- data/app/controllers/parapet/dashboard_controller.rb +8 -0
- data/app/controllers/parapet/drafts_controller.rb +30 -0
- data/app/controllers/parapet/explorers_controller.rb +53 -0
- data/app/controllers/parapet/files_controller.rb +208 -0
- data/app/controllers/parapet/footers_controller.rb +24 -0
- data/app/controllers/parapet/intentions_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/landing_controller.rb +14 -0
- data/app/controllers/parapet/monitor_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/pm_suite/publish_controller.rb +16 -0
- data/app/controllers/parapet/polyglot_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/runner_controller.rb +34 -0
- data/app/controllers/parapet/sessions_controller.rb +35 -0
- data/app/controllers/parapet/signal_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/test_suite/dashboard_controller.rb +10 -0
- data/app/controllers/parapet/widgets_controller.rb +33 -0
- data/app/helpers/parapet/application_helper.rb +23 -0
- data/app/helpers/parapet/icon_helper.rb +57 -0
- data/app/jobs/parapet/application_job.rb +4 -0
- data/app/mailers/parapet/application_mailer.rb +6 -0
- data/app/models/parapet/activity.rb +14 -0
- data/app/models/parapet/application_record.rb +5 -0
- data/app/models/parapet/current.rb +6 -0
- data/app/models/parapet/draft_recovery.rb +9 -0
- data/app/models/parapet/internal_record.rb +34 -0
- data/app/models/parapet/run_history.rb +22 -0
- data/app/models/parapet/runtime_setting.rb +8 -0
- data/app/models/parapet/session.rb +7 -0
- data/app/models/parapet/suite_assignment.rb +30 -0
- data/app/models/parapet/user.rb +12 -0
- data/app/services/parapet/activity_phrase_renderer.rb +35 -0
- data/app/services/parapet/activity_writer.rb +26 -0
- data/app/services/parapet/authorization.rb +198 -0
- data/app/services/parapet/consistency_auditor.rb +58 -0
- data/app/services/parapet/directory_scanner.rb +159 -0
- data/app/services/parapet/draft_recovery_store.rb +18 -0
- data/app/services/parapet/explorer_manager.rb +402 -0
- data/app/services/parapet/file_save_service.rb +21 -0
- data/app/services/parapet/git_side_ref_committer.rb +50 -0
- data/app/services/parapet/noun_discovery.rb +26 -0
- data/app/services/parapet/publish_gate.rb +83 -0
- data/app/services/parapet/test_runner.rb +178 -0
- data/app/views/layouts/parapet/application.html.erb +83 -0
- data/app/views/layouts/parapet/landing.html.erb +19 -0
- data/app/views/parapet/activities/_ai_error_explainer.html.erb +33 -0
- data/app/views/parapet/activities/_ai_explainer_widget.html.erb +20 -0
- data/app/views/parapet/activities/_calendar_widget.html.erb +31 -0
- data/app/views/parapet/activities/_code_snippets_widget.html.erb +22 -0
- data/app/views/parapet/activities/_coverage_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_dropdown_test.html.erb +20 -0
- data/app/views/parapet/activities/_explorer_node.html.erb +102 -0
- data/app/views/parapet/activities/_explorer_tree.html.erb +16 -0
- data/app/views/parapet/activities/_file_editor.html.erb +183 -0
- data/app/views/parapet/activities/_file_explorer.html.erb +23 -0
- data/app/views/parapet/activities/_fix_suggestions_widget.html.erb +13 -0
- data/app/views/parapet/activities/_generic.html.erb +6 -0
- data/app/views/parapet/activities/_live_preview.html.erb +13 -0
- data/app/views/parapet/activities/_perspective_filters.html.erb +36 -0
- data/app/views/parapet/activities/_progress_area.html.erb +37 -0
- data/app/views/parapet/activities/_project_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_registry_explorer.html.erb +1 -0
- data/app/views/parapet/activities/_run_history.html.erb +2 -0
- data/app/views/parapet/activities/_smart_search.html.erb +11 -0
- data/app/views/parapet/activities/file_explorer/_perspective_scripts.html.erb +58 -0
- data/app/views/parapet/activities/file_explorer/_perspectives.html.erb +0 -0
- data/app/views/parapet/activities/file_explorer/_viewport_scripts.html.erb +33 -0
- data/app/views/parapet/css_suite/_workbench.html.erb +51 -0
- data/app/views/parapet/dashboards/index.html.erb +113 -0
- data/app/views/parapet/debug_panel.html.erb +79 -0
- data/app/views/parapet/debug_panel_tailwind.html.erb +98 -0
- data/app/views/parapet/erb_patch_test.html.erb +5 -0
- data/app/views/parapet/explorers/show.html.erb +1 -0
- data/app/views/parapet/git_viewer.html.erb +62 -0
- data/app/views/parapet/git_viewer_after.html.erb +14 -0
- data/app/views/parapet/git_viewer_before.html.erb +14 -0
- data/app/views/parapet/git_viewer_governed.html.erb +88 -0
- data/app/views/parapet/git_viewer_governed_after.html.erb +36 -0
- data/app/views/parapet/git_viewer_governed_before.html.erb +36 -0
- data/app/views/parapet/git_viewer_tailwind.html.erb +88 -0
- data/app/views/parapet/git_viewer_thin_wrapper.html.erb +85 -0
- data/app/views/parapet/hierarchical_layout_test.html.erb +85 -0
- data/app/views/parapet/hybrid_stress_test.html.erb +25 -0
- data/app/views/parapet/index.html.erb +35 -0
- data/app/views/parapet/mutation_stress_test.html.erb +82 -0
- data/app/views/parapet/recursive_anchoring_test.html.erb +66 -0
- data/app/views/parapet/sessions/new.html.erb +28 -0
- data/app/views/parapet/shared/_cad_inspector.html.erb +92 -0
- data/app/views/parapet/shared/_discovery_sidebar.html.erb +48 -0
- data/app/views/parapet/shared/_explorer_bar.html.erb +71 -0
- data/app/views/parapet/shared/_footer.html.erb +10 -0
- data/app/views/parapet/shared/_footer_ideas.html.erb +36 -0
- data/app/views/parapet/shared/_footer_languages.html.erb +33 -0
- data/app/views/parapet/shared/_footer_pulse.html.erb +33 -0
- data/app/views/parapet/shared/_footer_state.html.erb +33 -0
- data/app/views/parapet/shared/_footer_styles.html.erb +37 -0
- data/app/views/parapet/shared/_footer_tests.html.erb +39 -0
- data/app/views/parapet/shared/_footer_traffic.html.erb +45 -0
- data/app/views/parapet/shared/_global_nav.html.erb +55 -0
- data/app/views/parapet/shared/_head.html.erb +4 -0
- data/app/views/parapet/shared/_header.html.erb +20 -0
- data/app/views/parapet/shared/_object_inspector.html.erb +76 -0
- data/app/views/parapet/shared/_ribbon_header.html.erb +3 -0
- data/app/views/parapet/shared/_root_selector_modal.html.erb +15 -0
- data/app/views/parapet/shared/_tab_bar.html.erb +62 -0
- data/app/views/parapet/shared/activities_ribbon/_agent_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_analysis_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_architect_group.html.erb +21 -0
- data/app/views/parapet/shared/activities_ribbon/_explorer_group.html.erb +12 -0
- data/app/views/parapet/shared/activities_ribbon/_inspector_group.html.erb +15 -0
- data/app/views/parapet/shared/activities_ribbon/_proficiency_filter.html.erb +24 -0
- data/app/views/parapet/shared/activities_ribbon/_runner_group.html.erb +21 -0
- data/app/views/parapet/shared/activities_ribbon/_spatial_inspector_group.html.erb +18 -0
- data/app/views/parapet/shared/activities_ribbon/_studio_group.html.erb +18 -0
- data/app/views/parapet/shared/activities_ribbon/_stylist_group.html.erb +18 -0
- data/app/views/parapet/shared/explorers/_base.html.erb +107 -0
- data/app/views/parapet/shared/explorers/_css.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_intentions.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_project.html.erb +1 -0
- data/app/views/parapet/shared/explorers/_tests.html.erb +1 -0
- data/app/views/parapet/shared/head/_meta.html.erb +3 -0
- data/app/views/parapet/shared/head/_styles_activities.html.erb +152 -0
- data/app/views/parapet/shared/head/_styles_shared.html.erb +52 -0
- data/app/views/parapet/shared/head/_styles_workspace.html.erb +67 -0
- data/app/views/parapet/shared/ribbon_header/_diagnostic_controls.html.erb +17 -0
- data/app/views/parapet/shared/ribbon_header/_execution_controls.html.erb +28 -0
- data/app/views/parapet/shared/ribbon_header/_focus_search.html.erb +5 -0
- data/app/views/parapet/shared/root_selector/_header.html.erb +10 -0
- data/app/views/parapet/shared/root_selector/_path_input.html.erb +13 -0
- data/app/views/parapet/shared/root_selector/_quick_select.html.erb +20 -0
- data/app/views/parapet/shared/root_selector/_scripts.html.erb +61 -0
- data/app/views/parapet/shared/root_selector/_warning.html.erb +11 -0
- data/app/views/parapet/shared/widgets/_base.html.erb +6 -0
- data/app/views/parapet/shared/widgets/_css.html.erb +79 -0
- data/app/views/parapet/widgets/_explainer.html.erb +17 -0
- data/app/views/parapet/widgets/_hotkeys.html.erb +20 -0
- data/app/views/parapet/widgets/_search.html.erb +68 -0
- data/app/views/parapet/widgets/_spatial_inspector.html.erb +54 -0
- data/app/views/parapet/widgets/_widget-explainer.html.erb +17 -0
- data/app/views/parapet/widgets/_widget-hotkeys.html.erb +20 -0
- data/app/views/parapet/widgets/_widget-search.html.erb +68 -0
- data/app/views/parapet/widgets/_widget-snippets.html.erb +18 -0
- data/app/views/parapet/widgets/_widget-suggestions.html.erb +15 -0
- data/app/views/parapet/widgets/show.html.erb +3 -0
- data/app/views/shared/_footer.html.erb +0 -0
- data/config/parapet/qss_config.yml +117 -0
- data/config/parapet/qss_registry.yml +14 -0
- data/config/routes.rb +40 -0
- data/db/migrate/20260916035300_create_run_histories.rb +16 -0
- data/db/migrate/20261002000000_expand_run_histories_for_activity_tracking.rb +7 -0
- data/db/parapet_migrate/20260929184200_create_parapet_users.rb +12 -0
- data/db/parapet_migrate/20260929184300_create_parapet_sessions.rb +9 -0
- data/db/parapet_migrate/20260930000000_create_parapet_runtime_settings.rb +12 -0
- data/db/parapet_migrate/20260930000100_create_parapet_suite_assignments.rb +14 -0
- data/db/parapet_migrate/20261002000000_create_parapet_activities.rb +16 -0
- data/db/parapet_migrate/20261002000100_create_parapet_draft_recoveries.rb +13 -0
- data/db/parapet_schema.rb +64 -0
- data/db/schema.rb +21 -0
- data/db/schema.released.rb +0 -0
- data/lib/parapet/engine.rb +11 -0
- data/lib/parapet/environment_guard.rb +14 -0
- data/lib/parapet/version.rb +3 -0
- data/lib/parapet-rails.rb +1 -0
- data/lib/parapet.rb +59 -0
- data/lib/tasks/parapet_qss.rake +26 -0
- data/lib/tasks/parapet_tasks.rake +25 -0
- data/lib/tasks/randomize_explorer_health.rb +101 -0
- metadata +423 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class ExplorerManager
|
|
3
|
+
def self.storage_path(filename = nil)
|
|
4
|
+
base = Parapet::Engine.root.join("storage").to_s
|
|
5
|
+
filename ? File.join(base, filename) : base
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.summary(type = 'project')
|
|
9
|
+
root_path = Parapet::Engine.root.to_s
|
|
10
|
+
scanner = Parapet::DirectoryScanner.new(root_path)
|
|
11
|
+
full_tree = scanner.scan
|
|
12
|
+
|
|
13
|
+
# Use virtualization to get the correct subset for counting
|
|
14
|
+
focused_tree = virtualize(full_tree, type)
|
|
15
|
+
files = flatten_files(focused_tree[:children] || [])
|
|
16
|
+
|
|
17
|
+
counts = { 'green' => 0, 'red' => 0, 'orange' => 0, 'grey' => 0 }
|
|
18
|
+
files.each do |f|
|
|
19
|
+
status = f[:status] || 'grey'
|
|
20
|
+
counts[status] += 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
{ 'counts' => counts, 'total_files' => files.count }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.audit_data(type = 'css')
|
|
27
|
+
root_path = Parapet::Engine.root.to_s
|
|
28
|
+
scanner = Parapet::DirectoryScanner.new(root_path)
|
|
29
|
+
full_tree = scanner.scan
|
|
30
|
+
|
|
31
|
+
focused_tree = virtualize(full_tree, type)
|
|
32
|
+
files = flatten_files(focused_tree[:children] || [])
|
|
33
|
+
|
|
34
|
+
audit_stats = {
|
|
35
|
+
'legibility' => 0,
|
|
36
|
+
'ergonomic' => 0,
|
|
37
|
+
'protocol' => 0,
|
|
38
|
+
'grammar' => 0,
|
|
39
|
+
'parameter' => 0,
|
|
40
|
+
'z_collision' => 0 # Future implementation
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
files.each do |f|
|
|
44
|
+
full_path = File.join(Parapet::Engine.root, f[:path])
|
|
45
|
+
audit = QSS::QssAuditor.audit_file(full_path)
|
|
46
|
+
audit[:issues].each do |issue|
|
|
47
|
+
audit_stats[issue[:type]] += 1 if audit_stats.key?(issue[:type])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
audit_stats
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.virtualize(tree, type, perspective: 'project')
|
|
55
|
+
return nil if tree.nil?
|
|
56
|
+
|
|
57
|
+
# The 'perspective' parameter from the switcher overrides the base suite 'type'
|
|
58
|
+
# unless the perspective is 'project', in which case we use the suite's default view.
|
|
59
|
+
active_type = (perspective == 'project') ? type : perspective
|
|
60
|
+
|
|
61
|
+
case active_type
|
|
62
|
+
when 'tests', 'test', 'project'
|
|
63
|
+
# Focus on all test-related directories
|
|
64
|
+
test_dir = Parapet.test_root
|
|
65
|
+
test_dirs = [ test_dir, 'features', 'test', 'spec' ].uniq
|
|
66
|
+
nodes = tree[:children] || []
|
|
67
|
+
focused_nodes = nodes.select { |c| test_dirs.include?(c[:name]) }
|
|
68
|
+
|
|
69
|
+
{
|
|
70
|
+
name: 'Project Tests',
|
|
71
|
+
type: 'directory',
|
|
72
|
+
path: '/project-tests',
|
|
73
|
+
icon: 'icon-microscope',
|
|
74
|
+
children: focused_nodes,
|
|
75
|
+
health: calculate_virtual_health(flatten_files(focused_nodes))
|
|
76
|
+
}
|
|
77
|
+
when 'css', 'styles'
|
|
78
|
+
group_by_styles(tree)
|
|
79
|
+
when 'views'
|
|
80
|
+
group_by_views(tree)
|
|
81
|
+
when 'intentions', 'ideas'
|
|
82
|
+
group_by_intentions(tree)
|
|
83
|
+
when 'registry'
|
|
84
|
+
files = flatten_files(tree[:children] || [])
|
|
85
|
+
group_by_registry(files)
|
|
86
|
+
when 'coverage'
|
|
87
|
+
files = flatten_files(tree[:children] || [])
|
|
88
|
+
group_by_coverage(files)
|
|
89
|
+
when 'grammar'
|
|
90
|
+
group_by_grammar
|
|
91
|
+
else
|
|
92
|
+
tree
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def self.group_by_views(tree)
|
|
99
|
+
return nil if tree.nil?
|
|
100
|
+
nodes = tree[:children] || []
|
|
101
|
+
app_node = nodes.find { |n| n[:name] == 'app' }
|
|
102
|
+
return tree unless app_node
|
|
103
|
+
|
|
104
|
+
focused_children = filter_view_files(app_node[:children] || [])
|
|
105
|
+
|
|
106
|
+
{
|
|
107
|
+
name: 'Views Suite',
|
|
108
|
+
type: 'directory',
|
|
109
|
+
path: '/views',
|
|
110
|
+
icon: 'icon-layout',
|
|
111
|
+
iconColor: 'text-emerald-400',
|
|
112
|
+
children: focused_children,
|
|
113
|
+
health: calculate_virtual_health(flatten_files(focused_children))
|
|
114
|
+
}
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.filter_view_files(nodes)
|
|
118
|
+
nodes.map do |node|
|
|
119
|
+
if node[:type] == 'directory'
|
|
120
|
+
children = filter_view_files(node[:children] || [])
|
|
121
|
+
node.merge(children: children) if children.any?
|
|
122
|
+
elsif [ 'html', 'erb', 'qss' ].include?(node[:ext])
|
|
123
|
+
node
|
|
124
|
+
end
|
|
125
|
+
end.compact
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.group_by_intentions(tree)
|
|
129
|
+
return nil if tree.nil?
|
|
130
|
+
|
|
131
|
+
# For "Intentions/Code Suite", we focus on the app directory as the source of truth
|
|
132
|
+
nodes = tree[:children] || []
|
|
133
|
+
app_node = nodes.find { |n| n[:name] == 'app' }
|
|
134
|
+
|
|
135
|
+
# If app doesn't exist (unlikely in Rails), fallback to tree
|
|
136
|
+
return tree unless app_node
|
|
137
|
+
|
|
138
|
+
{
|
|
139
|
+
name: 'Application Intentions',
|
|
140
|
+
type: 'directory',
|
|
141
|
+
path: '/intentions',
|
|
142
|
+
icon: 'icon-lightbulb',
|
|
143
|
+
children: app_node[:children],
|
|
144
|
+
health: app_node[:health]
|
|
145
|
+
}
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def self.group_by_styles(tree)
|
|
149
|
+
return nil if tree.nil?
|
|
150
|
+
|
|
151
|
+
# For CSS Suite, we show both Views and Styles in their Rails hierarchy
|
|
152
|
+
nodes = tree[:children] || []
|
|
153
|
+
|
|
154
|
+
# Helper to find a path in the tree
|
|
155
|
+
find_node = ->(current_nodes, name) { (current_nodes || []).find { |n| n[:name] == name } }
|
|
156
|
+
|
|
157
|
+
app_node = find_node.call(nodes, 'app')
|
|
158
|
+
return tree unless app_node
|
|
159
|
+
|
|
160
|
+
# Filter for views and styles within the app directory
|
|
161
|
+
focused_children = filter_style_and_view_files(app_node[:children] || [])
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
name: 'Styles Suite',
|
|
165
|
+
type: 'directory',
|
|
166
|
+
path: '/styles',
|
|
167
|
+
icon: 'icon-palette',
|
|
168
|
+
iconColor: 'text-indigo-400',
|
|
169
|
+
children: focused_children,
|
|
170
|
+
health: calculate_virtual_health(flatten_files(focused_children))
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.filter_style_and_view_files(nodes)
|
|
175
|
+
nodes.map do |node|
|
|
176
|
+
if node[:type] == 'directory'
|
|
177
|
+
children = filter_style_and_view_files(node[:children] || [])
|
|
178
|
+
node.merge(children: children) if children.any?
|
|
179
|
+
elsif [ 'html', 'erb' ].include?(node[:ext])
|
|
180
|
+
node
|
|
181
|
+
end
|
|
182
|
+
end.compact
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def self.group_by_grammar
|
|
186
|
+
# Define the Agentic CSS Grammar categories
|
|
187
|
+
categories = [
|
|
188
|
+
{
|
|
189
|
+
name: 'Core Syntax',
|
|
190
|
+
icon: 'icon-code',
|
|
191
|
+
color: 'text-indigo-400',
|
|
192
|
+
rules: [
|
|
193
|
+
{ name: 'Noun (ID)', syntax: '#[id]', description: 'Structural Identity' },
|
|
194
|
+
{ name: 'Adjective (Class)', syntax: '.[class]', description: 'Characteristic' },
|
|
195
|
+
{ name: 'Parameter (Bracket)', syntax: '[value]', description: 'Dimension/Attribute' }
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'Spatial Coordinates',
|
|
200
|
+
icon: 'icon-move',
|
|
201
|
+
color: 'text-emerald-400',
|
|
202
|
+
rules: [
|
|
203
|
+
{ name: 'X-Position', syntax: '.pos-x[0...2000]', description: 'Left offset in qss-units' },
|
|
204
|
+
{ name: 'Y-Position', syntax: '.pos-y[0...2000]', description: 'Top offset in qss-units' },
|
|
205
|
+
{ name: 'Right-Position', syntax: '.pos-r[0...2000]', description: 'Right offset in qss-units' },
|
|
206
|
+
{ name: 'Bottom-Position', syntax: '.pos-b[0...2000]', description: 'Bottom offset in qss-units' }
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: 'Dimensions',
|
|
211
|
+
icon: 'icon-maximize',
|
|
212
|
+
color: 'text-amber-400',
|
|
213
|
+
rules: [
|
|
214
|
+
{ name: 'Width', syntax: '.w[0...2000]', description: 'Width in qss-units' },
|
|
215
|
+
{ name: 'Height', syntax: '.h[0...2000]', description: 'Height in qss-units' },
|
|
216
|
+
{ name: 'Width %', syntax: '.w-pct[0...1000]', description: 'Width in 0.1% increments' },
|
|
217
|
+
{ name: 'Height %', syntax: '.h-pct[0...1000]', description: 'Height in 0.1% increments' }
|
|
218
|
+
]
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
name: 'Visual Depth',
|
|
222
|
+
icon: 'icon-layers',
|
|
223
|
+
color: 'text-purple-400',
|
|
224
|
+
rules: [
|
|
225
|
+
{ name: 'Canvas', syntax: '.depth[canvas]', description: 'z-index: 0' },
|
|
226
|
+
{ name: 'Content', syntax: '.depth[content]', description: 'z-index: 100' },
|
|
227
|
+
{ name: 'UI', syntax: '.depth[ui]', description: 'z-index: 500' },
|
|
228
|
+
{ name: 'Overlay', syntax: '.depth[overlay]', description: 'z-index: 900' },
|
|
229
|
+
{ name: 'Modal', syntax: '.depth[modal]', description: 'z-index: 1000' }
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
virtual_children = categories.map do |cat|
|
|
235
|
+
{
|
|
236
|
+
name: cat[:name],
|
|
237
|
+
type: 'directory',
|
|
238
|
+
path: "/grammar/#{cat[:name].parameterize}",
|
|
239
|
+
icon: cat[:icon],
|
|
240
|
+
iconColor: cat[:color],
|
|
241
|
+
children: cat[:rules].map { |r|
|
|
242
|
+
{
|
|
243
|
+
name: r[:name],
|
|
244
|
+
type: 'file',
|
|
245
|
+
path: "/grammar/#{cat[:name].parameterize}/#{r[:name].parameterize}",
|
|
246
|
+
icon: 'icon-file-text',
|
|
247
|
+
status: 'grey',
|
|
248
|
+
metadata: { syntax: r[:syntax], description: r[:description] }
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
health: nil
|
|
252
|
+
}
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
{ name: 'Grammar', type: 'directory', path: '/grammar', icon: 'icon-book', children: virtual_children }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def self.group_by_registry(files)
|
|
259
|
+
# Filter for files that might contain QSS Nouns (views and partials)
|
|
260
|
+
view_files = files.select do |f|
|
|
261
|
+
[ 'html', 'erb', 'haml', 'slim' ].include?(f[:ext]) && !f[:path].include?('vendor')
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
registry_nodes = []
|
|
265
|
+
|
|
266
|
+
view_files.each do |f|
|
|
267
|
+
full_path = File.join(Parapet::Engine.root, f[:path])
|
|
268
|
+
nouns = Parapet::NounDiscovery.discover(full_path)
|
|
269
|
+
next if nouns.empty?
|
|
270
|
+
|
|
271
|
+
nouns.each do |noun|
|
|
272
|
+
# Find all files related to this noun (e.g. user.rb, user_spec.rb)
|
|
273
|
+
noun_id = noun[:id].downcase
|
|
274
|
+
related_files = files.select { |f| f[:name].downcase.start_with?(noun_id) }
|
|
275
|
+
|
|
276
|
+
registry_nodes << {
|
|
277
|
+
name: noun[:id],
|
|
278
|
+
type: 'directory',
|
|
279
|
+
path: "/registry/noun/#{noun[:id]}",
|
|
280
|
+
icon: 'icon-box',
|
|
281
|
+
iconColor: 'text-indigo-400',
|
|
282
|
+
children: related_files,
|
|
283
|
+
health: calculate_virtual_health(related_files),
|
|
284
|
+
metadata: {
|
|
285
|
+
source: f[:path],
|
|
286
|
+
adjectives: noun[:adjectives],
|
|
287
|
+
is_phantom: noun[:is_phantom],
|
|
288
|
+
type: 'Noun'
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Group nouns into logical categories or just alphabetical for now
|
|
295
|
+
# In a real app, we'd use metadata to categorize (UI, Layout, Form, etc.)
|
|
296
|
+
categories = [
|
|
297
|
+
{ name: 'Core Layout', icon: 'icon-layout', color: 'text-blue-400' },
|
|
298
|
+
{ name: 'UI Components', icon: 'icon-component', color: 'text-emerald-500' },
|
|
299
|
+
{ name: 'Form Elements', icon: 'icon-input', color: 'text-amber-500' },
|
|
300
|
+
{ name: 'Media Actors', icon: 'icon-image', color: 'text-purple-500' }
|
|
301
|
+
]
|
|
302
|
+
|
|
303
|
+
virtual_children = categories.map do |cat|
|
|
304
|
+
{
|
|
305
|
+
name: cat[:name],
|
|
306
|
+
type: 'directory',
|
|
307
|
+
path: "/registry/#{cat[:name].parameterize}",
|
|
308
|
+
icon: cat[:icon],
|
|
309
|
+
iconColor: cat[:color],
|
|
310
|
+
children: [],
|
|
311
|
+
health: nil
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Distribute nouns into categories (simple heuristic for demo)
|
|
316
|
+
registry_nodes.sort_by { |n| n[:name] }.each_with_index do |node, idx|
|
|
317
|
+
cat_idx = idx % virtual_children.count
|
|
318
|
+
virtual_children[cat_idx][:children] << node
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
{ name: 'Registry', type: 'directory', path: '/registry', icon: 'icon-database', children: virtual_children }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def self.group_by_coverage(files)
|
|
325
|
+
categories = get_categories('coverage')
|
|
326
|
+
|
|
327
|
+
virtual_children = categories.map do |cat|
|
|
328
|
+
# Filter files based on their determined status
|
|
329
|
+
status_map = {
|
|
330
|
+
'Regressions' => 'red',
|
|
331
|
+
'Critical Failures' => 'red',
|
|
332
|
+
'Healthy (Green Zone)' => 'green',
|
|
333
|
+
'The Danger Zone' => 'orange',
|
|
334
|
+
'General / Unsorted' => 'grey'
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
target_status = status_map[cat[:name]]
|
|
338
|
+
assigned_files = files.select { |f| f[:status] == target_status }
|
|
339
|
+
|
|
340
|
+
{
|
|
341
|
+
name: cat[:name],
|
|
342
|
+
type: 'directory',
|
|
343
|
+
path: "/coverage/#{cat[:name].parameterize}",
|
|
344
|
+
icon: cat[:icon],
|
|
345
|
+
iconColor: cat[:color],
|
|
346
|
+
children: assigned_files,
|
|
347
|
+
health: calculate_virtual_health(assigned_files)
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
{ name: 'Coverage', type: 'directory', path: '/coverage', icon: 'icon-activity qss-icon-activity', children: virtual_children.reject { |c| c[:children].empty? } }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def self.flatten_files(nodes)
|
|
357
|
+
return [] if nodes.nil?
|
|
358
|
+
list = []
|
|
359
|
+
nodes.each do |node|
|
|
360
|
+
if node[:type] == 'file'
|
|
361
|
+
list << node
|
|
362
|
+
else
|
|
363
|
+
# It's a directory, recurse into children
|
|
364
|
+
list += flatten_files(node[:children])
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
list
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def self.calculate_virtual_health(files)
|
|
371
|
+
return nil if files.empty?
|
|
372
|
+
counts = files.group_by { |f| f[:status] }.transform_values { |v| (v.count.to_f / files.count * 100).round }
|
|
373
|
+
counts.select { |k, v| v > 0 }
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def self.get_categories(type)
|
|
377
|
+
base = case type
|
|
378
|
+
when 'registry'
|
|
379
|
+
[
|
|
380
|
+
{ name: 'Identity & Access', icon: 'icon-shield-check', color: 'text-blue-400' },
|
|
381
|
+
{ name: 'Commerce & Billing', icon: 'icon-credit-card', color: 'text-emerald-500' },
|
|
382
|
+
{ name: 'Inventory & Catalog', icon: 'icon-package', color: 'text-amber-500' },
|
|
383
|
+
{ name: 'Engagement & Comms', icon: 'icon-mail', color: 'text-blue-500' },
|
|
384
|
+
{ name: 'Infrastructure', icon: 'icon-cpu', color: 'text-slate-400' }
|
|
385
|
+
]
|
|
386
|
+
when 'coverage'
|
|
387
|
+
[
|
|
388
|
+
{ name: 'Regressions', icon: 'icon-git-branch', color: 'text-red-600' },
|
|
389
|
+
{ name: 'Critical Failures', icon: 'icon-alert-octagon', color: 'text-red-500' },
|
|
390
|
+
{ name: 'Healthy (Green Zone)', icon: 'icon-check-circle', color: 'text-emerald-500' },
|
|
391
|
+
{ name: 'The Danger Zone', icon: 'icon-zap', color: 'text-yellow-500' }
|
|
392
|
+
]
|
|
393
|
+
else
|
|
394
|
+
[]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Always add a catch-all for teaching/realism
|
|
398
|
+
base << { name: 'General / Unsorted', icon: 'icon-archive', color: 'text-slate-500' }
|
|
399
|
+
base
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
class FileSaveService
|
|
5
|
+
def initialize(user:, full_path:, relative_path:, content:, performed_via: "human")
|
|
6
|
+
@user = user
|
|
7
|
+
@full_path = full_path
|
|
8
|
+
@relative_path = relative_path
|
|
9
|
+
@content = content
|
|
10
|
+
@performed_via = performed_via
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def save!
|
|
14
|
+
FileUtils.mkdir_p(File.dirname(@full_path))
|
|
15
|
+
File.write(@full_path, @content)
|
|
16
|
+
GitSideRefCommitter.new.record!(user: @user, path: @full_path)
|
|
17
|
+
DraftRecoveryStore.clear!(user: @user, path: @relative_path)
|
|
18
|
+
ActivityWriter.file_saved!(user: @user, path: @relative_path, performed_via: @performed_via)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require "digest"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
|
|
6
|
+
module Parapet
|
|
7
|
+
class GitSideRefCommitter
|
|
8
|
+
def initialize(root_path: Parapet::Engine.root.to_s)
|
|
9
|
+
@root_path = root_path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def record!(user:, path:)
|
|
13
|
+
repository_root = capture!("rev-parse", "--show-toplevel").strip
|
|
14
|
+
absolute_path = File.expand_path(path, @root_path)
|
|
15
|
+
relative_path = Pathname.new(absolute_path).relative_path_from(Pathname.new(repository_root)).to_s.tr("\\", "/")
|
|
16
|
+
raise ArgumentError, "File is outside the repository" if relative_path == ".." || relative_path.start_with?("../")
|
|
17
|
+
|
|
18
|
+
ref = "refs/parapet/file-editor-saves/#{user.id}/#{Digest::SHA256.hexdigest(relative_path)}"
|
|
19
|
+
parent = capture("rev-parse", "--verify", ref, chdir: repository_root)
|
|
20
|
+
parent_id = parent[:status].success? ? parent[:stdout].strip : nil
|
|
21
|
+
|
|
22
|
+
Dir.mktmpdir("parapet-index-") do |directory|
|
|
23
|
+
index_path = File.join(directory, "index")
|
|
24
|
+
env = { "GIT_INDEX_FILE" => index_path }
|
|
25
|
+
capture!("read-tree", "--empty", env: env, chdir: repository_root)
|
|
26
|
+
capture!("add", "-f", "--", relative_path, env: env, chdir: repository_root)
|
|
27
|
+
tree = capture!("write-tree", env: env, chdir: repository_root).strip
|
|
28
|
+
commit_args = [ "-c", "user.name=Parapet", "-c", "user.email=parapet@localhost", "commit-tree", tree ]
|
|
29
|
+
commit_args.concat([ "-p", parent_id ]) if parent_id
|
|
30
|
+
commit = capture!(*commit_args, input: "Parapet file save\n\nUser-ID: #{user.id}\n", chdir: repository_root).strip
|
|
31
|
+
capture!("update-ref", ref, commit, chdir: repository_root)
|
|
32
|
+
commit
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def capture(*args, env: {}, input: nil, chdir: @root_path)
|
|
39
|
+
stdout, stderr, status = Open3.capture3(env, "git", *args, chdir: chdir, stdin_data: input)
|
|
40
|
+
{ stdout: stdout, stderr: stderr, status: status }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def capture!(*args, env: {}, input: nil, chdir: @root_path)
|
|
44
|
+
result = capture(*args, env: env, input: input, chdir: chdir)
|
|
45
|
+
raise "Git operation failed: #{result[:stderr]}" unless result[:status].success?
|
|
46
|
+
|
|
47
|
+
result[:stdout]
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
# Discovers the QSS nouns (element ids) a view file declares, so the CSS Suite can list the
|
|
3
|
+
# elements on the page in view.
|
|
4
|
+
#
|
|
5
|
+
# PLACEHOLDER. Noun Discovery is shelved until the CSS Suite is built (decision 2026-09-25).
|
|
6
|
+
# It discovers nothing yet. Callers expect a list, so it returns an empty one, and it says so
|
|
7
|
+
# in the log (once per process) so the gap is never silent. The pending example in
|
|
8
|
+
# spec/services/parapet/noun_discovery_spec.rb describes the real behavior and starts failing
|
|
9
|
+
# loudly, as RSpec requires, the moment that behavior exists.
|
|
10
|
+
class NounDiscovery
|
|
11
|
+
REMINDER = "[Parapet] Noun Discovery is a placeholder (shelved until the CSS Suite): " \
|
|
12
|
+
"Parapet::NounDiscovery.discover returns no nouns, so registry views list none."
|
|
13
|
+
|
|
14
|
+
def self.discover(_path)
|
|
15
|
+
remind_once
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.remind_once
|
|
20
|
+
return if @reminded
|
|
21
|
+
@reminded = true
|
|
22
|
+
Rails.logger&.warn(REMINDER)
|
|
23
|
+
end
|
|
24
|
+
private_class_method :remind_once
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
class PublishGate
|
|
5
|
+
class Result
|
|
6
|
+
attr_reader :run_history
|
|
7
|
+
|
|
8
|
+
def initialize(run_history)
|
|
9
|
+
@run_history = run_history
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def passed?
|
|
13
|
+
@run_history.passed?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def initialize(triggered_by_actor_type: "human", user: Current.user, performed_via: nil)
|
|
18
|
+
@actor_type = triggered_by_actor_type
|
|
19
|
+
@user = user
|
|
20
|
+
@performed_via = performed_via || (triggered_by_actor_type == "ai_agent" ? "ai_agent" : "human")
|
|
21
|
+
# Use Rails.root to point to the host application, but fallback to Engine root
|
|
22
|
+
# if we are in the engine's own development environment (where Rails.root is dummy)
|
|
23
|
+
@root_path = if defined?(Rails) && Rails.root.join('spec').exist?
|
|
24
|
+
Rails.root.to_s
|
|
25
|
+
elsif defined?(Rails) && Rails.root.join('features').exist?
|
|
26
|
+
Rails.root.to_s
|
|
27
|
+
else
|
|
28
|
+
Parapet::Engine.root.to_s
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def run
|
|
33
|
+
run_history = Parapet::RunHistory.create!(
|
|
34
|
+
run_type: "publish",
|
|
35
|
+
status: "running",
|
|
36
|
+
triggered_by_actor_type: @actor_type,
|
|
37
|
+
user_id: @user&.id,
|
|
38
|
+
performed_via: @performed_via,
|
|
39
|
+
started_at: Time.current,
|
|
40
|
+
command: "bundle exec rspec && bundle exec cucumber"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
full_output = ""
|
|
44
|
+
final_status = "passed"
|
|
45
|
+
final_exit_code = 0
|
|
46
|
+
|
|
47
|
+
# Determine which commands to run based on presence of directories
|
|
48
|
+
commands = []
|
|
49
|
+
commands << "bundle exec rspec" if Dir.exist?(File.join(@root_path, 'spec'))
|
|
50
|
+
commands << "bundle exec cucumber" if Dir.exist?(File.join(@root_path, 'features'))
|
|
51
|
+
|
|
52
|
+
# If no commands to run, it passes by default (or should it fail? Spec implies both run)
|
|
53
|
+
# Spec says: "Run RSpec, then Cucumber... fail fast"
|
|
54
|
+
|
|
55
|
+
commands.each do |cmd|
|
|
56
|
+
stdout, stderr, status = Open3.capture3(cmd, chdir: @root_path)
|
|
57
|
+
full_output += stdout + stderr
|
|
58
|
+
|
|
59
|
+
if status.exitstatus != 0
|
|
60
|
+
final_status = "failed"
|
|
61
|
+
final_exit_code = status.exitstatus
|
|
62
|
+
break
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
run_history.update!(
|
|
67
|
+
status: final_status,
|
|
68
|
+
exit_code: final_exit_code,
|
|
69
|
+
raw_output: full_output,
|
|
70
|
+
finished_at: Time.current
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
ActivityWriter.run!(
|
|
74
|
+
run_history: run_history,
|
|
75
|
+
user: @user,
|
|
76
|
+
action: "publish",
|
|
77
|
+
performed_via: @performed_via
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
Result.new(run_history)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|