alembic 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 +105 -0
- data/Rakefile +6 -0
- data/app/assets/builds/alembic/canvas.js +1 -0
- data/app/assets/builds/alembic/page_builder.css +1 -0
- data/app/assets/builds/alembic/page_builder.js +5 -0
- data/app/assets/stylesheets/alembic/application.css +15 -0
- data/app/controllers/alembic/application_controller.rb +32 -0
- data/app/controllers/alembic/flows_controller.rb +104 -0
- data/app/controllers/alembic/manage/base_controller.rb +16 -0
- data/app/controllers/alembic/manage/canvas_controller.rb +183 -0
- data/app/controllers/alembic/manage/definitions_controller.rb +15 -0
- data/app/controllers/alembic/manage/flows_controller.rb +55 -0
- data/app/controllers/alembic/manage/page_blocks_controller.rb +19 -0
- data/app/controllers/alembic/manage/pages_controller.rb +38 -0
- data/app/controllers/alembic/manage/previews_controller.rb +38 -0
- data/app/controllers/alembic/manage/versions_controller.rb +22 -0
- data/app/controllers/concerns/alembic/manage/authenticates_admin.rb +19 -0
- data/app/controllers/concerns/alembic/manage/draws_canvas.rb +35 -0
- data/app/helpers/alembic/application_helper.rb +11 -0
- data/app/javascript/alembic/canvas/Canvas.jsx +197 -0
- data/app/javascript/alembic/canvas/Connector.jsx +30 -0
- data/app/javascript/alembic/canvas/ConnectorLayer.jsx +31 -0
- data/app/javascript/alembic/canvas/Control.jsx +42 -0
- data/app/javascript/alembic/canvas/Inspector.jsx +44 -0
- data/app/javascript/alembic/canvas/Panel.jsx +67 -0
- data/app/javascript/alembic/canvas/Placeholder.jsx +24 -0
- data/app/javascript/alembic/canvas/Port.jsx +16 -0
- data/app/javascript/alembic/canvas/Records.jsx +34 -0
- data/app/javascript/alembic/canvas/StepCard.jsx +57 -0
- data/app/javascript/alembic/canvas/Toolbar.jsx +19 -0
- data/app/javascript/alembic/canvas/TypePicker.jsx +21 -0
- data/app/javascript/alembic/canvas/changes.js +5 -0
- data/app/javascript/alembic/canvas/choices.js +4 -0
- data/app/javascript/alembic/canvas/flow.js +7 -0
- data/app/javascript/alembic/canvas/flowSender.js +22 -0
- data/app/javascript/alembic/canvas/ids.js +7 -0
- data/app/javascript/alembic/canvas/rows.js +2 -0
- data/app/javascript/alembic/canvas/styles.js +3 -0
- data/app/javascript/alembic/canvas/useConnectors.js +75 -0
- data/app/javascript/alembic/canvas/useFlow.js +17 -0
- data/app/javascript/alembic/canvas.jsx +13 -0
- data/app/javascript/alembic/page_builder/PageBuilder.jsx +18 -0
- data/app/javascript/alembic/page_builder.jsx +10 -0
- data/app/jobs/alembic/application_job.rb +4 -0
- data/app/mailers/alembic/application_mailer.rb +6 -0
- data/app/models/alembic/application_record.rb +5 -0
- data/app/models/alembic/asked.rb +3 -0
- data/app/models/alembic/choice.rb +7 -0
- data/app/models/alembic/flow/admission.rb +19 -0
- data/app/models/alembic/flow/canvas.rb +167 -0
- data/app/models/alembic/flow/change.rb +18 -0
- data/app/models/alembic/flow/condition.rb +24 -0
- data/app/models/alembic/flow/definition.rb +132 -0
- data/app/models/alembic/flow/digest.rb +116 -0
- data/app/models/alembic/flow/document.rb +131 -0
- data/app/models/alembic/flow/drawing.rb +15 -0
- data/app/models/alembic/flow/edge.rb +9 -0
- data/app/models/alembic/flow/edit_history.rb +75 -0
- data/app/models/alembic/flow/layout.rb +88 -0
- data/app/models/alembic/flow/name.rb +15 -0
- data/app/models/alembic/flow/node.rb +5 -0
- data/app/models/alembic/flow/output.rb +23 -0
- data/app/models/alembic/flow/progress/kept.rb +38 -0
- data/app/models/alembic/flow/progress/loose.rb +43 -0
- data/app/models/alembic/flow/progress.rb +11 -0
- data/app/models/alembic/flow/registry.rb +25 -0
- data/app/models/alembic/flow/run.rb +54 -0
- data/app/models/alembic/flow/runner.rb +65 -0
- data/app/models/alembic/flow/settings.rb +80 -0
- data/app/models/alembic/flow/start.rb +13 -0
- data/app/models/alembic/flow/step.rb +42 -0
- data/app/models/alembic/flow/step_type/declaration.rb +118 -0
- data/app/models/alembic/flow/step_type.rb +69 -0
- data/app/models/alembic/flow/summaries.rb +44 -0
- data/app/models/alembic/flow/summary_version.rb +13 -0
- data/app/models/alembic/flow/switch.rb +17 -0
- data/app/models/alembic/flow/terminal.rb +13 -0
- data/app/models/alembic/flow/validator.rb +171 -0
- data/app/models/alembic/flow/version.rb +26 -0
- data/app/models/alembic/flow/violation.rb +9 -0
- data/app/models/alembic/flow.rb +44 -0
- data/app/models/alembic/outputs/band.rb +22 -0
- data/app/models/alembic/outputs/grouped.rb +26 -0
- data/app/models/alembic/outputs/lowest.rb +20 -0
- data/app/models/alembic/outputs/percentage.rb +31 -0
- data/app/models/alembic/outputs/tally.rb +23 -0
- data/app/models/alembic/outputs/weighted_sum.rb +26 -0
- data/app/models/alembic/page.rb +11 -0
- data/app/models/alembic/runner.rb +13 -0
- data/app/models/alembic/steps/question.rb +52 -0
- data/app/models/alembic/summary/output_type.rb +40 -0
- data/app/models/alembic/summary/registry.rb +25 -0
- data/app/models/alembic/summary/report.rb +31 -0
- data/app/models/alembic/summary/result.rb +5 -0
- data/app/models/alembic/summary/run.rb +13 -0
- data/app/models/alembic/summary.rb +15 -0
- data/app/views/alembic/flow/steps/_choosing.html.erb +9 -0
- data/app/views/alembic/flows/complete.html.erb +34 -0
- data/app/views/alembic/flows/show.html.erb +17 -0
- data/app/views/alembic/flows/step.html.erb +27 -0
- data/app/views/alembic/manage/definitions/edit.html.erb +13 -0
- data/app/views/alembic/manage/flows/edit.html.erb +29 -0
- data/app/views/alembic/manage/flows/index.html.erb +32 -0
- data/app/views/alembic/manage/flows/show.html.erb +26 -0
- data/app/views/alembic/manage/pages/_block_content.html.erb +1 -0
- data/app/views/alembic/manage/pages/index.html.erb +20 -0
- data/app/views/alembic/manage/pages/show.html.erb +11 -0
- data/app/views/alembic/manage/versions/index.html.erb +41 -0
- data/app/views/layouts/alembic/application.html.erb +17 -0
- data/config/routes.rb +42 -0
- data/db/migrate/20260604215031_create_alembic_diagnostics.rb +14 -0
- data/db/migrate/20260605015051_create_alembic_questions.rb +12 -0
- data/db/migrate/20260605021526_create_alembic_options.rb +14 -0
- data/db/migrate/20260605023308_create_alembic_conditions.rb +11 -0
- data/db/migrate/20260605025623_create_alembic_bands.rb +12 -0
- data/db/migrate/20260605032859_create_alembic_condition_options.rb +10 -0
- data/db/migrate/20260605032900_reshape_conditions_for_placement.rb +11 -0
- data/db/migrate/20260605125119_create_alembic_results.rb +14 -0
- data/db/migrate/20260605130338_create_alembic_rules.rb +10 -0
- data/db/migrate/20260605130339_create_alembic_rule_results.rb +10 -0
- data/db/migrate/20260605210714_add_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260606000855_add_guide_copy_to_alembic_diagnostics.rb +9 -0
- data/db/migrate/20260606005430_create_alembic_nodes.rb +22 -0
- data/db/migrate/20260606010312_create_alembic_build_steps.rb +12 -0
- data/db/migrate/20260606011110_create_alembic_warnings.rb +11 -0
- data/db/migrate/20260804180000_create_alembic_domains.rb +14 -0
- data/db/migrate/20260804180100_add_domain_to_alembic_questions.rb +5 -0
- data/db/migrate/20260822120000_create_alembic_definition_versions.rb +14 -0
- data/db/migrate/20260822120100_backfill_alembic_definition_versions.rb +17 -0
- data/db/migrate/20260822130000_remove_definition_from_alembic_diagnostics.rb +28 -0
- data/db/migrate/20260822140000_create_alembic_responses.rb +14 -0
- data/db/migrate/20260824120000_create_alembic_transitions.rb +10 -0
- data/db/migrate/20260824130000_drop_alembic_builder_tables.rb +17 -0
- data/db/migrate/20260824140000_add_definition_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824160000_add_summary_definition_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260824170000_drop_alembic_guide_copy_columns.rb +8 -0
- data/db/migrate/20260825120000_create_alembic_summary_versions.rb +14 -0
- data/db/migrate/20260825130000_add_summary_cursor_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825140000_move_summary_definitions_into_versions.rb +26 -0
- data/db/migrate/20260825150000_drop_summary_definition_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825160000_add_summary_version_to_alembic_responses.rb +6 -0
- data/db/migrate/20260825170000_rename_question_settings_in_current_definitions.rb +51 -0
- data/db/migrate/20260825180000_add_document_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825190000_seed_documents_from_current_definitions.rb +29 -0
- data/db/migrate/20260825200000_add_published_version_to_alembic_diagnostics.rb +6 -0
- data/db/migrate/20260825210000_publish_current_definitions.rb +29 -0
- data/db/migrate/20260825220000_add_undone_changes_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825223322_add_status_to_alembic_definition_versions.rb +8 -0
- data/db/migrate/20260825224538_remove_published_version_from_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825225015_change_alembic_diagnostic_status_to_availability.rb +16 -0
- data/db/migrate/20260825230000_add_undo_history_to_alembic_diagnostics.rb +5 -0
- data/db/migrate/20260825230500_add_changes_to_alembic_definition_versions.rb +5 -0
- data/db/migrate/20260826230000_rename_alembic_responses_to_flow_runs.rb +5 -0
- data/db/migrate/20260826234500_rename_answers_to_recorded_on_flow_runs.rb +5 -0
- data/db/migrate/20260826235000_rename_definition_versions_to_flow_versions.rb +5 -0
- data/db/migrate/20260826235500_rename_summary_versions_to_flow_summaries.rb +5 -0
- data/db/migrate/20260827000000_rename_diagnostics_to_flows.rb +8 -0
- data/db/migrate/20260831000000_add_persists_to_alembic_flows.rb +5 -0
- data/db/migrate/20260914120000_create_alembic_pages.rb +9 -0
- data/db/migrate/20260915120000_add_blocks_to_alembic_pages.rb +5 -0
- data/lib/alembic/engine.rb +43 -0
- data/lib/alembic/leftover_stylesheet.rb +14 -0
- data/lib/alembic/version.rb +3 -0
- data/lib/alembic.rb +34 -0
- data/lib/tasks/alembic_tasks.rake +4 -0
- metadata +252 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Summaries
|
|
4
|
+
def initialize(flow)
|
|
5
|
+
@flow = flow
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def document
|
|
9
|
+
current_version&.summary
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def current_version
|
|
13
|
+
@flow.summary_versions.find_by(number: cursor)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def record(payload)
|
|
17
|
+
@flow.summary_versions.create!(number: next_number, summary: payload)
|
|
18
|
+
.tap { |version| @flow.update!(summary_cursor: version.number) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def any?
|
|
22
|
+
document.present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def of(state)
|
|
26
|
+
Summary::Report.new(document).results(Summary::Run.new(state: state, steps: steps_by_id))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def steps_by_id
|
|
32
|
+
Array(@flow.definition.to_h["nodes"]).index_by { |node| node["id"] }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def cursor
|
|
36
|
+
@flow.summary_cursor || @flow.summary_versions.pluck(:number).max
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def next_number
|
|
40
|
+
(@flow.summary_versions.maximum(:number) || 0) + 1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class SummaryVersion < ApplicationRecord
|
|
4
|
+
self.table_name = "alembic_flow_summaries"
|
|
5
|
+
|
|
6
|
+
belongs_to :flow, class_name: "Alembic::Flow::Definition"
|
|
7
|
+
|
|
8
|
+
validates :number, uniqueness: { scope: :flow_id }
|
|
9
|
+
|
|
10
|
+
before_update { raise ActiveRecord::ReadOnlyRecord }
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Switch
|
|
4
|
+
include Step
|
|
5
|
+
|
|
6
|
+
step_name "Switch"
|
|
7
|
+
|
|
8
|
+
setting :step, type: :previous_step
|
|
9
|
+
|
|
10
|
+
output :choice, type: :string, label: "Choice", from: :step
|
|
11
|
+
|
|
12
|
+
def route(node, state)
|
|
13
|
+
state[node.config["step"]]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Validator
|
|
4
|
+
OPTIONAL = { unrouted_value: :unrouted_values, unfollowed_path: :unfollowed_paths, dead_end: :dead_ends }.freeze
|
|
5
|
+
|
|
6
|
+
def initialize(document, registry: Flow.registry, checks: Flow.checks)
|
|
7
|
+
@document = document
|
|
8
|
+
@registry = registry
|
|
9
|
+
@checks = checks
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def violations
|
|
13
|
+
structural_violations + unmet_requirements + missing_settings + missing_values + beginnings + asked_for
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def structural_violations
|
|
17
|
+
malformations + unreachable
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def malformations
|
|
21
|
+
missing_edge_targets + missing_edge_sources + duplicate_ids + no_beginning
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def asked_for
|
|
27
|
+
@checks.flat_map { |name| send(OPTIONAL.fetch(name)) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def missing_edge_targets
|
|
31
|
+
@document.edges.reject { |edge| known?(edge.to) }
|
|
32
|
+
.map { |edge| Violation.new(node: edge.from, problem: :missing_edge_target, detail: edge.to) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def missing_edge_sources
|
|
36
|
+
@document.edges.reject { |edge| known?(edge.from) }
|
|
37
|
+
.map { |edge| Violation.new(node: edge.to, problem: :missing_edge_source, detail: edge.from) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def duplicate_ids
|
|
41
|
+
@document.nodes.map(&:id).tally.select { |_id, count| count > 1 }
|
|
42
|
+
.map { |id, _count| Violation.new(node: id, problem: :duplicate_id) }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def no_beginning
|
|
46
|
+
return [] if known?(@document.entry)
|
|
47
|
+
|
|
48
|
+
[ Violation.new(node: nil, problem: :no_beginning) ]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def unmet_requirements
|
|
52
|
+
@document.nodes.flat_map do |node|
|
|
53
|
+
requirements_for(node).reject { |required| precedes_every_path?(required, node.id) }
|
|
54
|
+
.map { |required| Violation.new(node: node.id, problem: :unmet_requirement, detail: required) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def dead_ends
|
|
59
|
+
@document.nodes.filter_map { |node| ending_problem(node) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def ending_problem(node)
|
|
63
|
+
leaving = @document.edges_from(node.id).any?
|
|
64
|
+
return Violation.new(node: node.id, problem: :past_the_end) if ends_here?(node) && leaving
|
|
65
|
+
return if ends_here?(node) || leaving
|
|
66
|
+
|
|
67
|
+
Violation.new(node: node.id, problem: :dead_end)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def ends_here?(node)
|
|
71
|
+
@registry.registered?(node.type) && @registry.fetch(node.type).ends_here?
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def unfollowed_paths
|
|
75
|
+
@document.nodes.select { |node| leads_on?(node) }.flat_map { |node| unfollowed_paths_from(node) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def leads_on?(node)
|
|
79
|
+
@registry.registered?(node.type) && !@registry.fetch(node.type).routes?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def unfollowed_paths_from(node)
|
|
83
|
+
@document.edges_from(node.id).drop(1)
|
|
84
|
+
.map { |edge| Violation.new(node: node.id, problem: :unfollowed_path, detail: edge.to) }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def unrouted_values
|
|
88
|
+
@document.nodes.flat_map { |node| unrouted_values_in(node) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def unrouted_values_in(node)
|
|
92
|
+
wired = @document.edges_from(node.id).map { |edge| edge.on.to_s }
|
|
93
|
+
|
|
94
|
+
digest.routing_values(node.id).reject { |value| wired.include?(value) }
|
|
95
|
+
.map { |value| Violation.new(node: node.id, problem: :unrouted_value, detail: value) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def digest
|
|
99
|
+
Digest.new(@document, registry: @registry)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def beginnings
|
|
103
|
+
@document.beginnings.drop(1).map { |id| Violation.new(node: id, problem: :many_beginnings) } + led_into
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def led_into
|
|
107
|
+
@document.beginnings.select { |id| @document.edges.any? { |edge| edge.to == id } }
|
|
108
|
+
.map { |id| Violation.new(node: id, problem: :before_the_beginning) }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def missing_values
|
|
112
|
+
@document.nodes.flat_map { |node| missing_values_in(node) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def missing_values_in(node)
|
|
116
|
+
drawn_of(node).filter_map do |name, source|
|
|
117
|
+
chosen = node.config[name.to_s]
|
|
118
|
+
next if chosen.blank? || node.config[source.to_s].blank?
|
|
119
|
+
next if offered_to(node, source).include?(chosen.to_s)
|
|
120
|
+
|
|
121
|
+
Violation.new(node: node.id, problem: :missing_value, detail: chosen.to_s)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def offered_to(node, source)
|
|
126
|
+
naming = @registry.fetch(node.type).settings.outputs_of[source]
|
|
127
|
+
|
|
128
|
+
digest.values_of(node.config[naming.to_s], node.config[source.to_s]).map { |value| value["value"].to_s }
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def drawn_of(node)
|
|
132
|
+
return {} unless @registry.registered?(node.type)
|
|
133
|
+
|
|
134
|
+
@registry.fetch(node.type).settings.drawn_from
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def missing_settings
|
|
138
|
+
@document.nodes.flat_map do |node|
|
|
139
|
+
required_of(node).reject { |name| node.config[name.to_s].present? }
|
|
140
|
+
.map { |name| Violation.new(node: node.id, problem: :missing_setting, detail: name.to_s) }
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def required_of(node)
|
|
145
|
+
return [] unless @registry.registered?(node.type)
|
|
146
|
+
|
|
147
|
+
@registry.fetch(node.type).settings.required
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def requirements_for(node)
|
|
151
|
+
return [] unless @registry.registered?(node.type)
|
|
152
|
+
|
|
153
|
+
@registry.fetch(node.type).settings.requirements_for(node.config)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def precedes_every_path?(required, id)
|
|
157
|
+
known?(required) && !@document.reachable(without: required).include?(id)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def unreachable
|
|
161
|
+
return [] unless known?(@document.entry)
|
|
162
|
+
|
|
163
|
+
(@document.nodes.map(&:id).uniq - @document.reachable).map { |id| Violation.new(node: id, problem: :unreachable) }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def known?(id)
|
|
167
|
+
@document.node(id).present?
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
class Version < ApplicationRecord
|
|
4
|
+
self.table_name = "alembic_flow_versions"
|
|
5
|
+
|
|
6
|
+
enum :status, { draft: "draft", live: "live", superseded: "superseded",
|
|
7
|
+
retired: "retired", withdrawn: "withdrawn" }
|
|
8
|
+
|
|
9
|
+
belongs_to :flow, class_name: "Alembic::Flow::Definition"
|
|
10
|
+
|
|
11
|
+
validates :number, uniqueness: { scope: :flow_id }
|
|
12
|
+
|
|
13
|
+
def out_of_service?
|
|
14
|
+
retired? || withdrawn?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def changes
|
|
18
|
+
changes_captured.to_a
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
FROZEN = %w[definition number flow_id changes_captured].freeze
|
|
22
|
+
|
|
23
|
+
before_update { raise ActiveRecord::ReadOnlyRecord if changed.intersect?(FROZEN) }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Flow
|
|
3
|
+
FIELD_TYPES = %i[string integer float boolean select multi_select previous_step from_step list].freeze
|
|
4
|
+
|
|
5
|
+
OUTPUT_TYPES = %i[string integer float boolean].freeze
|
|
6
|
+
OPTIONAL_CHECKS = %i[unrouted_value unfollowed_path dead_end].freeze
|
|
7
|
+
|
|
8
|
+
class UnknownFieldType < ArgumentError; end
|
|
9
|
+
class UnknownOutputType < ArgumentError; end
|
|
10
|
+
class UnknownStepType < KeyError; end
|
|
11
|
+
class UnknownCheck < ArgumentError; end
|
|
12
|
+
class InvalidEdit < StandardError; end
|
|
13
|
+
|
|
14
|
+
DRAWING = "alembic/flow/steps/choosing".freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def draws_with(template)
|
|
18
|
+
@drawing = template
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def drawing
|
|
22
|
+
@drawing.presence || DRAWING
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def step(id, &declaration)
|
|
26
|
+
registry.register(StepType.define(id, &declaration))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def registry
|
|
30
|
+
@registry ||= Registry.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def check(name)
|
|
34
|
+
raise UnknownCheck, "#{name} is not one of #{OPTIONAL_CHECKS.join(', ')}" unless OPTIONAL_CHECKS.include?(name)
|
|
35
|
+
|
|
36
|
+
checks << name unless checks.include?(name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def checks
|
|
40
|
+
@checks ||= []
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Band
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:band) do
|
|
6
|
+
label "Band"
|
|
7
|
+
compute { |config, _run, so_far| Band.covering(config, so_far[config["of"]]) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.covering(config, number)
|
|
16
|
+
ordered = Array(config["bands"]).sort_by { |band| band["ceiling"] || Float::INFINITY }
|
|
17
|
+
|
|
18
|
+
ordered.find { |band| band["ceiling"].nil? || number < band["ceiling"] }&.fetch("name", nil)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Grouped
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:grouped) do
|
|
6
|
+
label "By area"
|
|
7
|
+
compute { |config, run, _so_far| Grouped.shares(run, config["by"].presence) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.bucket(step, marker)
|
|
16
|
+
marker ? step[marker] : Steps::Question.category_of(step)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.shares(run, marker)
|
|
20
|
+
run.state.keys.group_by { |id| Grouped.bucket(run.step(id), marker) }
|
|
21
|
+
.except(nil)
|
|
22
|
+
.transform_values { |ids| Percentage.of(run, ids) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Lowest
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:lowest) do
|
|
6
|
+
label "Weakest"
|
|
7
|
+
compute { |config, _run, so_far| Lowest.of(so_far[config["of"]], config["count"]) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.of(shares, count)
|
|
16
|
+
shares.to_h.sort_by { |_name, share| share }.first(count.presence&.to_i || 1).map(&:first)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Percentage
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:percentage) do
|
|
6
|
+
label "Share"
|
|
7
|
+
compute { |_config, run, _so_far| Percentage.of(run, run.state.keys) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.of(run, ids)
|
|
16
|
+
offered = ids.sum { |id| most(run.step(id)) }
|
|
17
|
+
return 0 if offered.zero?
|
|
18
|
+
|
|
19
|
+
(captured(run, ids).to_f / offered * 100).round
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.captured(run, ids)
|
|
23
|
+
ids.sum { |id| WeightedSum.weight_of(run.step(id), run.state[id]) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.most(step)
|
|
27
|
+
Steps::Question.answers_of(step).filter_map { |option| option["weight"].to_i if option.is_a?(Hash) }.max.to_i
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module Tally
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:tally) do
|
|
6
|
+
label "How many"
|
|
7
|
+
compute { |config, run, _so_far| Tally.counted(run, config["tag"], config["by"].presence) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.counted(run, tag, marker)
|
|
16
|
+
answered = run.state.keys
|
|
17
|
+
return answered.size if tag.blank?
|
|
18
|
+
|
|
19
|
+
answered.count { |id| Grouped.bucket(run.step(id), marker) == tag }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Outputs
|
|
3
|
+
module WeightedSum
|
|
4
|
+
def self.output_type
|
|
5
|
+
Summary::OutputType.define(:weighted_sum) do
|
|
6
|
+
label "Score"
|
|
7
|
+
compute { |_config, run, _so_far| WeightedSum.total(run) }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.register(registry = Summary.registry)
|
|
12
|
+
registry.register(output_type)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.total(run)
|
|
16
|
+
run.state.sum { |id, value| weight_of(run.step(id), value) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.weight_of(step, value)
|
|
20
|
+
chosen = Steps::Question.answers_of(step).find { |option| option.is_a?(Hash) && option["value"] == value }
|
|
21
|
+
|
|
22
|
+
chosen&.fetch("weight", nil).to_i
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
class Runner < Flow::Runner
|
|
3
|
+
def question_text(id)
|
|
4
|
+
Steps::Question.asked(step(id)&.config)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def choice_label(id, value)
|
|
8
|
+
chosen = Steps::Question.choices_in(step(id)).find { |choice| choice.value == value }
|
|
9
|
+
|
|
10
|
+
chosen&.label.presence || value
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Steps
|
|
3
|
+
class Question
|
|
4
|
+
include Flow::Step
|
|
5
|
+
|
|
6
|
+
step_name "Question"
|
|
7
|
+
|
|
8
|
+
setting :question, type: :string
|
|
9
|
+
setting :category, type: :string
|
|
10
|
+
setting :answers, type: :list, required: true do
|
|
11
|
+
setting :value, type: :string
|
|
12
|
+
setting :label, type: :string
|
|
13
|
+
setting :weight, type: :integer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
output :answer, type: :string, label: "Answer", values: ->(node) { Question.offered(node.config) }
|
|
17
|
+
|
|
18
|
+
names_by :question
|
|
19
|
+
awaits_input
|
|
20
|
+
|
|
21
|
+
displays_by { |node| Asked.new(id: node.id.to_sym, text: asked(node.config), choices: choices_in(node)) }
|
|
22
|
+
|
|
23
|
+
def self.asked(step)
|
|
24
|
+
step.to_h["question"] || step.to_h["text"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.answers_of(step)
|
|
28
|
+
Array(step.to_h["answers"] || step.to_h["options"])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.offered(step)
|
|
32
|
+
answers_of(step).map do |answer|
|
|
33
|
+
{ "value" => answer["value"], "label" => answer["label"].presence || answer["value"] }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.choices_in(node)
|
|
38
|
+
answers_of(node.config).map { |answer| choice_from(answer) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.choice_from(answer)
|
|
42
|
+
return Choice.new(value: answer) unless answer.is_a?(Hash)
|
|
43
|
+
|
|
44
|
+
Choice.new(value: answer["value"], label: answer["label"], hint: answer["hint"])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def self.category_of(step)
|
|
48
|
+
step.to_h["category"] || step.to_h["tag"]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Summary
|
|
3
|
+
class OutputType
|
|
4
|
+
def self.define(id, &declaration)
|
|
5
|
+
Declaration.new(id).tap { |decl| decl.instance_eval(&declaration) }.to_output_type
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
attr_reader :id, :label
|
|
9
|
+
|
|
10
|
+
def initialize(id:, label:, computation:)
|
|
11
|
+
@id = id
|
|
12
|
+
@label = label
|
|
13
|
+
@computation = computation
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def compute(config, state, so_far)
|
|
17
|
+
@computation&.call(config, state, so_far)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class Declaration
|
|
21
|
+
def initialize(id)
|
|
22
|
+
@id = id
|
|
23
|
+
@label = id.to_s
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def label(value)
|
|
27
|
+
@label = value
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def compute(&computation)
|
|
31
|
+
@computation = computation
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_output_type
|
|
35
|
+
OutputType.new(id: @id, label: @label, computation: @computation)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Summary
|
|
3
|
+
class Registry
|
|
4
|
+
def initialize
|
|
5
|
+
@output_types = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def register(output_type)
|
|
9
|
+
@output_types[output_type.id.to_sym] = output_type
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def fetch(id)
|
|
13
|
+
@output_types.fetch(id.to_sym) { raise UnknownOutputType, "no output type registered as #{id}" }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def registered?(id)
|
|
17
|
+
id.present? && @output_types.key?(id.to_sym)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def output_types
|
|
21
|
+
@output_types.values
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Alembic
|
|
2
|
+
module Summary
|
|
3
|
+
class Report
|
|
4
|
+
def initialize(configuration, registry: Summary.registry)
|
|
5
|
+
@configuration = configuration.to_h
|
|
6
|
+
@registry = registry
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def results(run)
|
|
10
|
+
produced = {}
|
|
11
|
+
|
|
12
|
+
outputs.map do |output|
|
|
13
|
+
output_type = @registry.fetch(output["type"])
|
|
14
|
+
produced[output["id"]] = output_type.compute(output, run, produced)
|
|
15
|
+
|
|
16
|
+
Result.new(id: output["id"], label: label_for(output, output_type), value: produced[output["id"]])
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def outputs
|
|
23
|
+
Array(@configuration["outputs"])
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def label_for(output, output_type)
|
|
27
|
+
output["label"].presence || output_type.label
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|