hecks 1.0.2 → 1.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 +4 -4
- data/lib/hecks/adapters/driven/claude_code.rb +3 -3
- data/lib/hecks/adapters/driven/d1.rb +48 -28
- data/lib/hecks/adapters/driven/folder.rb +10 -3
- data/lib/hecks/adapters/driven/google_authentication.rb +2 -4
- data/lib/hecks/adapters/driven/heki/journal.rb +1 -1
- data/lib/hecks/adapters/driven/heki.rb +3 -5
- data/lib/hecks/adapters/driven/in_memory_ordering.rb +4 -1
- data/lib/hecks/adapters/driven/lambda.rb +2 -2
- data/lib/hecks/adapters/driven/local_storage.adapter +3 -0
- data/lib/hecks/adapters/driven/local_storage.rb +120 -0
- data/lib/hecks/adapters/driven/memory.rb +48 -3
- data/lib/hecks/adapters/driven/postgres/codec.rb +3 -1
- data/lib/hecks/adapters/driven/postgres/outbox.rb +97 -0
- data/lib/hecks/adapters/driven/postgres/schema_builder.rb +3 -2
- data/lib/hecks/adapters/driven/postgres.rb +25 -14
- data/lib/hecks/adapters/driven/prism.rb +11 -0
- data/lib/hecks/adapters/driven/sql_query_builder.rb +45 -20
- data/lib/hecks/adapters/driven/sqlite/codec.rb +3 -1
- data/lib/hecks/adapters/driven/sqlite/projection.rb +17 -1
- data/lib/hecks/adapters/driven/sqlite/schema_builder.rb +23 -1
- data/lib/hecks/adapters/driven/sqlite.rb +87 -8
- data/lib/hecks/adapters/driven.rb +4 -0
- data/lib/hecks/adapters.rb +4 -0
- data/lib/hecks/behaviors/dsl.rb +7 -0
- data/lib/hecks/behaviors/expectations.rb +32 -4
- data/lib/hecks/behaviors/rspec.rb +2 -0
- data/lib/hecks/behaviors/runner.rb +4 -0
- data/lib/hecks/bluebook/aggregate.rb +6 -5
- data/lib/hecks/bluebook/assembly/aggregate_assembly.rb +4 -4
- data/lib/hecks/bluebook/assembly/build.rb +6 -3
- data/lib/hecks/bluebook/assembly/contract.rb +5 -1
- data/lib/hecks/bluebook/assembly/contracts.rb +4 -0
- data/lib/hecks/bluebook/assembly/marks.rb +2 -1
- data/lib/hecks/bluebook/assembly.rb +1 -3
- data/lib/hecks/bluebook/attribute.rb +6 -0
- data/lib/hecks/bluebook/behaviour/policy.rb +18 -0
- data/lib/hecks/bluebook/behaviour/process_manager.rb +17 -1
- data/lib/hecks/bluebook/behaviour/traits.rb +1 -1
- data/lib/hecks/bluebook/command.rb +6 -5
- data/lib/hecks/bluebook/dsl/adapter_builder.rb +4 -1
- data/lib/hecks/bluebook/dsl/aggregate_builder/sealing.rb +524 -0
- data/lib/hecks/bluebook/dsl/aggregate_builder.rb +19 -472
- data/lib/hecks/bluebook/dsl/attribute_collector.rb +14 -6
- data/lib/hecks/bluebook/dsl/binding_proxy.rb +5 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder/validation.rb +848 -0
- data/lib/hecks/bluebook/dsl/bluebook_builder.rb +25 -784
- data/lib/hecks/bluebook/dsl/command_builder.rb +53 -13
- data/lib/hecks/bluebook/dsl/const_shim.rb +6 -0
- data/lib/hecks/bluebook/dsl/domain_port_builder.rb +20 -7
- data/lib/hecks/bluebook/dsl/entity_builder.rb +27 -6
- data/lib/hecks/bluebook/dsl/generic_dispatch.rb +12 -2
- data/lib/hecks/bluebook/dsl/hecksagon_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/identity_declaration.rb +24 -12
- data/lib/hecks/bluebook/dsl/lifecycle_builder.rb +40 -1
- data/lib/hecks/bluebook/dsl/policy_builder.rb +10 -1
- data/lib/hecks/bluebook/dsl/port_builder.rb +6 -1
- data/lib/hecks/bluebook/dsl/port_operation_builder.rb +33 -12
- data/lib/hecks/bluebook/dsl/process_manager_builder.rb +56 -12
- data/lib/hecks/bluebook/dsl/query_builder.rb +7 -1
- data/lib/hecks/bluebook/dsl/read_model_builder.rb +11 -3
- data/lib/hecks/bluebook/dsl/rule_reference.rb +3 -1
- data/lib/hecks/bluebook/dsl/translation_builder.rb +13 -2
- data/lib/hecks/bluebook/dsl/value_object_builder.rb +8 -1
- data/lib/hecks/bluebook/dsl/word_gate.rb +5 -0
- data/lib/hecks/bluebook/dsl/world_builder.rb +13 -3
- data/lib/hecks/bluebook/entity.rb +5 -3
- data/lib/hecks/bluebook/expression/ast_json.rb +88 -9
- data/lib/hecks/bluebook/expression/ast_reader.rb +107 -0
- data/lib/hecks/bluebook/expression/canonical_form.rb +7 -0
- data/lib/hecks/bluebook/expression/evaluator.rb +64 -11
- data/lib/hecks/bluebook/expression/projection.json +12 -0
- data/lib/hecks/bluebook/expression/resolver/block_predicates.rb +4 -3
- data/lib/hecks/bluebook/expression/resolver.rb +83 -11
- data/lib/hecks/bluebook/expression.rb +1 -0
- data/lib/hecks/bluebook/hexagon.rb +10 -0
- data/lib/hecks/bluebook/lifecycle.rb +9 -0
- data/lib/hecks/bluebook/meta_validator/judge.rb +10 -4
- data/lib/hecks/bluebook/meta_validator/plan.rb +4 -0
- data/lib/hecks/bluebook/meta_validator/reconstruction.rb +1 -1
- data/lib/hecks/bluebook/meta_validator/shapes.rb +3 -3
- data/lib/hecks/bluebook/meta_validator/syntax_boot.rb +7 -4
- data/lib/hecks/bluebook/meta_validator/translation_judge.rb +42 -3
- data/lib/hecks/bluebook/meta_validator/world_judge.rb +1 -1
- data/lib/hecks/bluebook/meta_validator.rb +5 -5
- data/lib/hecks/bluebook/model_check.rb +119 -82
- data/lib/hecks/bluebook/pattern_subset.rb +9 -0
- data/lib/hecks/bluebook/policy.rb +2 -1
- data/lib/hecks/bluebook/process_manager.rb +6 -0
- data/lib/hecks/bluebook/project_register.rb +45 -25
- data/lib/hecks/bluebook/query.rb +4 -0
- data/lib/hecks/bluebook/read_model.rb +2 -2
- data/lib/hecks/bluebook/smoke_test.rb +5 -0
- data/lib/hecks/bluebook/synthesizer.rb +2 -1
- data/lib/hecks/bluebook/translation.rb +3 -0
- data/lib/hecks/bluebook/value_object.rb +2 -2
- data/lib/hecks/codemod.rb +13 -0
- data/lib/hecks/doc/reference.rb +17 -11
- data/lib/hecks/facade/cli_runner.rb +2 -2
- data/lib/hecks/facade/command_request.rb +11 -11
- data/lib/hecks/facade/handle.rb +3 -3
- data/lib/hecks/facade/surface/aggregate_door.rb +52 -38
- data/lib/hecks/facade.rb +4 -0
- data/lib/hecks/forms/app.rb +10 -6
- data/lib/hecks/forms/command_form_renderer.rb +3 -3
- data/lib/hecks/forms/field_renderer.rb +14 -9
- data/lib/hecks/forms/field_shape.rb +7 -2
- data/lib/hecks/forms/html.rb +5 -2
- data/lib/hecks/forms/index_renderer.rb +1 -1
- data/lib/hecks/forms/page.rb +7 -3
- data/lib/hecks/forms/query_form_renderer.rb +3 -2
- data/lib/hecks/forms/record_renderer.rb +22 -16
- data/lib/hecks/forms.rb +4 -0
- data/lib/hecks/fqn.rb +21 -9
- data/lib/hecks/fuzzing/bounded_exhaustive_expressions.rb +30 -8
- data/lib/hecks/fuzzing/invalid_value_generator.rb +0 -1
- data/lib/hecks/fuzzing/isolated_boot.rb +14 -6
- data/lib/hecks/fuzzing/properties/dispatch_and_mutations.rb +264 -0
- data/lib/hecks/fuzzing/properties/guards.rb +251 -0
- data/lib/hecks/fuzzing/properties/invariants_and_aggregation.rb +351 -0
- data/lib/hecks/fuzzing/properties/lifecycle_and_replay.rb +99 -0
- data/lib/hecks/fuzzing/properties/querying.rb +206 -0
- data/lib/hecks/fuzzing/properties.rb +41 -1065
- data/lib/hecks/fuzzing/replay.rb +60 -16
- data/lib/hecks/fuzzing/sequence_generator/catalog.rb +6 -2
- data/lib/hecks/fuzzing/sequence_generator/step_builder.rb +2 -1
- data/lib/hecks/fuzzing/sequence_generator.rb +0 -1
- data/lib/hecks/fuzzing/value_generator.rb +2 -2
- data/lib/hecks/grammar/evolve.rb +34 -10
- data/lib/hecks/grammar/expression_operators.json +124 -0
- data/lib/hecks/grammar.rb +15 -5
- data/lib/hecks/ir.rb +9 -0
- data/lib/hecks/language/bluebook/aggregate.bluebook +4 -2
- data/lib/hecks/language/bluebook/command.bluebook +2 -1
- data/lib/hecks/language/bluebook/entity.bluebook +2 -1
- data/lib/hecks/language/bluebook/projection.bluebook +2 -1
- data/lib/hecks/language/bluebook/query.bluebook +4 -2
- data/lib/hecks/language/bluebook/shape.bluebook +2 -1
- data/lib/hecks/language/bluebook/vocabulary.bluebook +2 -0
- data/lib/hecks/literal.rb +18 -8
- data/lib/hecks/naming.rb +6 -3
- data/lib/hecks/ports/access_control.rb +1 -1
- data/lib/hecks/ports/agent.rb +5 -3
- data/lib/hecks/ports/authentication.rb +3 -2
- data/lib/hecks/ports/authorization.rb +1 -1
- data/lib/hecks/ports/clock.rb +1 -1
- data/lib/hecks/ports/extraction.rb +5 -1
- data/lib/hecks/ports/identity_assignment.rb +1 -1
- data/lib/hecks/ports/identity_generation.rb +1 -1
- data/lib/hecks/ports/identity_resolution.rb +1 -1
- data/lib/hecks/ports/loading.rb +4 -1
- data/lib/hecks/ports/persistence/append_only.rb +40 -4
- data/lib/hecks/ports/persistence/binding_policy.rb +1 -1
- data/lib/hecks/ports/persistence/plugin.rb +3 -0
- data/lib/hecks/ports/persistence/plugins/era/era_check.rb +3 -1
- data/lib/hecks/ports/persistence/plugins/era/era_guard/shape_diff.rb +5 -5
- data/lib/hecks/ports/persistence/plugins/era/lineage.rb +14 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/era_store.rb +4 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/field_cache.rb +1 -1
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/head_compiler.rb +80 -16
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/mint_transaction.rb +23 -6
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/provisioning.rb +43 -5
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/tail_merge.rb +25 -4
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage/transform_installer.rb +49 -9
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/coverage_check.rb +2 -2
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/era_resolver.rb +10 -0
- data/lib/hecks/ports/persistence/plugins/era/postgres_era/lineage_manager/minter.rb +49 -38
- data/lib/hecks/ports/persistence/plugins/era/postgres_era.rb +23 -13
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_one.rb +19 -21
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/layer_two.rb +27 -9
- data/lib/hecks/ports/persistence/plugins/era/translation/audit/unfed_report.rb +9 -0
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/differ.rb +25 -10
- data/lib/hecks/ports/persistence/plugins/era/translation/scaffold/renderer.rb +2 -6
- data/lib/hecks/ports/persistence/plugins/era.rb +3 -1
- data/lib/hecks/ports/persistence/remote_runtime.rb +1 -1
- data/lib/hecks/ports/persistence.rb +7 -3
- data/lib/hecks/ports/projection.rb +13 -5
- data/lib/hecks/ports/query/in_memory.rb +8 -2
- data/lib/hecks/ports/query.rb +1 -3
- data/lib/hecks/ports.rb +2 -0
- data/lib/hecks/projections/diagrams.rb +11 -3
- data/lib/hecks/projections/ir.rb +2 -0
- data/lib/hecks/projections/model/deviations.rb +2 -1
- data/lib/hecks/projections/model.rb +15 -10
- data/lib/hecks/projections/vocabulary.rb +1 -3
- data/lib/hecks/projector/cli_projector.rb +49 -29
- data/lib/hecks/projector/docs_projector.rb +21 -12
- data/lib/hecks/projector/exporter.rb +10 -1
- data/lib/hecks/projector/narrate_projector.rb +83 -25
- data/lib/hecks/projector.rb +2 -2
- data/lib/hecks/query_ir.rb +60 -36
- data/lib/hecks/query_specification/common/authorization_spec.rb +1 -1
- data/lib/hecks/query_specification/common/comparators.rb +4 -0
- data/lib/hecks/query_specification/common/comparison.rb +8 -1
- data/lib/hecks/query_specification/common/dsl.rb +5 -0
- data/lib/hecks/query_specification/common/null_policy.rb +6 -2
- data/lib/hecks/query_specification/common/options.rb +17 -5
- data/lib/hecks/query_specification/field_path.rb +4 -4
- data/lib/hecks/query_specification/read_model/specification.rb +5 -2
- data/lib/hecks/router/namespace_installer.rb +22 -10
- data/lib/hecks/runtime/command_interpreter/mutation_applier.rb +57 -22
- data/lib/hecks/runtime/command_interpreter.rb +104 -65
- data/lib/hecks/runtime/command_rules/admissibility.rb +26 -8
- data/lib/hecks/runtime/command_rules/arithmetic.rb +35 -14
- data/lib/hecks/runtime/dependency_planning.rb +33 -21
- data/lib/hecks/runtime/dispatcher.rb +41 -7
- data/lib/hecks/runtime/entity_element.rb +25 -6
- data/lib/hecks/runtime/entity_interpreter.rb +28 -15
- data/lib/hecks/runtime/errors.rb +28 -2
- data/lib/hecks/runtime/instance.rb +9 -3
- data/lib/hecks/runtime/interpreting.rb +29 -2
- data/lib/hecks/runtime/loader.rb +21 -0
- data/lib/hecks/runtime/outbox.rb +365 -0
- data/lib/hecks/runtime/policy_interpreter.rb +32 -17
- data/lib/hecks/runtime/query_interpreter.rb +36 -16
- data/lib/hecks/runtime/reaction_invocation.rb +35 -22
- data/lib/hecks/runtime/read_model_interpreter.rb +26 -3
- data/lib/hecks/runtime/reference_hop.rb +1 -1
- data/lib/hecks/runtime/refusal_wording.rb +55 -49
- data/lib/hecks/runtime/registry/saga_persistence.rb +3 -1
- data/lib/hecks/runtime/registry/verification.rb +39 -2
- data/lib/hecks/runtime/registry.rb +21 -11
- data/lib/hecks/runtime/remote_dispatcher.rb +3 -1
- data/lib/hecks/runtime/routing.rb +52 -39
- data/lib/hecks/runtime/saga_interpreter/correlation.rb +7 -7
- data/lib/hecks/runtime/saga_interpreter.rb +119 -90
- data/lib/hecks/runtime/saga_pending_dispatch.rb +6 -3
- data/lib/hecks/runtime/tenant_scope.rb +7 -0
- data/lib/hecks/runtime/value/coercion.rb +207 -48
- data/lib/hecks/runtime/value.rb +1 -1
- data/lib/hecks/runtime.rb +7 -2
- data/lib/hecks/storehouse.rb +15 -8
- data/lib/hecks/version.rb +1 -1
- data/lib/hecks/vocabulary.rb +1 -0
- data/lib/hecks.rb +17 -13
- data/lib/rubocop/cop/hecks/fallback_hash_lookup.rb +1 -1
- data/lib/rubocop/cop/hecks/sequential_hash_rename_in_loop.rb +1 -1
- data/lib/rubocop/cop/hecks/thread_shared_ivar_mutation.rb +1 -1
- metadata +14 -2
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
require "set"
|
|
2
|
-
|
|
3
1
|
module Hecks
|
|
4
2
|
module Bluebook
|
|
5
3
|
# Lightweight formal methods over the IR — the same family as TLA+/
|
|
@@ -89,7 +87,7 @@ module Hecks
|
|
|
89
87
|
findings.concat(lifecycle_findings(aggregate, aggregate))
|
|
90
88
|
aggregate.entities.each { |entity| findings.concat(lifecycle_findings(aggregate, entity)) }
|
|
91
89
|
end
|
|
92
|
-
bluebook.process_managers.each { |
|
|
90
|
+
bluebook.process_managers.each { |process_manager| findings.concat(saga_findings(bluebook, process_manager)) }
|
|
93
91
|
bluebook.policies.each { |policy| findings.concat(policy_findings(bluebook, policy, hecksagon, known_domains)) }
|
|
94
92
|
findings
|
|
95
93
|
end
|
|
@@ -103,38 +101,46 @@ module Hecks
|
|
|
103
101
|
subject = declaring.equal?(aggregate) ? aggregate.hecks_name : "#{aggregate.hecks_name}::#{declaring.hecks_name}"
|
|
104
102
|
commands = Array(declaring.commands).map(&:hecks_name)
|
|
105
103
|
|
|
106
|
-
findings = []
|
|
107
|
-
findings.concat(unknown_transition_commands(lifecycle, commands, subject))
|
|
108
|
-
|
|
109
104
|
full = full_states(lifecycle)
|
|
110
105
|
reached = reachable_states(lifecycle)
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
findings = []
|
|
108
|
+
findings.concat(unknown_transition_commands(lifecycle, commands, subject))
|
|
109
|
+
findings.concat(unreachable_state_findings(lifecycle, full, reached, subject))
|
|
110
|
+
findings.concat(dead_transition_findings(lifecycle, reached, subject))
|
|
111
|
+
findings.concat(stuck_state_findings(lifecycle, reached, subject))
|
|
112
|
+
findings
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def unreachable_state_findings(lifecycle, full, reached, subject)
|
|
116
|
+
(full - reached.to_a).map do |state|
|
|
117
|
+
Finding.new(kind: :unreachable_state, severity: :error, subject: subject,
|
|
118
|
+
message: "#{state.inspect} is declared (in a transition's from: or target) " \
|
|
119
|
+
"but no path from #{lifecycle.default.inspect} ever reaches it")
|
|
116
120
|
end
|
|
121
|
+
end
|
|
117
122
|
|
|
118
|
-
|
|
123
|
+
def dead_transition_findings(lifecycle, reached, subject)
|
|
124
|
+
lifecycle.transitions.filter_map do |command, transition|
|
|
119
125
|
next unless transition.constrained?
|
|
120
126
|
next if Array(transition.from).any? { |source| reached.include?(source) }
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
Finding.new(kind: :dead_transition, severity: :error, subject: subject,
|
|
129
|
+
message: "#{command} from #{Array(transition.from).inspect} can never fire — " \
|
|
130
|
+
"none of those states is ever reached")
|
|
125
131
|
end
|
|
132
|
+
end
|
|
126
133
|
|
|
134
|
+
def stuck_state_findings(lifecycle, reached, subject)
|
|
127
135
|
any_unconstrained = lifecycle.transitions.any? { |_, t| !t.constrained? }
|
|
128
|
-
(reached - terminal_exempt(lifecycle)).
|
|
136
|
+
(reached - terminal_exempt(lifecycle)).filter_map do |state|
|
|
129
137
|
next if any_unconstrained
|
|
130
138
|
next if lifecycle.transitions.any? { |_, t| t.constrained? && Array(t.from).include?(state) }
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
Finding.new(kind: :stuck_state, severity: :warning, subject: subject,
|
|
141
|
+
message: "#{state.inspect} is reached but no transition ever leaves it — " \
|
|
142
|
+
"fine if that is meant to be terminal")
|
|
135
143
|
end
|
|
136
|
-
|
|
137
|
-
findings
|
|
138
144
|
end
|
|
139
145
|
|
|
140
146
|
def unknown_transition_commands(lifecycle, commands, subject)
|
|
@@ -165,6 +171,11 @@ module Hecks
|
|
|
165
171
|
reached = Set.new([lifecycle.default])
|
|
166
172
|
loop do
|
|
167
173
|
grown = false
|
|
174
|
+
# `transitions` is an Array of [from, transition]-shaped entries
|
|
175
|
+
# (Lifecycle#transitions), not a Hash — Style/HashEachMethods'
|
|
176
|
+
# `each_value` rewrite assumed otherwise from the block shape
|
|
177
|
+
# alone. False positive.
|
|
178
|
+
# rubocop:disable-next Style/HashEachMethods
|
|
168
179
|
lifecycle.transitions.each do |_, transition|
|
|
169
180
|
next if reached.include?(transition.target)
|
|
170
181
|
next if transition.constrained? && Array(transition.from).none? { |source| reached.include?(source) }
|
|
@@ -202,79 +213,105 @@ module Hecks
|
|
|
202
213
|
|
|
203
214
|
# ── process managers / sagas ──────────────────────────────────────
|
|
204
215
|
|
|
205
|
-
def saga_findings(bluebook,
|
|
216
|
+
def saga_findings(bluebook, process_manager)
|
|
217
|
+
emitted = emitted_events(bluebook)
|
|
218
|
+
verbs = verbs_of(bluebook)
|
|
219
|
+
reached = pm_reachable_states(process_manager, emitted)
|
|
220
|
+
|
|
206
221
|
findings = []
|
|
207
|
-
emitted
|
|
208
|
-
|
|
222
|
+
findings.concat(deaf_trigger_findings(process_manager, emitted))
|
|
223
|
+
findings.concat(unreachable_pm_state_findings(process_manager, reached))
|
|
224
|
+
process_manager.handlers.each do |handler|
|
|
225
|
+
findings.concat(handler_findings(bluebook, process_manager, emitted, verbs, handler))
|
|
226
|
+
end
|
|
227
|
+
findings.concat(dead_compensation_findings(process_manager, reached))
|
|
228
|
+
findings
|
|
229
|
+
end
|
|
209
230
|
|
|
210
|
-
|
|
231
|
+
def deaf_trigger_findings(process_manager, emitted)
|
|
232
|
+
[process_manager.starts_on, process_manager.ends_on].compact.filter_map do |event|
|
|
211
233
|
next if emitted.include?(bare(event))
|
|
212
234
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
235
|
+
Finding.new(kind: :deaf_trigger, severity: :error, subject: process_manager.name,
|
|
236
|
+
message: "starts_on/ends_on names #{event.inspect}, which no command in this " \
|
|
237
|
+
"domain emits")
|
|
216
238
|
end
|
|
239
|
+
end
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
(Array(
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
def unreachable_pm_state_findings(process_manager, reached)
|
|
242
|
+
(Array(process_manager.states) - reached.to_a).map do |state|
|
|
243
|
+
Finding.new(kind: :unreachable_pm_state, severity: :error, subject: process_manager.name,
|
|
244
|
+
message: "#{state.inspect} is declared but no handler chain from " \
|
|
245
|
+
"#{process_manager.states.first.inspect} ever reaches it")
|
|
223
246
|
end
|
|
247
|
+
end
|
|
224
248
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
"in this domain emits")
|
|
234
|
-
end
|
|
249
|
+
# ONE HANDLER'S OWN FINDINGS — deaf_handler, unknown_dispatch (one
|
|
250
|
+
# per dispatch), and unarmed_compensation (one per compensating
|
|
251
|
+
# dispatch), pulled out of saga_findings' own handler loop; each
|
|
252
|
+
# check reads only this handler plus the domain-wide emitted/verbs
|
|
253
|
+
# sets saga_findings already resolved once, no state shared BETWEEN
|
|
254
|
+
# handlers.
|
|
255
|
+
def handler_findings(bluebook, process_manager, emitted, verbs, handler)
|
|
256
|
+
findings = []
|
|
235
257
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: pm.name,
|
|
246
|
-
message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
|
|
247
|
-
"declares no command at — cross-domain dispatch is out of this " \
|
|
248
|
-
"checker's scope, same as CommandRules#resolve_references")
|
|
249
|
-
end
|
|
258
|
+
# The compensating leg answers REFUSED, a synthetic trigger no
|
|
259
|
+
# command ever emits by name (ProcessManager::REFUSED) — not
|
|
260
|
+
# a deaf handler, the one handler this domain's own events can
|
|
261
|
+
# never satisfy on purpose.
|
|
262
|
+
if handler.event_type != ProcessManager::REFUSED && !emitted.include?(bare(handler.event_type))
|
|
263
|
+
findings << Finding.new(kind: :deaf_handler, severity: :error, subject: process_manager.name,
|
|
264
|
+
message: "a handler answers #{handler.event_type.inspect}, which no command " \
|
|
265
|
+
"in this domain emits")
|
|
266
|
+
end
|
|
250
267
|
|
|
251
|
-
|
|
252
|
-
#
|
|
253
|
-
#
|
|
254
|
-
#
|
|
255
|
-
#
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
268
|
+
handler.dispatches.each do |dispatch|
|
|
269
|
+
# SAME-DOMAIN, same as `SagaInterpreter#qualified` — a dispatch
|
|
270
|
+
# naming no domain at all (the ordinary shape a bare command
|
|
271
|
+
# constant now produces, S6) means THIS one, and is compared
|
|
272
|
+
# against `verbs_of`'s own fully-qualified spelling qualified
|
|
273
|
+
# the identical way, not left bare to miss it on a technicality.
|
|
274
|
+
qualified = if dispatch.command_name.include?("::")
|
|
275
|
+
dispatch.command_name
|
|
276
|
+
else
|
|
277
|
+
"#{bluebook.name}::#{dispatch.command_name}"
|
|
278
|
+
end
|
|
279
|
+
next if verbs.include?(qualified)
|
|
280
|
+
|
|
281
|
+
findings << Finding.new(kind: :unknown_dispatch, severity: :error, subject: process_manager.name,
|
|
282
|
+
message: "dispatches #{dispatch.command_name.inspect}, which this domain " \
|
|
283
|
+
"declares no command at — cross-domain dispatch is out of this " \
|
|
284
|
+
"checker's scope, same as CommandRules#resolve_references")
|
|
267
285
|
end
|
|
268
286
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
287
|
+
# A `compensates` DECLARED WITH NOWHERE TO EVER FIRE — the exact
|
|
288
|
+
# shape of the real bug this whole feature closes ("the
|
|
289
|
+
# reversal was written and never armed"), caught at build/
|
|
290
|
+
# model-check time instead of discovered in production. No
|
|
291
|
+
# handler anywhere answers REFUSED (`process_manager.saga?` false) means
|
|
292
|
+
# `SagaInterpreter#unwind` never runs for this process
|
|
293
|
+
# manager at all, so a declared `compensates` is structurally
|
|
294
|
+
# unreachable — not a warning about style, a dead declaration.
|
|
295
|
+
if !process_manager.saga? && handler.dispatches.any?(&:compensates)
|
|
296
|
+
handler.dispatches.select(&:compensates).each do |dispatch|
|
|
297
|
+
findings << Finding.new(kind: :unarmed_compensation, severity: :error, subject: process_manager.name,
|
|
298
|
+
message: "#{dispatch.command_name} compensates #{dispatch.compensates.command_name}, " \
|
|
299
|
+
"but no handler anywhere in this saga answers a refusal — the " \
|
|
300
|
+
"compensation is declared and can never fire")
|
|
301
|
+
end
|
|
273
302
|
end
|
|
274
303
|
|
|
275
304
|
findings
|
|
276
305
|
end
|
|
277
306
|
|
|
307
|
+
def dead_compensation_findings(process_manager, reached)
|
|
308
|
+
return [] unless process_manager.saga? && !reached.include?(process_manager.saga.from_state)
|
|
309
|
+
|
|
310
|
+
[Finding.new(kind: :dead_compensation, severity: :error, subject: process_manager.name,
|
|
311
|
+
message: "the compensation leaves #{process_manager.saga.from_state.inspect}, which no " \
|
|
312
|
+
"handler chain ever reaches — a refusal here can never fire it")]
|
|
313
|
+
end
|
|
314
|
+
|
|
278
315
|
# A handler edge is only usable in the closure if it can actually
|
|
279
316
|
# FIRE — REFUSED always can (it is a compensation trigger, not an
|
|
280
317
|
# event), and any other handler needs its event genuinely emitted.
|
|
@@ -283,13 +320,13 @@ module Hecks
|
|
|
283
320
|
# it, which would hide exactly the states this walk exists to
|
|
284
321
|
# catch (a state only "reachable" through a handler that itself
|
|
285
322
|
# never fires).
|
|
286
|
-
def pm_reachable_states(
|
|
287
|
-
return Set.new if Array(
|
|
323
|
+
def pm_reachable_states(process_manager, emitted)
|
|
324
|
+
return Set.new if Array(process_manager.states).empty?
|
|
288
325
|
|
|
289
|
-
reached = Set.new([
|
|
326
|
+
reached = Set.new([process_manager.states.first])
|
|
290
327
|
loop do
|
|
291
328
|
grown = false
|
|
292
|
-
|
|
329
|
+
process_manager.handlers.each do |handler|
|
|
293
330
|
next unless handler.event_type == ProcessManager::REFUSED || emitted.include?(bare(handler.event_type))
|
|
294
331
|
next unless reached.include?(handler.from_state)
|
|
295
332
|
next if reached.include?(handler.to_state)
|
|
@@ -446,9 +483,9 @@ module Hecks
|
|
|
446
483
|
bluebook.aggregates.flat_map do |aggregate|
|
|
447
484
|
verbs = aggregate.commands.map { |command| "#{bluebook.name}::#{aggregate.hecks_name}.#{command.hecks_name}" }
|
|
448
485
|
verbs + aggregate.entities.flat_map do |entity|
|
|
449
|
-
entity.commands.map
|
|
486
|
+
entity.commands.map do |command|
|
|
450
487
|
"#{bluebook.name}::#{aggregate.hecks_name}.#{entity.hecks_name}.#{command.hecks_name}"
|
|
451
|
-
|
|
488
|
+
end
|
|
452
489
|
end
|
|
453
490
|
end
|
|
454
491
|
end
|
|
@@ -63,6 +63,15 @@ module Hecks
|
|
|
63
63
|
# characters "(?=" and says nothing about lookahead — which is why this
|
|
64
64
|
# steps over each backslash pair rather than matching the pattern as a
|
|
65
65
|
# whole.
|
|
66
|
+
# One character walk is the subset's DEFINITION (see the method
|
|
67
|
+
# comment above): each construct-check is a branch in a single
|
|
68
|
+
# ordered pass sharing `index`/`in_class`/`class_start`. Splitting
|
|
69
|
+
# the branches into separate methods would force those three cursor
|
|
70
|
+
# variables to thread as parameters/return values between them,
|
|
71
|
+
# turning "the subset is this walk" into "the subset is these
|
|
72
|
+
# methods agreeing about a shared cursor" — exactly what the walk's
|
|
73
|
+
# own comment says a cleverer spelling would obscure.
|
|
74
|
+
# rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
66
75
|
def validate(pattern)
|
|
67
76
|
chars = pattern.to_s.chars
|
|
68
77
|
index = 0
|
|
@@ -16,7 +16,8 @@ module Hecks
|
|
|
16
16
|
target_domain: :target_domain,
|
|
17
17
|
where: :where,
|
|
18
18
|
for_each: :for_each,
|
|
19
|
-
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } }
|
|
19
|
+
with_spec: -> { with_spec.map { |key, value| [key.to_s, Bluebook.render_value(value)] } },
|
|
20
|
+
where_ast: -> { where_ast }
|
|
20
21
|
)
|
|
21
22
|
|
|
22
23
|
attr_reader :name, :on_event, :trigger_command, :target_domain, :where, :for_each, :with_spec
|
|
@@ -74,6 +74,12 @@ module Hecks
|
|
|
74
74
|
def to_s = "#{trigger} → #{to_state} (#{undoes.join(', ')})"
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# The built form of a `process_manager "Name" do ... end` block,
|
|
78
|
+
# produced by `DSL::ProcessManagerBuilder` — its start/end events,
|
|
79
|
+
# correlation field, derived states, and handler rows. Its
|
|
80
|
+
# compensation half (`saga`, `Behaviour::ProcessManager#saga`) is
|
|
81
|
+
# DERIVED from the handler answering `REFUSED`, below, not declared as
|
|
82
|
+
# its own construct.
|
|
77
83
|
class ProcessManager
|
|
78
84
|
# The BLUEBOOK's name for this construct, asked the same way of a class
|
|
79
85
|
# that has crossed over and of an IR object that has not. Collapses into
|
|
@@ -35,34 +35,56 @@ module Hecks
|
|
|
35
35
|
"#{bluebook.name} in #{directory} declares latest #{world.latest.inspect}, not #{bluebook.version.inspect}"
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
bluebook.aggregates.each
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
38
|
+
bluebook.aggregates.each { |aggregate| register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher) }
|
|
39
|
+
bluebook.read_models.each { |model| register_read_model!(model, bluebook, world, realm, directory, dispatcher) }
|
|
40
|
+
end
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Split out of `register` per-aggregate: same versioned-then-current
|
|
47
|
+
# registration shape as `register_read_model!`, just applied to a
|
|
48
|
+
# command/query pair instead of a single report. No state threads
|
|
49
|
+
# back to `register` — each `add` call is independent.
|
|
50
|
+
def register_aggregate!(aggregate, bluebook, world, realm, directory, dispatcher)
|
|
51
|
+
aggregate.commands.each do |command|
|
|
52
|
+
if bluebook.version
|
|
53
|
+
add(Fqn.command(realm: realm, domain: bluebook.name, version: bluebook.version,
|
|
54
|
+
aggregate: aggregate.hecks_name, command: command.hecks_name),
|
|
55
|
+
directory, dispatcher, command.hecks_name, bluebook.version)
|
|
54
56
|
end
|
|
55
|
-
bluebook
|
|
57
|
+
if current?(bluebook, world)
|
|
58
|
+
add(Fqn.command(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
|
|
59
|
+
command: command.hecks_name), directory, dispatcher, command.hecks_name, bluebook.version)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
aggregate.queries.each do |query|
|
|
63
|
+
name = Naming.snake(query.name)
|
|
64
|
+
if bluebook.version
|
|
56
65
|
add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
|
|
57
|
-
query:
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
aggregate: aggregate.hecks_name, query: name), directory, dispatcher, query.name, bluebook.version)
|
|
67
|
+
end
|
|
68
|
+
if current?(bluebook, world)
|
|
69
|
+
add(Fqn.query(realm: realm, domain: bluebook.name, aggregate: aggregate.hecks_name,
|
|
70
|
+
query: name), directory, dispatcher, query.name, bluebook.version)
|
|
60
71
|
end
|
|
61
72
|
end
|
|
62
|
-
self
|
|
63
73
|
end
|
|
64
74
|
|
|
65
|
-
|
|
75
|
+
# Split out of `register` per-read-model: registers the versioned FQN
|
|
76
|
+
# (if this bluebook declares a version) and the current/unversioned
|
|
77
|
+
# FQN (if this bluebook is the world's latest) for one report.
|
|
78
|
+
def register_read_model!(model, bluebook, world, realm, directory, dispatcher)
|
|
79
|
+
if bluebook.version
|
|
80
|
+
add(Fqn.query(realm: realm, domain: bluebook.name, version: bluebook.version,
|
|
81
|
+
query: model.query_name), directory, dispatcher, model.query_name, bluebook.version)
|
|
82
|
+
end
|
|
83
|
+
if current?(bluebook, world)
|
|
84
|
+
add(Fqn.query(realm: realm, domain: bluebook.name, query: model.query_name),
|
|
85
|
+
directory, dispatcher, model.query_name, bluebook.version)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
66
88
|
|
|
67
89
|
# THE ACTUAL "more than one tenant" MOMENT — Runtime::TenantCheck's
|
|
68
90
|
# own header names this table as the one place real multitenancy
|
|
@@ -93,9 +115,7 @@ module Hecks
|
|
|
93
115
|
|
|
94
116
|
def add(fqn, directory, dispatcher, declared_verb, domain_version)
|
|
95
117
|
existing = entries[fqn.to_s]
|
|
96
|
-
if existing
|
|
97
|
-
raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}"
|
|
98
|
-
end
|
|
118
|
+
raise DuplicateFqn, "#{fqn} is declared in both #{existing.source_directory} and #{directory}" if existing
|
|
99
119
|
|
|
100
120
|
entries[fqn.to_s] = Entry.new(
|
|
101
121
|
fqn: fqn, source_directory: directory, dispatcher: dispatcher,
|
data/lib/hecks/bluebook/query.rb
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
require_relative "behaviour/query"
|
|
2
2
|
|
|
3
3
|
module Hecks
|
|
4
|
+
# Reopened here only to add `render_value` below — the pinned
|
|
5
|
+
# wire-literal rendering (`Literal.render`) the `emits_ir` Procs
|
|
6
|
+
# throughout this chapter reach for. See lib/hecks/bluebook.rb's own
|
|
7
|
+
# header for what `Bluebook` is as a whole.
|
|
4
8
|
module Bluebook
|
|
5
9
|
def self.render_value(value) = Literal.render(value)
|
|
6
10
|
|
|
@@ -35,8 +35,8 @@ module Hecks
|
|
|
35
35
|
# `reference_target.nil?` stays a real, checkable fact for the
|
|
36
36
|
# interpreter instead of silently becoming `""`.
|
|
37
37
|
def initialize(name:, description: nil, reference_name: nil, reference_target: nil, aggregate_heads: [],
|
|
38
|
-
group_by: [], count: nil, median_field: nil, **
|
|
39
|
-
super(joins: aggregate_heads, **
|
|
38
|
+
group_by: [], count: nil, median_field: nil, **)
|
|
39
|
+
super(joins: aggregate_heads, **)
|
|
40
40
|
@name = name.to_s
|
|
41
41
|
@hecks_name = @name
|
|
42
42
|
@description = description
|
|
@@ -118,6 +118,11 @@ module Hecks
|
|
|
118
118
|
# the time that aggregate's own turn comes, the same reason
|
|
119
119
|
# `Synthesizer#args_for`'s `created` map is threaded through in
|
|
120
120
|
# this same order.
|
|
121
|
+
# `created` and `failures` thread through both the aggregate loop and the
|
|
122
|
+
# read_models loop below in the declaration order the comment above already
|
|
123
|
+
# documents; splitting would turn that ordering into an implicit contract on
|
|
124
|
+
# parameter/return passing instead of one visible sequence.
|
|
125
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
121
126
|
def smoke_domain(dispatcher, domain)
|
|
122
127
|
chapter = dispatcher.registry.bluebook(domain)
|
|
123
128
|
return [] unless chapter
|
|
@@ -64,7 +64,8 @@ module Hecks
|
|
|
64
64
|
chapter.aggregates.filter_map { |a| a.value_object(type_name) }.first
|
|
65
65
|
return "smoke-test" unless value_object
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
closed_set = value_object.respond_to?(:closed_set?) && value_object.closed_set? && value_object.members.any?
|
|
68
|
+
return value_object.members.first.to_h if closed_set
|
|
68
69
|
|
|
69
70
|
value_object.attributes.to_h { |field| [field.name, field_value_for(chapter, aggregate, field.type)] }
|
|
70
71
|
end
|
|
@@ -11,6 +11,9 @@ module Hecks
|
|
|
11
11
|
# Paths follow `TranslationMove`'s convention (dotted reaches a VO
|
|
12
12
|
# member); `values` maps every old-era value that can appear to its
|
|
13
13
|
# new-era replacement.
|
|
14
|
+
# `:values` shadows Struct#values on purpose (same reasoning as
|
|
15
|
+
# AttributeCollector::OneOf) — no caller reads the built-in behavior.
|
|
16
|
+
# rubocop:disable-next Lint/StructNewOverride
|
|
14
17
|
TranslationConvert = Struct.new(:from, :to, :values)
|
|
15
18
|
|
|
16
19
|
# A value object's or entity's own TYPE NAME changed with its member
|
|
@@ -3,7 +3,7 @@ require_relative "expression/ast_json"
|
|
|
3
3
|
|
|
4
4
|
module Hecks
|
|
5
5
|
module Bluebook
|
|
6
|
-
Invariant = Struct.new(:description, :canonical, :predicate, keyword_init: true)
|
|
6
|
+
Invariant = Struct.new(:description, :canonical, :predicate, :ast, keyword_init: true)
|
|
7
7
|
|
|
8
8
|
# A value object — a DECLARATION HOLDER, never instantiated.
|
|
9
9
|
#
|
|
@@ -37,7 +37,7 @@ module Hecks
|
|
|
37
37
|
# `Expression::AstJson`'s own header — built for `rust/host`'s
|
|
38
38
|
# own mint-time invariant check (`reference_validate.rs`), which
|
|
39
39
|
# has no kernel crate to parse `canonical` with.
|
|
40
|
-
invariants: -> { invariants.map { |rule|
|
|
40
|
+
invariants: -> { invariants.map { |rule| Expression::AstJson.rule_row(rule) } },
|
|
41
41
|
closed_set: :closed_set?,
|
|
42
42
|
# THE FIELD NAME IS STRINGIFIED, NEVER THE VALUE. A `member` row can
|
|
43
43
|
# hold any of the scalar types an attribute declares — `Integer 84`
|
data/lib/hecks/codemod.rb
CHANGED
|
@@ -221,7 +221,11 @@ module Hecks
|
|
|
221
221
|
# loop for why it can't batch) — splitting it across methods just
|
|
222
222
|
# to satisfy the line count would scatter state four ways for no
|
|
223
223
|
# readability gain.
|
|
224
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
224
225
|
# rubocop:disable-next Metrics/BlockLength
|
|
226
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
227
|
+
# rubocop:disable-next Metrics/MethodLength
|
|
228
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
225
229
|
def run_example_domains(results, dry_run)
|
|
226
230
|
Codemod::EXAMPLE_ROOTS.each do |domain_dir|
|
|
227
231
|
bluebook_files = Dir.glob(File.join(domain_dir, "bluebook", "*.bluebook"))
|
|
@@ -286,6 +290,15 @@ module Hecks
|
|
|
286
290
|
# changes the IR, so the running state's own export should always
|
|
287
291
|
# still equal the pristine original after each kept edit, by
|
|
288
292
|
# definition, no moving target needed.
|
|
293
|
+
# Same shape and same reason as run_example_domains just above (see
|
|
294
|
+
# its own comment) — the write/verify/revert sequence, PER
|
|
295
|
+
# CANDIDATE (this method's own comment explains why it cannot
|
|
296
|
+
# batch), is one coherent unit; splitting it would scatter
|
|
297
|
+
# before_meta/live_meta/applied_by_file state across methods for
|
|
298
|
+
# no gain.
|
|
299
|
+
# rubocop:disable-next Metrics/AbcSize
|
|
300
|
+
# rubocop:disable-next Metrics/CyclomaticComplexity
|
|
301
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
289
302
|
def run_meta_domain(results, dry_run)
|
|
290
303
|
before_meta = Codemod.boot_meta
|
|
291
304
|
meta_candidates = @find_candidates.call(Codemod.meta_registry)
|
data/lib/hecks/doc/reference.rb
CHANGED
|
@@ -114,7 +114,7 @@ module Hecks
|
|
|
114
114
|
by `bin/reference` — do not edit inside the markers. The prose
|
|
115
115
|
between them is hand-written and survives regeneration.*
|
|
116
116
|
#{GENERATED_END}
|
|
117
|
-
#{
|
|
117
|
+
#{"\n#{preamble}\n" unless preamble.empty?}
|
|
118
118
|
#{sections.join("\n")}
|
|
119
119
|
PAGE
|
|
120
120
|
end
|
|
@@ -145,7 +145,7 @@ module Hecks
|
|
|
145
145
|
## #{row[:word]}
|
|
146
146
|
|
|
147
147
|
#{generated_begin(row[:word])}
|
|
148
|
-
#{spellings}#{
|
|
148
|
+
#{spellings}#{" — #{facts.join(', ')}" unless facts.empty?}
|
|
149
149
|
#{table}#{GENERATED_END}
|
|
150
150
|
|
|
151
151
|
#{prose_or_sentinel(prose)}
|
|
@@ -181,7 +181,7 @@ module Hecks
|
|
|
181
181
|
name = arg[:at].to_s.empty? ? "`#{arg[:named]}:`" : "positional #{arg[:at]}"
|
|
182
182
|
lines << "| #{name} | #{arg[:kind]} | #{arg[:required]} | #{arg[:fills]} |"
|
|
183
183
|
end
|
|
184
|
-
lines.join("\n")
|
|
184
|
+
"#{lines.join("\n")}\n"
|
|
185
185
|
end
|
|
186
186
|
|
|
187
187
|
def render_index
|
|
@@ -210,6 +210,11 @@ module Hecks
|
|
|
210
210
|
# region existed has no generated marker ahead of its first `## `,
|
|
211
211
|
# so nothing is collecting when that heading arrives and no empty
|
|
212
212
|
# preamble is invented — the older shape reads back unchanged.
|
|
213
|
+
# A single-pass line-scanning state machine (current/collecting/
|
|
214
|
+
# buffer/in_fence) — each branch mutates shared local state that
|
|
215
|
+
# carries into the NEXT iteration, so splitting per branch would
|
|
216
|
+
# mean passing all four back and forth by reference every line.
|
|
217
|
+
# rubocop:disable-next Metrics/PerceivedComplexity
|
|
213
218
|
def harvest(text)
|
|
214
219
|
prose = {}
|
|
215
220
|
current = PREAMBLE
|
|
@@ -263,7 +268,7 @@ module Hecks
|
|
|
263
268
|
end
|
|
264
269
|
|
|
265
270
|
def guide_index(root)
|
|
266
|
-
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
271
|
+
paths = Dir.glob(File.join(root, "docs/implemented/guides/*.md"))
|
|
267
272
|
.reject { |p| %w[AUTHORING.md].include?(File.basename(p)) }
|
|
268
273
|
lines = paths.map do |path|
|
|
269
274
|
heading = File.foreach(path).find { |line| line.start_with?("# ") }
|
|
@@ -273,7 +278,7 @@ module Hecks
|
|
|
273
278
|
lines.join("\n")
|
|
274
279
|
end
|
|
275
280
|
|
|
276
|
-
def reference_index(
|
|
281
|
+
def reference_index(_root)
|
|
277
282
|
count = contexts.size
|
|
278
283
|
"[The DSL reference](docs/implemented/reference/index.md) — #{count} contexts, generated from " \
|
|
279
284
|
"the aggregate-local tables under `lib/hecks/language/` and held to them by " \
|
|
@@ -333,11 +338,11 @@ module Hecks
|
|
|
333
338
|
end
|
|
334
339
|
|
|
335
340
|
def corpus_roster(root)
|
|
336
|
-
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
341
|
+
dirs = Dir.glob(File.join(root, "examples/*/"))
|
|
337
342
|
lines = dirs.filter_map do |dir|
|
|
338
343
|
name = File.basename(dir.chomp("/"))
|
|
339
|
-
bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
|
|
340
|
-
bluebooks = Dir.glob(File.join(dir, "*.bluebook"))
|
|
344
|
+
bluebooks = Dir.glob(File.join(dir, "bluebook/*.bluebook"))
|
|
345
|
+
bluebooks = Dir.glob(File.join(dir, "*.bluebook")) if bluebooks.empty?
|
|
341
346
|
next if bluebooks.empty?
|
|
342
347
|
|
|
343
348
|
vision = bluebooks.filter_map { |bluebook| File.read(bluebook)[/vision\s+"([^"]*)"/, 1] }.first
|
|
@@ -364,7 +369,7 @@ module Hecks
|
|
|
364
369
|
# example — a word whose only "example" is invisible or inert is a
|
|
365
370
|
# word still shipping on its prose alone, which is the thing this
|
|
366
371
|
# gate exists to refuse.
|
|
367
|
-
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]
|
|
372
|
+
EXAMPLE_FENCE = /^```ruby(?: bluebook| boot)?[ \t]*$/
|
|
368
373
|
|
|
369
374
|
def exemplified?(prose) = prose.to_s.match?(EXAMPLE_FENCE)
|
|
370
375
|
|
|
@@ -378,12 +383,13 @@ module Hecks
|
|
|
378
383
|
# `harvest` already rejects empty prose and the TODO sentinel, so a
|
|
379
384
|
# word with nothing written for it arrives here with a nil.
|
|
380
385
|
def live_words(directory)
|
|
381
|
-
contexts.flat_map
|
|
386
|
+
rows = contexts.flat_map do |context|
|
|
382
387
|
path = File.join(directory, page_name(context))
|
|
383
388
|
prose = File.exist?(path) ? harvest(File.read(path)) : {}
|
|
384
389
|
keywords.select { |row| row[:context] == context && live?(row) }
|
|
385
390
|
.map { |row| [row[:word], context, prose[row[:word]]] }
|
|
386
|
-
|
|
391
|
+
end
|
|
392
|
+
rows.uniq { |word, context, _| [word, context] }
|
|
387
393
|
end
|
|
388
394
|
|
|
389
395
|
def name_of(word, context) = "#{word} (#{context})"
|