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,12 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class User < InternalRecord
|
|
3
|
+
self.table_name = "users"
|
|
4
|
+
|
|
5
|
+
has_secure_password
|
|
6
|
+
has_many :sessions, dependent: :destroy
|
|
7
|
+
|
|
8
|
+
normalizes :email_address, with: ->(email_address) { email_address.strip.downcase }
|
|
9
|
+
|
|
10
|
+
validates :email_address, presence: true, uniqueness: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class ActivityPhraseRenderer
|
|
3
|
+
def initialize(view_context:)
|
|
4
|
+
@view_context = view_context
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def render(activity:, viewer:)
|
|
8
|
+
activity_type = activity.activity_type.to_s
|
|
9
|
+
action = activity.action.to_s
|
|
10
|
+
partial = [
|
|
11
|
+
"parapet/activities/#{activity_type}/#{action}",
|
|
12
|
+
"parapet/activities/#{activity_type}",
|
|
13
|
+
"parapet/activities/generic"
|
|
14
|
+
].find { |candidate| @view_context.lookup_context.exists?(candidate, [], true) }
|
|
15
|
+
|
|
16
|
+
@view_context.render(
|
|
17
|
+
partial: partial,
|
|
18
|
+
locals: {
|
|
19
|
+
activity: activity,
|
|
20
|
+
actor_name: actor_name(activity, viewer),
|
|
21
|
+
viewer: viewer
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def actor_name(activity, viewer)
|
|
29
|
+
return "Parapet" unless activity.user
|
|
30
|
+
return "You" if viewer && activity.user_id == viewer.id
|
|
31
|
+
|
|
32
|
+
activity.user.email_address
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class ActivityWriter
|
|
3
|
+
def self.run!(run_history:, user:, action:, performed_via:)
|
|
4
|
+
return unless user
|
|
5
|
+
|
|
6
|
+
Activity.create!(
|
|
7
|
+
user: user,
|
|
8
|
+
activity_type: "run_history",
|
|
9
|
+
action: action,
|
|
10
|
+
performed_via: performed_via,
|
|
11
|
+
trackable: run_history,
|
|
12
|
+
details: {}
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.file_saved!(user:, path:, performed_via: "human")
|
|
17
|
+
Activity.create!(
|
|
18
|
+
user: user,
|
|
19
|
+
activity_type: "file_editor",
|
|
20
|
+
action: "file_saved",
|
|
21
|
+
performed_via: performed_via,
|
|
22
|
+
details: { "file_path" => path }
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Parapet
|
|
4
|
+
module Authorization
|
|
5
|
+
class Error < StandardError
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class NotPmCapableError < Error
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class DelegateLimitExceededError < Error
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class LastDelegateCannotBeRevokedError < Error
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class TierCeilingExceededError < Error
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class BootstrapAlreadyCompletedError < Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class InvalidConfigurationError < Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
DELEGATE_SETTING_KEY = "delegate_ids"
|
|
27
|
+
MAX_DELEGATES = 2
|
|
28
|
+
|
|
29
|
+
TIER_RANKS = {
|
|
30
|
+
"novice" => 0,
|
|
31
|
+
"intermediate" => 1,
|
|
32
|
+
"advanced" => 2,
|
|
33
|
+
"power" => 3
|
|
34
|
+
}.freeze
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def authorized?(user, suite, min_tier: :novice)
|
|
38
|
+
return false unless valid_identity?(user)
|
|
39
|
+
|
|
40
|
+
suite_name = suite.to_s
|
|
41
|
+
minimum_rank = TIER_RANKS[min_tier.to_s]
|
|
42
|
+
return false unless SuiteAssignment::SUITES.include?(suite_name) && minimum_rank
|
|
43
|
+
|
|
44
|
+
effective_tier = tier_for(user, suite_name)
|
|
45
|
+
effective_tier && TIER_RANKS.fetch(effective_tier) >= minimum_rank
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tier_for(user, suite)
|
|
49
|
+
return unless valid_identity?(user)
|
|
50
|
+
|
|
51
|
+
suite_name = suite.to_s
|
|
52
|
+
return unless SuiteAssignment::SUITES.include?(suite_name)
|
|
53
|
+
|
|
54
|
+
assignment = SuiteAssignment.find_by(user_id: user.id, suite: suite_name)
|
|
55
|
+
tier = assignment&.tier || ("novice" if suite_name == "intentions_suite")
|
|
56
|
+
assigned_rank = TIER_RANKS[tier]
|
|
57
|
+
ceiling_rank = TIER_RANKS[license_ceiling.to_s]
|
|
58
|
+
return unless assigned_rank && ceiling_rank
|
|
59
|
+
|
|
60
|
+
TIER_RANKS.key([ assigned_rank, ceiling_rank ].min)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def role_for(user, suite)
|
|
64
|
+
return unless valid_identity?(user)
|
|
65
|
+
|
|
66
|
+
suite_name = suite.to_s
|
|
67
|
+
return unless SuiteAssignment::SUITES.include?(suite_name)
|
|
68
|
+
|
|
69
|
+
SuiteAssignment.find_by(user_id: user.id, suite: suite_name)&.role
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def pm_capable?(user)
|
|
73
|
+
valid_identity?(user) && delegate_ids.include?(user.id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def bootstrap_owner!(owner:)
|
|
77
|
+
ensure_identity!(owner)
|
|
78
|
+
|
|
79
|
+
RuntimeSetting.transaction do
|
|
80
|
+
setting = RuntimeSetting.lock.find_by(setting_key: DELEGATE_SETTING_KEY)
|
|
81
|
+
|
|
82
|
+
if setting
|
|
83
|
+
ids = parse_delegate_ids(setting.value)
|
|
84
|
+
raise BootstrapAlreadyCompletedError unless ids.include?(owner.id)
|
|
85
|
+
else
|
|
86
|
+
RuntimeSetting.create!(
|
|
87
|
+
setting_key: DELEGATE_SETTING_KEY,
|
|
88
|
+
value: JSON.generate([ owner.id ])
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
owner
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def grant_pm!(actor:, user:)
|
|
97
|
+
require_pm!(actor)
|
|
98
|
+
ensure_identity!(user)
|
|
99
|
+
|
|
100
|
+
update_delegate_ids do |ids|
|
|
101
|
+
next ids if ids.include?(user.id)
|
|
102
|
+
raise DelegateLimitExceededError if ids.length >= MAX_DELEGATES
|
|
103
|
+
|
|
104
|
+
ids + [ user.id ]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
user
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def revoke_pm!(actor:, user:)
|
|
111
|
+
require_pm!(actor)
|
|
112
|
+
ensure_identity!(user)
|
|
113
|
+
|
|
114
|
+
update_delegate_ids do |ids|
|
|
115
|
+
raise LastDelegateCannotBeRevokedError if ids == [ user.id ]
|
|
116
|
+
|
|
117
|
+
ids.reject { |id| id == user.id }
|
|
118
|
+
end
|
|
119
|
+
user
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def assign!(actor:, user:, suite:, role: nil, tier: nil)
|
|
123
|
+
require_pm!(actor)
|
|
124
|
+
ensure_identity!(user)
|
|
125
|
+
|
|
126
|
+
suite_name = suite.to_s
|
|
127
|
+
raise ArgumentError, "Unknown suite" unless SuiteAssignment::SUITES.include?(suite_name)
|
|
128
|
+
raise ArgumentError, "Provide a role, a tier, or both" if role.nil? && tier.nil?
|
|
129
|
+
|
|
130
|
+
tier_name = tier&.to_s
|
|
131
|
+
if tier_name
|
|
132
|
+
tier_rank = TIER_RANKS[tier_name]
|
|
133
|
+
ceiling_rank = TIER_RANKS[license_ceiling.to_s]
|
|
134
|
+
raise ArgumentError, "Unknown tier" unless tier_rank
|
|
135
|
+
raise TierCeilingExceededError if tier_rank > ceiling_rank
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
assignment = SuiteAssignment.find_or_initialize_by(user: user, suite: suite_name)
|
|
139
|
+
assignment.role = role.to_s unless role.nil?
|
|
140
|
+
assignment.tier = tier_name if tier_name
|
|
141
|
+
assignment.save!
|
|
142
|
+
assignment
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def license_ceiling
|
|
146
|
+
:power
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def delegate_ids
|
|
152
|
+
setting = RuntimeSetting.find_by(setting_key: DELEGATE_SETTING_KEY)
|
|
153
|
+
setting ? parse_delegate_ids(setting.value) : []
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def parse_delegate_ids(value)
|
|
157
|
+
ids = JSON.parse(value)
|
|
158
|
+
unless ids.is_a?(Array) && ids.length <= MAX_DELEGATES && ids.uniq == ids && ids.all? { |id| id.is_a?(Integer) && id.positive? }
|
|
159
|
+
raise InvalidConfigurationError
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
ids
|
|
163
|
+
rescue JSON::ParserError, TypeError
|
|
164
|
+
raise InvalidConfigurationError
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def update_delegate_ids
|
|
168
|
+
RuntimeSetting.transaction do
|
|
169
|
+
setting = RuntimeSetting.lock.find_by(setting_key: DELEGATE_SETTING_KEY)
|
|
170
|
+
ids = setting ? parse_delegate_ids(setting.value) : []
|
|
171
|
+
updated_ids = yield(ids)
|
|
172
|
+
raise DelegateLimitExceededError if updated_ids.length > MAX_DELEGATES
|
|
173
|
+
|
|
174
|
+
value = JSON.generate(updated_ids)
|
|
175
|
+
if setting
|
|
176
|
+
setting.update!(value: value)
|
|
177
|
+
else
|
|
178
|
+
RuntimeSetting.create!(setting_key: DELEGATE_SETTING_KEY, value: value)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def require_pm!(user)
|
|
184
|
+
raise NotPmCapableError unless pm_capable?(user)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def ensure_identity!(user)
|
|
188
|
+
unless valid_identity?(user)
|
|
189
|
+
raise ArgumentError, "User must be a persisted account with credentials"
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def valid_identity?(user)
|
|
194
|
+
user.is_a?(User) && user.persisted? && user.password_digest.present?
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'nokogiri'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Parapet
|
|
6
|
+
class ConsistencyAuditor
|
|
7
|
+
CONSISTENT_IDS = [
|
|
8
|
+
'pp-header',
|
|
9
|
+
'pp-footer',
|
|
10
|
+
'pp-global-nav',
|
|
11
|
+
'pp-explorer-bar',
|
|
12
|
+
'pp-inspector-bar'
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def self.audit_project(root_path)
|
|
16
|
+
fingerprints = {} # id => { hash => [files] }
|
|
17
|
+
|
|
18
|
+
# Normalize path for Windows/Unix consistency
|
|
19
|
+
base_dir = File.expand_path(root_path)
|
|
20
|
+
pattern = File.join(base_dir, 'app', 'views', '**', '*.html.erb').gsub('\\', '/')
|
|
21
|
+
|
|
22
|
+
Dir.glob(pattern).each do |file|
|
|
23
|
+
next if file.include?('/shared/') # Skip partials as they are the source of truth
|
|
24
|
+
|
|
25
|
+
content = File.read(file) rescue next
|
|
26
|
+
doc = Nokogiri::HTML.fragment(content.gsub(/<%.*?%>/m, '<!--ERB-->'))
|
|
27
|
+
|
|
28
|
+
CONSISTENT_IDS.each do |id|
|
|
29
|
+
node = doc.at_css("##{id}")
|
|
30
|
+
next unless node
|
|
31
|
+
|
|
32
|
+
# Normalize node for comparison (remove whitespace/comments)
|
|
33
|
+
normalized = node.to_s.gsub(/\s+/, ' ').strip
|
|
34
|
+
hash = Digest::SHA256.hexdigest(normalized)
|
|
35
|
+
|
|
36
|
+
fingerprints[id] ||= {}
|
|
37
|
+
fingerprints[id][hash] ||= []
|
|
38
|
+
fingerprints[id][hash] << file.sub(base_dir, '')
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
issues = []
|
|
43
|
+
fingerprints.each do |id, variants|
|
|
44
|
+
if variants.size > 1
|
|
45
|
+
issues << {
|
|
46
|
+
type: 'consistency',
|
|
47
|
+
id: id,
|
|
48
|
+
message: "Structural Drift Detected: Element ##{id} has #{variants.size} different implementations across your views.",
|
|
49
|
+
variants: variants,
|
|
50
|
+
severity: 'red'
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ status: issues.empty? ? 'green' : 'red', issues: issues }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class DirectoryScanner
|
|
3
|
+
def initialize(root_path)
|
|
4
|
+
@root_path = root_path
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def scan
|
|
8
|
+
return nil unless File.directory?(@root_path)
|
|
9
|
+
|
|
10
|
+
{
|
|
11
|
+
name: File.basename(@root_path),
|
|
12
|
+
type: 'directory',
|
|
13
|
+
path: '/',
|
|
14
|
+
icon: 'icon-folder qss-icon-folder',
|
|
15
|
+
children: scan_directory(@root_path, '')
|
|
16
|
+
}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def scan_directory(current_path, relative_path)
|
|
22
|
+
items = []
|
|
23
|
+
|
|
24
|
+
# Directories to ignore (case-insensitive)
|
|
25
|
+
ignore_list = [ '.', '..', '.git', '.bundle', 'node_modules', 'tmp', 'log' ]
|
|
26
|
+
|
|
27
|
+
Dir.foreach(current_path) do |item|
|
|
28
|
+
next if ignore_list.any? { |ignored| item.downcase == ignored.downcase }
|
|
29
|
+
next if item == '.keep'
|
|
30
|
+
next if item.start_with?('.') && item != '.zencoder' # Keep our own folder for dogfooding
|
|
31
|
+
|
|
32
|
+
full_path = File.join(current_path, item)
|
|
33
|
+
item_relative_path = if relative_path.empty?
|
|
34
|
+
item
|
|
35
|
+
else
|
|
36
|
+
File.join(relative_path, item)
|
|
37
|
+
end.gsub('\\', '/')
|
|
38
|
+
|
|
39
|
+
if File.directory?(full_path)
|
|
40
|
+
children = scan_directory(full_path, item_relative_path)
|
|
41
|
+
health = calculate_health(children)
|
|
42
|
+
|
|
43
|
+
items << {
|
|
44
|
+
name: item,
|
|
45
|
+
type: 'directory',
|
|
46
|
+
path: item_relative_path,
|
|
47
|
+
icon: 'icon-folder',
|
|
48
|
+
children: children,
|
|
49
|
+
health: health
|
|
50
|
+
}
|
|
51
|
+
else
|
|
52
|
+
ext = File.extname(item).delete('.')
|
|
53
|
+
status = determine_status(full_path, item, ext)
|
|
54
|
+
|
|
55
|
+
# Detect Sidecar (.qss) for View files
|
|
56
|
+
sidecar = nil
|
|
57
|
+
if [ 'html', 'erb' ].include?(ext)
|
|
58
|
+
qss_item = item.sub(/\.(html\.erb|erb)$/, '.qss')
|
|
59
|
+
if File.exist?(File.join(current_path, qss_item))
|
|
60
|
+
sidecar = File.join(relative_path, qss_item).gsub('\\', '/')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
items << {
|
|
65
|
+
name: item,
|
|
66
|
+
type: 'file',
|
|
67
|
+
path: item_relative_path,
|
|
68
|
+
ext: ext,
|
|
69
|
+
icon: icon_for_file(item, ext),
|
|
70
|
+
iconColor: color_for_file(item, ext),
|
|
71
|
+
status: status,
|
|
72
|
+
children: [],
|
|
73
|
+
sidecar: sidecar
|
|
74
|
+
}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
items.sort_by { |i| [ i[:type] == 'directory' ? 0 : 1, i[:name] ] }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def determine_status(full_path, name, ext)
|
|
82
|
+
# 1. Use QSS Auditor for view/style files
|
|
83
|
+
if [ 'html', 'erb', 'qss', 'css', 'scss' ].include?(ext)
|
|
84
|
+
return QSS::QssAuditor.audit_file(full_path)[:status]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# 2. Use stable "random" status for other files to simulate active project state
|
|
88
|
+
return nil unless is_test_file?(name) || ext == 'rb'
|
|
89
|
+
|
|
90
|
+
hash = name.hash % 100
|
|
91
|
+
if hash < 60
|
|
92
|
+
'green'
|
|
93
|
+
elsif hash < 85
|
|
94
|
+
'red'
|
|
95
|
+
elsif hash < 95
|
|
96
|
+
'orange'
|
|
97
|
+
else
|
|
98
|
+
'grey'
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def calculate_health(children)
|
|
103
|
+
return nil if children.empty?
|
|
104
|
+
|
|
105
|
+
stats = { 'red' => 0, 'green' => 0, 'orange' => 0, 'grey' => 0 }
|
|
106
|
+
count = 0
|
|
107
|
+
|
|
108
|
+
children.each do |child|
|
|
109
|
+
if child[:type] == 'file'
|
|
110
|
+
if child[:status]
|
|
111
|
+
stats[child[:status]] += 1
|
|
112
|
+
count += 1
|
|
113
|
+
end
|
|
114
|
+
elsif child[:type] == 'directory' && child[:health]
|
|
115
|
+
# Factor in directory health
|
|
116
|
+
child[:health].each do |status, percent|
|
|
117
|
+
stats[status] += (percent / 100.0)
|
|
118
|
+
end
|
|
119
|
+
count += 1
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
return nil if count == 0
|
|
124
|
+
|
|
125
|
+
# Convert to percentages
|
|
126
|
+
stats.transform_values { |v| (v / count.to_f * 100).round }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def is_test_file?(name)
|
|
130
|
+
if Parapet.test_framework == :rspec
|
|
131
|
+
name.end_with?('_spec.rb')
|
|
132
|
+
else
|
|
133
|
+
name.end_with?('_test.rb')
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def icon_for_file(name, ext)
|
|
138
|
+
return 'icon-gem' if is_test_file?(name)
|
|
139
|
+
|
|
140
|
+
case ext
|
|
141
|
+
when 'rb' then 'icon-gem'
|
|
142
|
+
when 'feature' then 'icon-list-checks'
|
|
143
|
+
when 'json' then 'icon-database'
|
|
144
|
+
else 'icon-file'
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def color_for_file(name, ext)
|
|
149
|
+
return 'qss-text-amber-500' if is_test_file?(name)
|
|
150
|
+
|
|
151
|
+
case ext
|
|
152
|
+
when 'rb' then 'qss-text-red-400'
|
|
153
|
+
when 'feature' then 'qss-text-emerald-400'
|
|
154
|
+
when 'json' then 'qss-text-indigo-300'
|
|
155
|
+
else 'qss-text-slate-400'
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Parapet
|
|
2
|
+
class DraftRecoveryStore
|
|
3
|
+
def self.find(user:, path:)
|
|
4
|
+
DraftRecovery.find_by(user_id: user.id, file_path: path)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.save!(user:, path:, content:)
|
|
8
|
+
draft = DraftRecovery.find_or_initialize_by(user_id: user.id, file_path: path)
|
|
9
|
+
draft.content = content
|
|
10
|
+
draft.save!
|
|
11
|
+
draft
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.clear!(user:, path:)
|
|
15
|
+
DraftRecovery.where(user_id: user.id, file_path: path).delete_all
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|